node-red-contrib-yandex-station-management 0.3.7 → 0.3.9

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 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,11 +16,13 @@
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)
23
23
 
24
+ Третий из вариантов получения токена описан [тут](https://github.com/MarshalX/yandex-music-api/discussions/513#discussioncomment-2729781)
25
+
24
26
  Возможна работа с несколькими устройствами(протестировано) и несколькими учетными записями(протестировано).
25
27
 
26
28
  Состоит из 4 нод, позволяющих гибко настраивать автоматизации и использовать голосовые уведомления:
@@ -39,7 +41,7 @@
39
41
  ## Первоначальная настройка.
40
42
  После установки для начала работы добавить любую ноду, ввести учетные данные(токен) в раздел Login, сохранить и нажать Deploy(обязательно!). Как получить токен - написано в FAQ.
41
43
 
42
- После деплоя в настройках ноды в поле Station должны появиться станции доступные для управления.
44
+ После деплоя в настройках ноды в поле Station должны появиться станции доступные для управления.
43
45
 
44
46
  Если станция не появилась в списке, то можно подождать пару минут или перезапустить Node-Red.
45
47
 
@@ -114,7 +116,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
114
116
  Отправка команды, вместо того, чтобы говорить ее колонке голосом: "Включи свет", "Включи музыку", "Включи мой плейлист", "Отключись через 15 минут" и так далее.
115
117
 
116
118
  #### TTS.
117
- Воспроизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.
119
+ Воспроизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.
118
120
  Параметры для TTS могут задаваться как в настройках, так и некоторые из них могут быть переопределены входящим сообщением.
119
121
  - Text. Откуда и какой текст проговорить.
120
122
  - из входящего сообщения (msg.payload по-умолчанию)
@@ -123,7 +125,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
123
125
  - JSON: выбрать сообщение случайным образом из массива вида ["один", "два", "три"].
124
126
  - Voice. Выбор голоса для генерации. Может быть переопределено через msg.voice
125
127
  - Effect. Эффекты для генерации голоса. Может быть переопределено через msg.effect
126
- -
128
+ -
127
129
  Есть ряд опций:
128
130
  - Volume. Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения уровень громкости вернется в изначальный. Может быть переопределено через msg.volume
129
131
  - Whisper. Позволяет произнести фразу шептом.. Переопределяется через msg.whisper
@@ -236,7 +238,36 @@ Phrase to say - фраза, которую скажет Алиса вместо
236
238
  "volume" : 0.2
237
239
  }
238
240
  ```
239
- 10. Отправить "Текст" для TTS.
241
+ 10. Включить радио
242
+ ```json
243
+ {
244
+ "command": "playRadio",
245
+ "id": "detskoe"
246
+ }
247
+ ```
248
+ 11. Режим повтора. "One"/"All"/"None"
249
+ ```json
250
+ {
251
+ "command": "repeat",
252
+ "mode": "One"
253
+ }
254
+ ```
255
+ 12. Режим вразброс. Срабатывает, когда есть очередь треков(включен плейоист, альбом, артист) true/false
256
+ ```json
257
+ {
258
+ "command": "shuffle",
259
+ "enable": true
260
+ }
261
+ ```
262
+ 13. Принудительно включить режим индикации Алисы - занято, слушаю, простой "LISTENING"/"BUSY"/"IDLE"
263
+ ```json
264
+ {
265
+ "command": "showAliceVisualState",
266
+ "aliceStateName": "LISTENING",
267
+ "recognizedPhrase": ""
268
+ }
269
+ ```
270
+ 14. Отправить "Текст" для TTS.
240
271
  Больше не работает!
241
272
  ```json
242
273
  {
@@ -244,14 +275,14 @@ Phrase to say - фраза, которую скажет Алиса вместо
244
275
  "text" : "Повторяй за мной 'Текст'"
245
276
  }
246
277
  ```
247
- 11. Отправить голосовую команду.
278
+ 15. Отправить голосовую команду.
248
279
  ```json
249
280
  {
250
281
  "command" : "sendText",
251
282
  "text" : "Включи музыку"
252
283
  }
253
284
  ```
254
- 12. Прервать "слушание" после TTS и не только:
285
+ 16. Прервать "слушание" после TTS и не только:
255
286
  ```json
256
287
  {
257
288
  "command": "serverAction",
@@ -262,7 +293,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
262
293
  }
263
294
  ```
264
295
 
265
- 13. Отправить "Текст" для TTS со спецэффектами (**raw режим**):
296
+ 17. Отправить "Текст" для TTS со спецэффектами (**raw режим**):
266
297
  ```json
