mirror of
https://github.com/lasthead0/yandex2mqtt.git
synced 2025-08-07 08:40:29 +03:00
Merge branch 'features' into beta
This commit is contained in:
55
README.md
55
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
|
||||
|
68
app.js
68
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);
|
||||
});
|
||||
}));
|
||||
|
||||
/* */
|
||||
});
|
||||
|
||||
|
@@ -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();
|
||||
};
|
||||
|
@@ -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) => {
|
||||
|
@@ -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();
|
||||
};
|
||||
|
@@ -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();
|
||||
};
|
||||
|
10
device.js
10
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}`});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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",
|
||||
|
@@ -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);
|
||||
}
|
||||
}
|
||||
|
Reference in New Issue
Block a user