node-red-contrib-yandex-station-management 0.3.7 → 0.3.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +87 -12
- package/examples/Dashboard-player-twocolors.json +45 -0
- package/nodes/yandex-login.js +2 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
- Яндекс Станция мини 2 с экраном(протестировано)
|
|
6
6
|
- Яндекс Станция лайт(протестировано)
|
|
7
7
|
- Яндекс Станци Макс(протестировано)
|
|
8
|
-
- Яндекс Модуль(
|
|
9
|
-
- Яндекс Модуль - 2 (
|
|
8
|
+
- Яндекс Модуль( протестировано)
|
|
9
|
+
- Яндекс Модуль - 2 (протестировано)
|
|
10
10
|
- JBL Link Music(не протестировано)
|
|
11
11
|
- JBL Link Portable(протестировано)
|
|
12
12
|
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
- находятся в одной локальной сети с сервером Node-Red
|
|
17
17
|
|
|
18
18
|
|
|
19
|
-
Для работы требуется токен от Яндекс.Музыки.
|
|
19
|
+
Для работы требуется токен от Яндекс.Музыки.
|
|
20
20
|
В модуле в экспериментальном режиме реализована возможность получения токена из логин-пароля(Спасибо слать [сюда](https://github.com/twocolors)). Если получение токена не отрабатывает, то стоит попробовать включить и отключить двух-факторную аутентификацию в настройках Яндекса. [Источник](https://github.com/AlexxIT/YandexStation/issues/103). Убедиться в безопасности использования учетных данных можно, посмотрев [код](./nodes/yandex-login.html)
|
|
21
21
|
|
|
22
22
|
Второй из варинатов его получения описан в [FAQ](#faq)
|
|
@@ -39,7 +39,7 @@
|
|
|
39
39
|
## Первоначальная настройка.
|
|
40
40
|
После установки для начала работы добавить любую ноду, ввести учетные данные(токен) в раздел Login, сохранить и нажать Deploy(обязательно!). Как получить токен - написано в FAQ.
|
|
41
41
|
|
|
42
|
-
После деплоя в настройках ноды в поле Station должны появиться станции доступные для управления.
|
|
42
|
+
После деплоя в настройках ноды в поле Station должны появиться станции доступные для управления.
|
|
43
43
|
|
|
44
44
|
Если станция не появилась в списке, то можно подождать пару минут или перезапустить Node-Red.
|
|
45
45
|
|
|
@@ -114,7 +114,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
114
114
|
Отправка команды, вместо того, чтобы говорить ее колонке голосом: "Включи свет", "Включи музыку", "Включи мой плейлист", "Отключись через 15 минут" и так далее.
|
|
115
115
|
|
|
116
116
|
#### TTS.
|
|
117
|
-
Воспроизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.
|
|
117
|
+
Воспроизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.
|
|
118
118
|
Параметры для TTS могут задаваться как в настройках, так и некоторые из них могут быть переопределены входящим сообщением.
|
|
119
119
|
- Text. Откуда и какой текст проговорить.
|
|
120
120
|
- из входящего сообщения (msg.payload по-умолчанию)
|
|
@@ -123,7 +123,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
123
123
|
- JSON: выбрать сообщение случайным образом из массива вида ["один", "два", "три"].
|
|
124
124
|
- Voice. Выбор голоса для генерации. Может быть переопределено через msg.voice
|
|
125
125
|
- Effect. Эффекты для генерации голоса. Может быть переопределено через msg.effect
|
|
126
|
-
-
|
|
126
|
+
-
|
|
127
127
|
Есть ряд опций:
|
|
128
128
|
- Volume. Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения уровень громкости вернется в изначальный. Может быть переопределено через msg.volume
|
|
129
129
|
- Whisper. Позволяет произнести фразу шептом.. Переопределяется через msg.whisper
|
|
@@ -236,7 +236,36 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
236
236
|
"volume" : 0.2
|
|
237
237
|
}
|
|
238
238
|
```
|
|
239
|
-
10.
|
|
239
|
+
10. Включить радио
|
|
240
|
+
```json
|
|
241
|
+
{
|
|
242
|
+
"command": "playRadio",
|
|
243
|
+
"id": "detskoe"
|
|
244
|
+
}
|
|
245
|
+
```
|
|
246
|
+
11. Режим повтора. "One"/"All"/"None"
|
|
247
|
+
```json
|
|
248
|
+
{
|
|
249
|
+
"command": "repeat",
|
|
250
|
+
"mode": "One"
|
|
251
|
+
}
|
|
252
|
+
```
|
|
253
|
+
12. Режим вразброс. Срабатывает, когда есть очередь треков(включен плейоист, альбом, артист) true/false
|
|
254
|
+
```json
|
|
255
|
+
{
|
|
256
|
+
"command": "shuffle",
|
|
257
|
+
"enable": true
|
|
258
|
+
}
|
|
259
|
+
```
|
|
260
|
+
13. Принудительно включить режим индикации Алисы - занято, слушаю, простой "LISTENING"/"BUSY"/"IDLE"
|
|
261
|
+
```json
|
|
262
|
+
{
|
|
263
|
+
"command": "showAliceVisualState",
|
|
264
|
+
"aliceStateName": "LISTENING",
|
|
265
|
+
"recognizedPhrase": ""
|
|
266
|
+
}
|
|
267
|
+
```
|
|
268
|
+
14. Отправить "Текст" для TTS.
|
|
240
269
|
Больше не работает!
|
|
241
270
|
```json
|
|
242
271
|
{
|
|
@@ -244,14 +273,14 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
244
273
|
"text" : "Повторяй за мной 'Текст'"
|
|
245
274
|
}
|
|
246
275
|
```
|
|
247
|
-
|
|
276
|
+
15. Отправить голосовую команду.
|
|
248
277
|
```json
|
|
249
278
|
{
|
|
250
279
|
"command" : "sendText",
|
|
251
280
|
"text" : "Включи музыку"
|
|
252
281
|
}
|
|
253
282
|
```
|
|
254
|
-
|
|
283
|
+
16. Прервать "слушание" после TTS и не только:
|
|
255
284
|
```json
|
|
256
285
|
{
|
|
257
286
|
"command": "serverAction",
|
|
@@ -262,7 +291,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
262
291
|
}
|
|
263
292
|
```
|
|
264
293
|
|
|
265
|
-
|
|
294
|
+
17. Отправить "Текст" для TTS со спецэффектами (**raw режим**):
|
|
266
295
|
```json
|
|
267
296
|
{
|
|
268
297
|
"command": "serverAction",
|
|
@@ -314,7 +343,53 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
314
343
|
```json
|
|
315
344
|
"value": "<speaker voice='kostya' audio='alice-sounds-game-win-1.opus' effect='megaphone'>добро пожаловать"
|
|
316
345
|
```
|
|
317
|
-
|
|
346
|
+
18. Приветствие как в автомобиле. Кратко скажет погоду и пробки
|
|
347
|
+
```json
|
|
348
|
+
{
|
|
349
|
+
"command": "serverAction",
|
|
350
|
+
"serverActionEventPayload": {
|
|
351
|
+
"type": "server_action",
|
|
352
|
+
"name": "update_form",
|
|
353
|
+
"payload": {
|
|
354
|
+
"form_update": {
|
|
355
|
+
"name": "personal_assistant.automotive.greeting"
|
|
356
|
+
},
|
|
357
|
+
"resubmit": true
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
}
|
|
361
|
+
```
|
|
362
|
+
19. Включить и выключить блютуз
|
|
363
|
+
```json
|
|
364
|
+
{
|
|
365
|
+
"command": "serverAction",
|
|
366
|
+
"serverActionEventPayload": {
|
|
367
|
+
"type": "server_action",
|
|
368
|
+
"name": "update_form",
|
|
369
|
+
"payload": {
|
|
370
|
+
"form_update": {
|
|
371
|
+
"name": "personal_assistant.scenarios.bluetooth_on"
|
|
372
|
+
},
|
|
373
|
+
"resubmit": true
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
```
|
|
378
|
+
```json
|
|
379
|
+
{
|
|
380
|
+
"command": "serverAction",
|
|
381
|
+
"serverActionEventPayload": {
|
|
382
|
+
"type": "server_action",
|
|
383
|
+
"name": "update_form",
|
|
384
|
+
"payload": {
|
|
385
|
+
"form_update": {
|
|
386
|
+
"name": "personal_assistant.scenarios.bluetooth_off"
|
|
387
|
+
},
|
|
388
|
+
"resubmit": true
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
```
|
|
318
393
|
|
|
319
394
|
#### Stop listening.
|
|
320
395
|
|
|
@@ -370,7 +445,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
|
|
|
370
445
|

|
|
371
446
|

|
|
372
447
|
|
|
373
|
-
Есть еще один вариант от
|
|
448
|
+
Есть еще один вариант от [@twocolors](https://github.com/twocolors), в примерах.
|
|
374
449
|
|
|
375
450
|
Добавляется простым flow и выглядит отлично)
|
|
376
451
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
[
|
|
2
|
+
{
|
|
3
|
+
"id": "db7d25d3.9110a8",
|
|
4
|
+
"type": "ui_template",
|
|
5
|
+
"z": "0c069b6a05278757",
|
|
6
|
+
"group": "8865f91.bf77188",
|
|
7
|
+
"name": "",
|
|
8
|
+
"order": 1,
|
|
9
|
+
"width": "8",
|
|
10
|
+
"height": "8",
|
|
11
|
+
"format": "<style>\n .yandex-player {\n height: 100%;\n position: relative;\n min-height: 128px;\n z-index: 1;\n background: url(\"//avatars.mds.yandex.net/get-music-misc/29541/img.5e6a1c5b38be6e3bae26558a/600x600\");\n background-repeat: no-repeat;\n background-position: center;\n background-size: cover;\n }\n\n .yandex-player .gradient {\n background: linear-gradient(rgba(0, 0, 0, 0), rgba(0, 0, 0, 0.75));\n position: absolute;\n bottom: 0;\n left: 0;\n }\n\n .yandex-player .controls {\n margin: 24px 32px;\n color: rgba(255, 255, 255, 0.75);\n font-family: Roboto, Noto, sans-serif;\n -moz-osx-font-smoothing: grayscale;\n -webkit-font-smoothing: antialiased;\n font-weight: lighter;\n }\n\n .yandex-player .title {\n font-size: larger;\n }\n\n .yandex-player .subtitle {\n font-size: medium;\n }\n\n .yandex-player .ui {\n padding: 16px 0 24px 0;\n }\n\n .yandex-player .ui i {\n cursor: pointer;\n }\n\n .yandex-player .ui i:hover {\n color: rgba(255, 255, 255, 1);\n }\n\n .yandex-player .ui-btn {\n float: left;\n display: flex;\n justify-content: space-between;\n width: 20%;\n }\n\n .yandex-player .ui-volume {\n float: right;\n display: flex;\n justify-content: space-between;\n width: 80%;\n vertical-align: middle;\n }\n\n .yandex-player .slider {\n position: relative;\n cursor: pointer;\n width: 100%;\n margin: 0 16px;\n opacity: 1;\n display: inline-block;\n }\n\n .yandex-player .slider_base {\n width: 100%;\n height: 2px;\n background-color: rgba(255, 255, 255, 0.75);\n border-radius: 2px;\n position: absolute;\n top: 8px;\n }\n\n .yandex-player .slider_progress {\n height: 4px;\n background-color: rgba(255, 255, 255, 1);\n border-radius: 2px;\n position: absolute;\n top: 7px;\n }\n\n .yandex-player .bar {\n position: absolute;\n cursor: pointer;\n width: 100%;\n opacity: 1;\n bottom: 0;\n height: 6px;\n background-color: rgba(255, 255, 255, 0.75);\n }\n\n .yandex-player .bar_progress {\n width: 0;\n height: 100%;\n background-color: #fc0;\n }\n\n .yandex-player .red {\n color: red;\n }\n</style>\n<div class=\"yandex-player\">\n <div class=\"gradient\" style=\"min-height: 128px; width: 100%;\">\n <div class=\"controls\">\n <div class=\"title\"></div>\n <div class=\"subtitle\"></div>\n <div class=\"ui\">\n <div class=\"ui-btn\">\n <i class=\"fa fa-step-backward fa-lg\" aria-hidden=\"true\"></i>\n <i class=\"btn-play-pause fa fa-play fa-lg\" aria-hidden=\"true\"></i>\n <i class=\"fa fa-step-forward fa-lg\" aria-hidden=\"true\"></i>\n </div>\n\n <div class=\"ui-volume\">\n <div class=\"slider\">\n <div class=\"slider_base\"></div>\n <div class=\"slider_progress\"></div>\n </div>\n <div style=\"width: 10%;\">\n <i class=\"btn-volume fa fa-volume-up fa-lg\" aria-hidden=\"true\"></i>\n </div>\n <div style=\"width: 10%; text-align: right; margin-left: 16px;\">\n <i class=\"fa fa-heart fa-lg\" aria-hidden=\"true\"></i>\n </div>\n </div>\n </div>\n </div>\n <div class=\"bar\">\n <div class=\"bar_progress\"></div>\n </div>\n </div>\n</div>\n<script>\n /*! js-cookie v3.0.0-rc.1 | MIT */\n !function (e, t) { \"object\" == typeof exports && \"undefined\" != typeof module ? module.exports = t() : \"function\" == typeof define && define.amd ? define(t) : (e = e || self, function () { var n = e.Cookies, r = e.Cookies = t(); r.noConflict = function () { return e.Cookies = n, r } }()) }(this, function () { \"use strict\"; function e(e) { for (var t = 1; t < arguments.length; t++) { var n = arguments[t]; for (var r in n) e[r] = n[r] } return e } var t = { read: function (e) { return e.replace(/(%[\\dA-F]{2})+/gi, decodeURIComponent) }, write: function (e) { return encodeURIComponent(e).replace(/%(2[346BF]|3[AC-F]|40|5[BDE]|60|7[BCD])/g, decodeURIComponent) } }; return function n(r, o) { function i(t, n, i) { if (\"undefined\" != typeof document) { \"number\" == typeof (i = e({}, o, i)).expires && (i.expires = new Date(Date.now() + 864e5 * i.expires)), i.expires && (i.expires = i.expires.toUTCString()), t = encodeURIComponent(t).replace(/%(2[346B]|5E|60|7C)/g, decodeURIComponent).replace(/[()]/g, escape), n = r.write(n, t); var c = \"\"; for (var u in i) i[u] && (c += \"; \" + u, !0 !== i[u] && (c += \"=\" + i[u].split(\";\")[0])); return document.cookie = t + \"=\" + n + c } } return Object.create({ set: i, get: function (e) { if (\"undefined\" != typeof document && (!arguments.length || e)) { for (var n = document.cookie ? document.cookie.split(\"; \") : [], o = {}, i = 0; i < n.length; i++) { var c = n[i].split(\"=\"), u = c.slice(1).join(\"=\"); '\"' === u[0] && (u = u.slice(1, -1)); try { var f = t.read(c[0]); if (o[f] = r.read(u, f), e === f) break } catch (e) { } } return e ? o[e] : o } }, remove: function (t, n) { i(t, \"\", e({}, n, { expires: -1 })) }, withAttributes: function (t) { return n(this.converter, e({}, this.attributes, t)) }, withConverter: function (t) { return n(e({}, this.converter, t), this.attributes) } }, { attributes: { value: Object.freeze(o) }, converter: { value: Object.freeze(r) } }) }(t, { path: \"/\" }) });\n</script>\n<script>\n (function (scope) {\n // function\n function setVolume(volume) {\n //console.log('setVolume:' + volume);\n\n if (volume <= 4.5) {\n volume = 0;\n } else if (volume > 4.5 && volume < 10) {\n volume = 10;\n } else if (volume >= 95.5) {\n volume = 100;\n }\n\n let toggle = volume > 0 ? true : false;\n $('.btn-volume').toggleClass('fa-volume-up', toggle).toggleClass('fa-volume-off', !toggle);\n\n scope.send({ 'payload': { 'command': 'setVolume', 'volume': volume / 100 } });\n }\n\n function setProgress(playerState) {\n let progress = (playerState.progress / playerState.duration) * 100;\n //$('.bar_progress').animate({ width: progress + '%' }, 'fast');\n $('.bar_progress').width(progress + '%');\n }\n\n function setLike(id) {\n let like = JSON.parse(Cookies.get('yandex-like') || '{}');\n like[id] = id;\n Cookies.set('yandex-like', JSON.stringify(like), { expires: 3 });\n $('.fa-heart').addClass('red');\n\n scope.send({ payload: { 'command': 'sendText', 'text': 'поставь лайк' } });\n }\n\n function setDislike(id) {\n let like = JSON.parse(Cookies.get('yandex-like') || '{}');\n if (like[id] !== undefined) {\n delete like[id];\n }\n Cookies.set('yandex-like', JSON.stringify(like), { expires: 3 });\n $('.fa-heart').removeClass('red');\n\n scope.send({ payload: { 'command': 'sendText', 'text': 'поставь дизлайк' } });\n }\n\n // init+update\n scope.$watch('msg', function (msg) {\n if (msg) {\n\n if ('volume' in msg.payload) {\n $('.slider_progress').animate({width: msg.payload.volume * 100 + '%'}, 150);\n }\n\n if (msg.payload.playing === true || msg.payload.playing === false) {\n $('.btn-play-pause').toggleClass('fa-pause', msg.payload.playing).toggleClass('fa-play', !msg.payload.playing);\n }\n\n if ('playerState' in msg.payload) {\n //update title\n $('.title').text(msg.payload.playerState.title);\n $('.subtitle').text(msg.payload.playerState.subtitle);\n\n //update img\n let thumb = 'avatars.mds.yandex.net/get-music-misc/29541/img.5e6a1c5b38be6e3bae26558a/%%';\n if (msg.payload.playerState.extra !== null && typeof msg.payload.playerState.extra.coverURI !== \"undefined\") {\n thumb = msg.payload.playerState.extra.coverURI;\n }\n thumb = '//' + thumb.replace(/%%/g, \"600x600\");\n\n if (thumb != $('.yandex-player').attr('data-image')) {\n $('.yandex-player').css('background-image', 'url(' + thumb + ')').attr('data-image', thumb);\n }\n\n setProgress(msg.payload.playerState);\n\n let id = msg.payload.playerState.id;\n if (id != $('.fa-heart').attr('data-id')) {\n $('.fa-heart').removeClass('red').attr('data-id', id);\n let like = JSON.parse(Cookies.get('yandex-like') || '{}');\n if (like[id]) {\n $('.fa-heart').addClass('red');\n }\n }\n }\n }\n });\n\n // controls\n $('.btn-play-pause').click(function () {\n scope.send({ 'payload': { 'command': scope.msg.payload.playing ? 'stop' : 'play' } });\n });\n\n $('.fa-step-backward').click(function () {\n scope.send({ 'payload': { 'command': 'prev' } });\n });\n\n $('.fa-step-forward').click(function () {\n scope.send({ 'payload': { 'command': 'next' } });\n });\n\n // slider volume\n $('.slider').on('click', function (event) {\n let left = $('.slider').offset().left;\n let width = $('.slider').outerWidth();\n\n let volume = (event.clientX - left) / width * 100;\n\n setVolume(volume);\n });\n\n // volume\n $('.btn-volume').on('click', function () {\n if ($(this).hasClass('fa-volume-up')) {\n Cookies.set('yandex-volume-save', $('.slider_progress').width(), { expires: 7 });\n setVolume(0);\n } else if ($(this).hasClass('fa-volume-off')) {\n setVolume(Cookies.get('yandex-volume-save'));\n }\n });\n\n // bar\n $('.bar').on('click', function (event) {\n let left = $('.bar').offset().left;\n let width = $('.bar').outerWidth();\n\n let percent = (event.clientX - left) / width * 100;\n let position = Math.round(percent * (scope.msg.payload.playerState.duration / 100));\n\n scope.send({ 'payload': { 'command': 'rewind', 'position': position } });\n if ($('.btn-play-pause').hasClass('fa-play')) {\n scope.send({ 'payload': { 'command': 'play' } });\n }\n });\n\n //like\n $('.fa-heart').on('click', function () {\n let id = $(this).attr('data-id');\n if (!$(this).hasClass('red')) {\n setLike(id);\n } else {\n setDislike(id);\n }\n });\n\n })(scope);\n</script>",
|
|
12
|
+
"storeOutMessages": true,
|
|
13
|
+
"fwdInMessages": false,
|
|
14
|
+
"resendOnRefresh": true,
|
|
15
|
+
"templateScope": "local",
|
|
16
|
+
"className": "",
|
|
17
|
+
"x": 400,
|
|
18
|
+
"y": 260,
|
|
19
|
+
"wires": [
|
|
20
|
+
[
|
|
21
|
+
"ae2a51d2fe666ff9"
|
|
22
|
+
]
|
|
23
|
+
]
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
"id": "8865f91.bf77188",
|
|
27
|
+
"type": "ui_group",
|
|
28
|
+
"name": "Alice",
|
|
29
|
+
"tab": "bd17abae.b2461",
|
|
30
|
+
"order": 1,
|
|
31
|
+
"disp": true,
|
|
32
|
+
"width": "8",
|
|
33
|
+
"collapse": false,
|
|
34
|
+
"className": ""
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
"id": "bd17abae.b2461",
|
|
38
|
+
"type": "ui_tab",
|
|
39
|
+
"name": "Кубик³",
|
|
40
|
+
"icon": "dashboard",
|
|
41
|
+
"order": 1,
|
|
42
|
+
"disabled": false,
|
|
43
|
+
"hidden": false
|
|
44
|
+
}
|
|
45
|
+
]
|
package/nodes/yandex-login.js
CHANGED
|
@@ -94,7 +94,8 @@ module.exports = function(RED) {
|
|
|
94
94
|
if (bufferStation) {
|
|
95
95
|
let result = registerDevice(bufferStation.id, bufferStation.manager, bufferStation.parameters)
|
|
96
96
|
if (result != 2 && result != undefined) {
|
|
97
|
-
|
|
97
|
+
//https://github.com/n0name45/node-red-contrib-yandex-station-management/issues/20#issuecomment-1373709408
|
|
98
|
+
//registrationBuffer.splice(registrationBuffer.indexOf(bufferStation,1));
|
|
98
99
|
}
|
|
99
100
|
|
|
100
101
|
}
|