267
298
  {
268
299
  "command": "serverAction",
@@ -314,7 +345,53 @@ Phrase to say - фраза, которую скажет Алиса вместо
314
345
  ```json
315
346
  "value": "<speaker voice='kostya' audio='alice-sounds-game-win-1.opus' effect='megaphone'>добро пожаловать"
316
347
  ```
317
-
348
+ 18. Приветствие как в автомобиле. Кратко скажет погоду и пробки
349
+ ```json
350
+ {
351
+ "command": "serverAction",
352
+ "serverActionEventPayload": {
353
+ "type": "server_action",
354
+ "name": "update_form",
355
+ "payload": {
356
+ "form_update": {
357
+ "name": "personal_assistant.automotive.greeting"
358
+ },
359
+ "resubmit": true
360
+ }
361
+ }
362
+ }
363
+ ```
364
+ 19. Включить и выключить блютуз
365
+ ```json
366
+ {
367
+ "command": "serverAction",
368
+ "serverActionEventPayload": {
369
+ "type": "server_action",
370
+ "name": "update_form",
371
+ "payload": {
372
+ "form_update": {
373
+ "name": "personal_assistant.scenarios.bluetooth_on"
374
+ },
375
+ "resubmit": true
376
+ }
377
+ }
378
+ }
379
+ ```
380
+ ```json
381
+ {
382
+ "command": "serverAction",
383
+ "serverActionEventPayload": {
384
+ "type": "server_action",
385
+ "name": "update_form",
386
+ "payload": {
387
+ "form_update": {
388
+ "name": "personal_assistant.scenarios.bluetooth_off"
389
+ },
390
+ "resubmit": true
391
+ }
392
+ }
393
+ }
394
+ ```
318
395
 
319
396
  #### Stop listening.
320
397
 
@@ -370,7 +447,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
370
447
  ![alt text](/readme_images/dashboardPlayer.png "player")
371
448
  ![alt text](/readme_images/dashboardPlayerFlow.png "player")
372
449
 
373
- Есть еще один вариант от сообщества, который надо самостоятельно импортировать [со страницы автора](https://github.com/twocolors/node-red-dashboard-template/blob/main/alice_v2.json)
450
+ Есть еще один вариант от [@twocolors](https://github.com/twocolors), в примерах.
374
451
 
375
452
  Добавляется простым flow и выглядит отлично)
376
453
 
@@ -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
+ ]
@@ -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
- registrationBuffer.splice(registrationBuffer.indexOf(bufferStation,1));
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
  }
@@ -294,7 +295,7 @@ module.exports = function(RED) {
294
295
  device.watchDog = setTimeout(() => {
295
296
  if (typeof(device) != 'undefined' && typeof(device.ws) != 'undefined') {device.ws.close()}
296
297
  }, 10000);
297
- device.pingInterval = setInterval(onPing,300,device);
298
+ device.pingInterval = setInterval(onPing,1500,device);
298
299
  debugMessage(`${device.id}: Kill connection watchdog`);
299
300
  clearTimeout(device.watchDogConn);
300
301
  clearTimeout(device.timer);
@@ -394,7 +395,7 @@ module.exports = function(RED) {
394
395
  };
395
396
 
396
397
  function messageConstructor(messageType, message, device){
397
- let commands = ['play', 'stop', 'next', 'prev', 'ping'];
398
+ let commands = ['play', 'stop', 'next', 'prev', 'ping', 'softwareVersion'];
398
399
  let extraCommands = ['forward', 'backward', 'volumeup', 'volumedown', 'volume'];
399
400
  switch(messageType){
400
401
  case 'command':
@@ -454,7 +455,7 @@ module.exports = function(RED) {
454
455
  } else {
455
456
  debugMessage(`Bad command ${message.payload}`)
456
457
  //node.error(`You can send commands in msg.payload from list as String ${commands + extraCommands}`);
457
- return [{"command": "ping"}];
458
+ return [{"command": "softwareVersion"}];
458
459
  }
459
460
  case 'voice':
460
461
  debugMessage(`Message Voice command: ${message}`);
@@ -594,9 +595,9 @@ module.exports = function(RED) {
594
595
  }
595
596
 
596
597
  debugMessage('unknown command')
597
- return messageConstructor('command', { 'payload': 'ping' })
598
+ return messageConstructor('command', { 'payload': 'softwareVersion' })
598
599
  } else {
599
- return messageConstructor('command', { 'payload': 'ping' })
600
+ return messageConstructor('command', { 'payload': 'softwareVersion' })
600
601
  }
601
602
  case 'raw':
602
603
  if (Array.isArray(message.payload)) { return message.payload }
@@ -664,11 +665,11 @@ module.exports = function(RED) {
664
665
  }
665
666
  }
666
667
  function onPing(device) {
667
- if (device) {sendMessage(device.id, 'command', {payload: 'ping'});}
668
+ if (device) {sendMessage(device.id, 'command', {payload: 'softwareVersion'});}
668
669
  }
669
670
 
670
671
  function onPing(device) {
671
- sendMessage(device.id, 'command', {payload: 'ping'});
672
+ sendMessage(device.id, 'command', {payload: 'softwareVersion'});
672
673
  }
673
674
  function getStatus(id) {
674
675
  let device = searchDeviceByID(id);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-yandex-station-management",
3
- "version": "0.3.7",
3
+ "version": "0.3.9",
4
4
  "description": "Local management of YandexStation using API on websockets",
5
5
  "main": "index.js",
6
6
  "scripts": {