Merge branch 'features' into beta

This commit is contained in:
Evgenii Abramov
2021-05-17 00:41:47 +03:00
9 changed files with 165 additions and 37 deletions

View File

@@ -4,20 +4,26 @@
Форк [Проекта](https://github.com/munrexio/yandex2mqtt) и [Статья на Хабре](https://habr.com/ru/post/465537/) к оригиналу. Форк [Проекта](https://github.com/munrexio/yandex2mqtt) и [Статья на Хабре](https://habr.com/ru/post/465537/) к оригиналу.
## Важно ## Важно
Те, кто пользуется оригинальным проектом (или его форками), обратите внимание на то, что немного изменились настройки устройств (блок **devices** в файле конфигурации). Те, кто пользуется оригинальным проектом (или его форками), обратите внимание на то, что немного изменились настройки устройств (блок `devices` в файле конфигурации).
На данный момент проверено получение температуры и влажности с датчиков (датчики дверей и движения пока в бета-тесте), и включение/выключение света (вкл./выкл. других устройств по аналогии тоже должно работать). На данный момент проверено получение температуры и влажности с датчиков (датчики дверей и движения пока в бета-тесте), и включение/выключение света (вкл./выкл. других устройств по аналогии тоже должно работать).
Прочий функционал (изменение громкости, каналов, отключение звука), поидее, так же должны работать. Прочий функционал (изменение громкости, каналов, отключение звука), поидее, так же должны работать.
## ChangeLog ## ChangeLog
###### 16.05.2021
Добавлено логирование некоторых событий.
###### 13.05.2021
Добавлена поддержка API уведомлений об изменении состояний устройств.
###### 31.03.2021 ###### 31.03.2021
Добавлена поддрежка разделения доступа пользователей к устройствам. Добавлена поддрежка разделения доступа пользователей к устройствам.
###### Release ###### Release
Проведён рефакторинг кода и, местами, внесены значительные правки. Проведён рефакторинг кода и, местами, внесены значительные правки.
Добавлена поддержка датчиков (устройств **devices.types.sensor**) Добавлена поддержка датчиков (устройств `devices.types.sensor`)
## Требования ## Требования
- **"Белый" IP адрес и домен**. Если нет своего домена и белого IP адреса можно воспользоваться Dynamic DNS сервисами (например, noip.com). - **"Белый" IP адрес и домен**. Если нет своего домена и белого IP адреса можно воспользоваться Dynamic DNS сервисами (например, noip.com).
@@ -56,7 +62,7 @@ npm start
``` ```
## Настройка yandex2mqtt ## Настройка yandex2mqtt
Все основные настройки моста прописываются в файл config.js. Перед запуском обязательно отредактируйте его. Все основные настройки моста прописываются в файл `config.js`. Перед запуском обязательно отредактируйте его.
``` ```
mv config.orig.js config.js mv config.orig.js config.js
``` ```
@@ -64,6 +70,12 @@ mv config.orig.js config.js
#### Файл конфигурации #### Файл конфигурации
``` ```
module.exports = { module.exports = {
notification: [
{
...
},
...
]
mqtt: { mqtt: {
... ...
}, },
@@ -76,18 +88,21 @@ module.exports = {
{ {
... ...
}, },
...
], ],
users: [ users: [
{ {
... ...
}, },
...
], ],
devices: [ devices: [
{ {
... ...
}, },
...
] ]
} }
``` ```
@@ -273,13 +288,35 @@ devices: [
*В случае отсутсвия id в конфиге, он будет назначен автоматически по индексу в массиве.* *В случае отсутсвия 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) #### Разрешенные пользователи для устройств (allowedUsers)
В блоке конфигурации можно указать пользователей (id пользователей), для которых будет доступно устройство. В блоке конфигурации можно указать пользователей (id пользователей), для которых будет доступно устройство.
В опции allowedUsers указыватся массив (строковых значений) id. Если данная опция не указана, то для неё будет назначено значение ['1']; В опции `allowedUsers` указыватся массив (строковых значений) id. Если данная опция не указана, то для неё будет назначено значение ['1'];
#### Mapping значений #### 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.* *Например, если в УД состояние влючено/выключено соответствует значениям 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/capability-types.html)
- [Типы встроенных датчиков](https://yandex.ru/dev/dialogs/alice/doc/smart-home/concepts/properties-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] [Unit]
Description=yandex2mqtt Description=yandex2mqtt
After=network.target After=network.target
[Service] [Service]
ExecStart=/usr/bin/npm start ExecStart=/usr/bin/node app.js --log-error
WorkingDirectory=/opt/yandex2mqtt WorkingDirectory=/opt/yandex2mqtt
StandardOutput=inherit StandardOutput=inherit
StandardError=inherit StandardError=inherit

68
app.js
View File

@@ -2,6 +2,8 @@
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
/* */
const {createLogger, format, transports} = require('winston');
/* express and https */ /* express and https */
const ejs = require('ejs'); const ejs = require('ejs');
const express = require('express'); const express = require('express');
@@ -20,6 +22,29 @@ const mqtt = require('mqtt');
const config = require('./config'); const config = require('./config');
const Device = require('./device'); 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.engine('ejs', ejs.__express);
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, './views')); 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); const ldevice = global.devices.find(d => d.data.id == deviceId);
ldevice.updateState(`${message}`, instance); 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);
});
}));
/* */ /* */
}); });

