diff --git a/README.md b/README.md index e86de37..e09d1bd 100644 --- a/README.md +++ b/README.md @@ -4,20 +4,26 @@ Форк [Проекта](https://github.com/munrexio/yandex2mqtt) и [Статья на Хабре](https://habr.com/ru/post/465537/) к оригиналу. ## Важно -Те, кто пользуется оригинальным проектом (или его форками), обратите внимание на то, что немного изменились настройки устройств (блок **devices** в файле конфигурации). +Те, кто пользуется оригинальным проектом (или его форками), обратите внимание на то, что немного изменились настройки устройств (блок `devices` в файле конфигурации). На данный момент проверено получение температуры и влажности с датчиков (датчики дверей и движения пока в бета-тесте), и включение/выключение света (вкл./выкл. других устройств по аналогии тоже должно работать). Прочий функционал (изменение громкости, каналов, отключение звука), поидее, так же должны работать. ## ChangeLog +###### 16.05.2021 +Добавлено логирование некоторых событий. + +###### 13.05.2021 +Добавлена поддержка API уведомлений об изменении состояний устройств. + ###### 31.03.2021 Добавлена поддрежка разделения доступа пользователей к устройствам. ###### Release Проведён рефакторинг кода и, местами, внесены значительные правки. -Добавлена поддержка датчиков (устройств **devices.types.sensor**) +Добавлена поддержка датчиков (устройств `devices.types.sensor`) ## Требования - **"Белый" IP адрес и домен**. Если нет своего домена и белого IP адреса можно воспользоваться Dynamic DNS сервисами (например, noip.com). @@ -56,7 +62,7 @@ npm start ``` ## Настройка yandex2mqtt -Все основные настройки моста прописываются в файл config.js. Перед запуском обязательно отредактируйте его. +Все основные настройки моста прописываются в файл `config.js`. Перед запуском обязательно отредактируйте его. ``` mv config.orig.js config.js ``` @@ -64,6 +70,12 @@ mv config.orig.js config.js #### Файл конфигурации ``` module.exports = { + notification: [ + { + ... + }, + ... + ] mqtt: { ... }, @@ -76,18 +88,21 @@ module.exports = { { ... }, + ... ], users: [ { ... }, + ... ], devices: [ { ... }, + ... ] } ``` @@ -273,13 +288,35 @@ devices: [ *В случае отсутсвия id в конфиге, он будет назначен автоматически по индексу в массиве.* +#### Уведомление об изменении состояний устройств +Платформа УД Яндекс предоставляет сервис уведомлений об изменении состояний устройств. При изменении состояния устройства (например, изменение влажности) yandex2mqtt будет отправлять запрос с новым состоянием. + +В настройках предусмотрен блок `notification`. + +``` +notification: [ + { + skill_id: '6fca0a54-a505-4420-b774-f01da95e5c31', + oauth_token: 'AQA11AAPv-V2BAT7o_ps6gEtrtNNjlE2ENYt96w', + user_id: '2' + }, +] +``` + +Если к yandex2mqtt "подключено" несколько навыков УД, то в массиве необходимо указать настройки для каждого навыка УД, который должен получать уведомления. + +`skill_id` (идентификатор вызываемого навыка, присвоенный при создании) и `oauth_token` (авторизационный токен владельца навыка) можно узнать из документации на [Уведомление об изменении состояний устройств](https://yandex.ru/dev/dialogs/smart-home/doc/reference-alerts/post-skill_id-callback-state.html), а `user_id` - id пользователя в файле конфигурации yandex2mqtt. + +*Важно. Уведомления будут отправляться при изменнии mqtt топика хранящего состояние устройства. Соответственно, если для устройства не задан топик state, то уведомление для устройтва отправляться не будет.* + + #### Разрешенные пользователи для устройств (allowedUsers) В блоке конфигурации можно указать пользователей (id пользователей), для которых будет доступно устройство. -В опции allowedUsers указыватся массив (строковых значений) id. Если данная опция не указана, то для неё будет назначено значение ['1']; +В опции `allowedUsers` указыватся массив (строковых значений) id. Если данная опция не указана, то для неё будет назначено значение ['1']; #### Mapping значений -Блок valueMapping позволяет настроить конвертацию значений между yandex api и MQTT. Это может быть актуально для умений типа **devices.capabilities.on_off** и **devices.capabilities.toggle**. +Блок valueMapping позволяет настроить конвертацию значений между yandex api и MQTT. Это может быть актуально для умений типа `devices.capabilities.on_off` и `devices.capabilities.toggle`. *Например, если в УД состояние влючено/выключено соответствует значениям 1/0, то Вам понадобиться их конвертировать, т.к. в навыках Yandex значения true/false.* ``` @@ -297,15 +334,19 @@ valueMapping: [ - [Типы умений устройства](https://yandex.ru/dev/dialogs/alice/doc/smart-home/concepts/capability-types.html) - [Типы встроенных датчиков](https://yandex.ru/dev/dialogs/alice/doc/smart-home/concepts/properties-types.html) +## Логирование +Добавлено две "стратегии" логирования: сообщений об ошибках в файл `log/error.log` (аргумент запуска `--log-error`) и всех сообщений в консоль (`--log-info`). +Для запуска y2m с логирование необходимо добавить аргумент запуска в команду запуска в файле настройки служба (**раздел ниже**) или запустить из консоли. + ## Создание службы -В папке /etc/systemd/system/ создать файл yandex2mqtt.service со следующим содержанем: +В папке `/etc/systemd/system/` создать файл `yandex2mqtt.service` со следующим содержанем: ``` [Unit] Description=yandex2mqtt After=network.target [Service] -ExecStart=/usr/bin/npm start +ExecStart=/usr/bin/node app.js --log-error WorkingDirectory=/opt/yandex2mqtt StandardOutput=inherit StandardError=inherit diff --git a/app.js b/app.js index 91481f8..f231134 100644 --- a/app.js +++ b/app.js @@ -2,6 +2,8 @@ const fs = require('fs'); const path = require('path'); +/* */ +const {createLogger, format, transports} = require('winston'); /* express and https */ const ejs = require('ejs'); const express = require('express'); @@ -20,6 +22,29 @@ const mqtt = require('mqtt'); const config = require('./config'); const Device = require('./device'); +/* */ +const clArgv = process.argv.slice(2); + +/* Logging */ +global.logger = createLogger({ + level: 'info', + format: format.combine( + format.errors({stack: true}), + format.timestamp(), + format.printf(({level, message, timestamp, stack}) => { + return `${timestamp} ${level}: ${stack != undefined ? stack : message}`; + }), + ), + transports: [ + new transports.Console({ + silent: clArgv.indexOf('--log-info') == -1 + }) + ], +}); + +if (clArgv.indexOf('--log-error') > -1) global.logger.add(new transports.File({filename: 'log/error.log', level: 'error'})); + +/* */ app.engine('ejs', ejs.__express); app.set('view engine', 'ejs'); app.set('views', path.join(__dirname, './views')); @@ -111,6 +136,49 @@ global.mqttClient = mqtt.connect(`mqtt://${config.mqtt.host}`, { const ldevice = global.devices.find(d => d.data.id == deviceId); ldevice.updateState(`${message}`, instance); + /* Make Request to Yandex Dialog notification API */ + Promise.all(config.notification.map(el => { + let {skill_id, oauth_token, user_id} = el; + + return new Promise((resolve, reject) => { + let req = https.request({ + hostname: 'dialogs.yandex.net', + port: 443, + path: `/api/v1/skills/${skill_id}/callback/state`, + method: 'POST', + headers: { + 'Content-Type': `application/json`, + 'Authorization': `OAuth ${oauth_token}` + } + }, res => { + res.on('data', d => { + global.logger.log('info', {message: `${d}`}); + }); + }); + + req.on('error', error => { + global.logger.log('error', {message: `${error}`}); + }); + + let {id, capabilities, properties} = ldevice.getState(); + req.write(JSON.stringify({ + "ts": Math.floor(Date.now() / 1000), + "payload": { + "user_id": `${user_id}`, + "devices": [{ + id, + capabilities: capabilities.filter(c => c.state.instance == instance), + properties: properties.filter(p => p.state.instance == instance) + }], + } + })); + + req.end(); + + resolve(true); + }); + })); + /* */ }); diff --git a/db/access_tokens.js b/db/access_tokens.js index 0a43256..dacc2c4 100644 --- a/db/access_tokens.js +++ b/db/access_tokens.js @@ -1,5 +1,6 @@ 'use strict'; +const {logger, authl} = global; const loki = require('lokijs'); global.dbl = new loki('./loki.json', { @@ -7,46 +8,50 @@ global.dbl = new loki('./loki.json', { autosave: true, autosaveInterval: 5000, autoloadCallback() { - global.authl = global.dbl.getCollection('tokens'); - if (global.authl === null) { - global.authl = global.dbl.addCollection('tokens'); + authl = global.dbl.getCollection('tokens'); + if (authl === null) { + authl = global.dbl.addCollection('tokens'); } } }); module.exports.find = (key, done) => { - const ltoken = global.authl.findOne({'token': key}); + const ltoken = authl.findOne({'token': key}); if (ltoken){ - console.log('Token found'); const {userId, clientId} = ltoken; return done(null, {userId, clientId}) } else { - return done(new Error('Token Not Found')); + logger.log('error', new Error('Token Not Found')); + return done(); } }; module.exports.findByUserIdAndClientId = (userId, clientId, done) => { - const ltoken = global.authl.findOne({'userId': userId}); + const ltoken = authl.findOne({'userId': userId}); if (ltoken){ - console.log('Load token by userId: User found'); + logger.log('info', {message: `Load token by userId (${userId}): User found`}); const {token, userId: uid, clientId: cid} = ltoken; - if (uid === userId && cid === clientId) return done(null, token); - else return done(new Error('Token Not Found')); + if (uid === userId && cid === clientId) { + return done(null, token); + } else { + logger.log('error', new Error('Token Not Found')); + return done(); + } } else { - console.log('User not found'); - return done(new Error('User Not Found')); + logger.log('error', new Error('User Not Found')); + return done(); } }; module.exports.save = (token, userId, clientId, done) => { - console.log('Start saving token'); - const ltoken = global.authl.findOne({'userId': userId}); + logger.log('info', {message: `Start saving token`}); + const ltoken = authl.findOne({'userId': userId}); if (ltoken){ - console.log('User Updated'); - global.authl.update(Object.assign({}, ltoken, {token, userId, clientId})); + logger.log('info', {message: `User Updated`}); + authl.update(Object.assign({}, ltoken, {token, userId, clientId})); } else { - console.log('User not Found. Create new...'); - global.authl.insert({'type': 'token', token, userId, clientId}); + logger.log('info', {message: `User not Found. Create new...`}); + authl.insert({'type': 'token', token, userId, clientId}); } done(); }; diff --git a/db/authorization_codes.js b/db/authorization_codes.js index ae37649..c716cf2 100644 --- a/db/authorization_codes.js +++ b/db/authorization_codes.js @@ -1,10 +1,15 @@ 'use strict'; +const {logger} = global; const codes = {}; module.exports.find = (key, done) => { - if (codes[key]) return done(null, codes[key]); - return done(new Error('Code Not Found')); + if (codes[key]) { + return done(null, codes[key]); + } else { + logger.log('error', new Error('Code Not Found')); + return done(); + } }; module.exports.save = (code, clientId, redirectUri, userId, userName, done) => { diff --git a/db/clients.js b/db/clients.js index e49069f..4308a4b 100644 --- a/db/clients.js +++ b/db/clients.js @@ -1,17 +1,20 @@ 'use strict'; +const {logger} = global; const {clients} = require('../config'); module.exports.findById = (id, done) => { for (const client of clients) { if (client.id === id) return done(null, client); } - return done(new Error('Client Not Found')); + logger.log('error', new Error('Client Not Found')); + return done(); }; module.exports.findByClientId = (clientId, done) => { for (const client of clients) { if (client.clientId === clientId) return done(null, client); } - return done(new Error('Client Not Found')); + logger.log('error', new Error('Client Not Found')); + return done(); }; diff --git a/db/users.js b/db/users.js index 906954d..e2bf6a6 100644 --- a/db/users.js +++ b/db/users.js @@ -1,17 +1,20 @@ 'use strict'; +const {logger} = global; const {users} = require('../config'); module.exports.findById = (id, done) => { for (const user of users) { if (user.id === id) return done(null, user); } - return done(new Error('User Not Found')); + logger.log('error', new Error('User Not Found')); + return done(); }; module.exports.findByUsername = (username, done) => { for (const user of users) { if (user.username === username) return done(null, user); } - return done(new Error('User Not Found')); + logger.log('error', new Error('User Not Found')); + return done(); }; diff --git a/device.js b/device.js index 7745afe..b3e6fc2 100644 --- a/device.js +++ b/device.js @@ -1,3 +1,5 @@ +const {logger} = global; + /* function for convert system values to Yandex (depends of capability or property type) */ function convertToYandexValue(val, actType) { switch(actType) { @@ -8,7 +10,7 @@ function convertToYandexValue(val, actType) { const value = parseFloat(val); return isNaN(value) ? 0.0 : value; } catch(e) { - console.error(`Can't parse to float: ${val}`); + logger.log('error', {message: `Can't parse to float: ${val}`}); return 0.0; } } @@ -82,7 +84,7 @@ class Device { } } default: { - console.error(`Unsupported capability type: ${type}`) + logger.log('error', {message: `Unsupported capability type: ${type}`}); return undefined; } } @@ -181,7 +183,7 @@ class Device { message = `${value}`; } catch(e) { topic = false; - console.log(e); + logger.log('error', {message: `${e}`}); } if (topic) { @@ -211,7 +213,7 @@ class Device { const value = this.getMappedValue(val, actType, false); cp.state = {instance, value: convertToYandexValue(value, actType)}; } catch(e) { - console.error(e); + logger.log('error', {message: `${e}`}); } } } diff --git a/package.json b/package.json index a3ec69c..6150dfe 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,8 @@ "passport-http": "^0.3.0", "passport-http-bearer": "^1.0.1", "passport-local": "^1.0.0", - "passport-oauth2-client-password": "^0.1.2" + "passport-oauth2-client-password": "^0.1.2", + "winston": "^3.3.3" }, "devDependencies": { "eslint": "^7.21.0", diff --git a/routes/user.js b/routes/user.js index d3f230c..96224a1 100644 --- a/routes/user.js +++ b/routes/user.js @@ -39,7 +39,7 @@ module.exports.devices = [ res.status(200).send(r); } catch (e) { - console.error(e); + global.logger.log('error', {message: `${e}`}); res.status(404).send(undefined); } }