View File

@@ -1,5 +1,6 @@
'use strict'; 'use strict';
const {logger, authl} = global;
const loki = require('lokijs'); const loki = require('lokijs');
global.dbl = new loki('./loki.json', { global.dbl = new loki('./loki.json', {
@@ -7,46 +8,50 @@ global.dbl = new loki('./loki.json', {
autosave: true, autosave: true,
autosaveInterval: 5000, autosaveInterval: 5000,
autoloadCallback() { autoloadCallback() {
global.authl = global.dbl.getCollection('tokens'); authl = global.dbl.getCollection('tokens');
if (global.authl === null) { if (authl === null) {
global.authl = global.dbl.addCollection('tokens'); authl = global.dbl.addCollection('tokens');
} }
} }
}); });
module.exports.find = (key, done) => { module.exports.find = (key, done) => {
const ltoken = global.authl.findOne({'token': key}); const ltoken = authl.findOne({'token': key});
if (ltoken){ if (ltoken){
console.log('Token found');
const {userId, clientId} = ltoken; const {userId, clientId} = ltoken;
return done(null, {userId, clientId}) return done(null, {userId, clientId})
} else { } else {
return done(new Error('Token Not Found')); logger.log('error', new Error('Token Not Found'));
return done();
} }
}; };
module.exports.findByUserIdAndClientId = (userId, clientId, done) => { module.exports.findByUserIdAndClientId = (userId, clientId, done) => {
const ltoken = global.authl.findOne({'userId': userId}); const ltoken = authl.findOne({'userId': userId});
if (ltoken){ 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; const {token, userId: uid, clientId: cid} = ltoken;
if (uid === userId && cid === clientId) return done(null, token); if (uid === userId && cid === clientId) {
else return done(new Error('Token Not Found')); return done(null, token);
} else { } else {
console.log('User not found'); logger.log('error', new Error('Token Not Found'));
return done(new Error('User Not Found')); return done();
}
} else {
logger.log('error', new Error('User Not Found'));
return done();
} }
}; };
module.exports.save = (token, userId, clientId, done) => { module.exports.save = (token, userId, clientId, done) => {
console.log('Start saving token'); logger.log('info', {message: `Start saving token`});
const ltoken = global.authl.findOne({'userId': userId}); const ltoken = authl.findOne({'userId': userId});
if (ltoken){ if (ltoken){
console.log('User Updated'); logger.log('info', {message: `User Updated`});
global.authl.update(Object.assign({}, ltoken, {token, userId, clientId})); authl.update(Object.assign({}, ltoken, {token, userId, clientId}));
} else { } else {
console.log('User not Found. Create new...'); logger.log('info', {message: `User not Found. Create new...`});
global.authl.insert({'type': 'token', token, userId, clientId}); authl.insert({'type': 'token', token, userId, clientId});
} }
done(); done();
}; };

View File

@@ -1,10 +1,15 @@
'use strict'; 'use strict';
const {logger} = global;
const codes = {}; const codes = {};
module.exports.find = (key, done) => { module.exports.find = (key, done) => {
if (codes[key]) return done(null, codes[key]); if (codes[key]) {
return done(new Error('Code Not Found')); 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) => { module.exports.save = (code, clientId, redirectUri, userId, userName, done) => {

View File

@@ -1,17 +1,20 @@
'use strict'; 'use strict';
const {logger} = global;
const {clients} = require('../config'); const {clients} = require('../config');
module.exports.findById = (id, done) => { module.exports.findById = (id, done) => {
for (const client of clients) { for (const client of clients) {
if (client.id === id) return done(null, client); 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) => { module.exports.findByClientId = (clientId, done) => {
for (const client of clients) { for (const client of clients) {
if (client.clientId === clientId) return done(null, client); 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();
}; };

View File

@@ -1,17 +1,20 @@
'use strict'; 'use strict';
const {logger} = global;
const {users} = require('../config'); const {users} = require('../config');
module.exports.findById = (id, done) => { module.exports.findById = (id, done) => {
for (const user of users) { for (const user of users) {
if (user.id === id) return done(null, user); 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) => { module.exports.findByUsername = (username, done) => {
for (const user of users) { for (const user of users) {
if (user.username === username) return done(null, user); 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();
}; };

View File

@@ -1,3 +1,5 @@
const {logger} = global;
/* function for convert system values to Yandex (depends of capability or property type) */ /* function for convert system values to Yandex (depends of capability or property type) */
function convertToYandexValue(val, actType) { function convertToYandexValue(val, actType) {
switch(actType) { switch(actType) {
@@ -8,7 +10,7 @@ function convertToYandexValue(val, actType) {
const value = parseFloat(val); const value = parseFloat(val);
return isNaN(value) ? 0.0 : value; return isNaN(value) ? 0.0 : value;
} catch(e) { } catch(e) {
console.error(`Can't parse to float: ${val}`); logger.log('error', {message: `Can't parse to float: ${val}`});
return 0.0; return 0.0;
} }
} }
@@ -82,7 +84,7 @@ class Device {
} }
} }
default: { default: {
console.error(`Unsupported capability type: ${type}`) logger.log('error', {message: `Unsupported capability type: ${type}`});
return undefined; return undefined;
} }
} }
@@ -181,7 +183,7 @@ class Device {
message = `${value}`; message = `${value}`;
} catch(e) { } catch(e) {
topic = false; topic = false;
console.log(e); logger.log('error', {message: `${e}`});
} }
if (topic) { if (topic) {
@@ -211,7 +213,7 @@ class Device {
const value = this.getMappedValue(val, actType, false); const value = this.getMappedValue(val, actType, false);
cp.state = {instance, value: convertToYandexValue(value, actType)}; cp.state = {instance, value: convertToYandexValue(value, actType)};
} catch(e) { } catch(e) {
console.error(e); logger.log('error', {message: `${e}`});
} }
} }
} }

View File

@@ -30,7 +30,8 @@
"passport-http": "^0.3.0", "passport-http": "^0.3.0",
"passport-http-bearer": "^1.0.1", "passport-http-bearer": "^1.0.1",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"passport-oauth2-client-password": "^0.1.2" "passport-oauth2-client-password": "^0.1.2",
"winston": "^3.3.3"
}, },
"devDependencies": { "devDependencies": {
"eslint": "^7.21.0", "eslint": "^7.21.0",

View File

@@ -39,7 +39,7 @@ module.exports.devices = [
res.status(200).send(r); res.status(200).send(r);
} catch (e) { } catch (e) {
console.error(e); global.logger.log('error', {message: `${e}`});
res.status(404).send(undefined); res.status(404).send(undefined);
} }
} }