node-red-contrib-yandex-station-management 0.2.7 → 0.3.3

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
@@ -2,8 +2,11 @@
2
2
  С помощью модуля можно через локальное API управлять вопсроизведением на устройсвах Яндекса:
3
3
  - Яндекс Станция(протестировано)
4
4
  - Яндекс Станция мини(протестировано)
5
+ - Яндекс Станция мини 2 с экраном(протестировано)
6
+ - Яндекс Станция лайт(протестировано)
5
7
  - Яндекс Станци Макс(протестировано)
6
8
  - Яндекс Модуль(не протестировано)
9
+ - Яндекс Модуль - 2 (в процессе тестирования)
7
10
  - JBL Link Music(не протестировано)
8
11
  - JBL Link Portable(протестировано)
9
12
 
@@ -14,7 +17,7 @@
14
17
 
15
18
 
16
19
  Для работы требуется токен от Яндекс.Музыки.
17
- В модуле в экспериментальном режиме реализована возможность получения токена из логин-пароля(Спасибо слать [сюда](https://github.com/twocolors)). Если получение токена не отрабатывает, то стоит попробовать включить и отключить двух-факторную аутентификацию в настройках Яндекса. [Источник](https://github.com/AlexxIT/YandexStation/issues/103). Убедиться в безопасности использования учетных данных можно посмотрев [код](./nodes/yandex-login.html)
20
+ В модуле в экспериментальном режиме реализована возможность получения токена из логин-пароля(Спасибо слать [сюда](https://github.com/twocolors)). Если получение токена не отрабатывает, то стоит попробовать включить и отключить двух-факторную аутентификацию в настройках Яндекса. [Источник](https://github.com/AlexxIT/YandexStation/issues/103). Убедиться в безопасности использования учетных данных можно, посмотрев [код](./nodes/yandex-login.html)
18
21
 
19
22
  Второй из варинатов его получения описан в [FAQ](#faq)
20
23
 
@@ -36,7 +39,9 @@
36
39
  ## Первоначальная настройка.
37
40
  После установки для начала работы добавить любую ноду, ввести учетные данные(токен) в раздел Login, сохранить и нажать Deploy(обязательно!). Как получить токен - написано в FAQ.
38
41
 
39
- После деплоя в настройках ноды в поле Station должны появиться станции доступные для управления. Если станция не появилась в списке, то можно подождать пару минут или перезапустить Node-Red.
42
+ После деплоя в настройках ноды в поле Station должны появиться станции доступные для управления.
43
+
44
+ Если станция не появилась в списке, то можно подождать пару минут или перезапустить Node-Red.
40
45
 
41
46
  ## Описание возможностей и сценариев использования.
42
47
  ### Нода Station
@@ -109,13 +114,54 @@ Phrase to say - фраза, которую скажет Алиса вместо
109
114
  Отправка команды, вместо того, чтобы говорить ее колонке голосом: "Включи свет", "Включи музыку", "Включи мой плейлист", "Отключись через 15 минут" и так далее.
110
115
 
111
116
  #### TTS.
112
- Воспросизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам. Есть ряд опций:
113
- - Fixed volume level. Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения уровень громкости вернется в изначальный.
114
- - Prevent listening. Если выбрано, то колонка после воспроизведения не "слушает", что ей ответят.
115
- - Pause while TTS. Ставит воспроизведение плеера на паузу на время речи. Воспроизведение будет продолжено, только если что-то играло на момент поступления команды.
117
+ Воспроизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.
118
+ Параметры для TTS могут задаваться как в настройках, так и некоторые из них могут быть переопределены входящим сообщением.
119
+ - Text. Откуда и какой текст проговорить.
120
+ - из входящего сообщения (msg.payload по-умолчанию)
121
+ - строго заданную строку
122
+ - переменную из flow или global context
123
+ - JSON: выбрать сообщение случайным образом из массива вида ["один", "два", "три"].
124
+ - Voice. Выбор голоса для генерации. Может быть переопределено через msg.voice
125
+ - Effect. Эффекты для генерации голоса. Может быть переопределено через msg.effect
126
+ -
127
+ Есть ряд опций:
128
+ - Volume. Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения уровень громкости вернется в изначальный. Может быть переопределено через msg.volume
129
+ - Whisper. Позволяет произнести фразу шептом.. Переопределяется через msg.whisper
130
+ - Prevent listening. Если выбрано, то колонка после воспроизведения не "слушает", что ей ответят. Может быть переопределено через msg.prevent_listening
131
+ - Pause while TTS. Ставит воспроизведение плеера на паузу на время речи. Воспроизведение будет продолжено, только если что-то играло на момент поступления команды. Может быть переопределено через msg.pause_music
116
132
  Все опции комбинируемы между собой.
117
133
 
118
- Работают голосовые [спецэффекты](https://yandex.ru/dev/dialogs/alice/doc/speech-effects-docpage/),[дополнительные голоса](https://yandex.ru/dev/dialogs/alice/doc/speech-effects-docpage/) и [звуки](https://yandex.ru/dev/dialogs/alice/doc/sounds-docpage/), при этом отправка производится в ввиде строки. Например, "смелость sil <[500]> город+а берёт"
134
+ ##### Добавление голосу жизни и красок.
135
+ ###### Раставляйте ударения
136
+ При необходимости ударные гласные в словах следует отмечать знаком «+», например:
137
+
138
+ остр+ота
139
+ м+ука
140
+
141
+ ##### Разделяйте слова
142
+ Длинные слова можно разбить на слова покороче и проставить ударения для каждого из этих коротких слов, например:
143
+
144
+ мн+ого пр+офильный
145
+ с+еми пал+атинск
146
+
147
+ ##### Меняйте написание слов
148
+ Некоторые слова можно попробовать писать так, как они слышатся:
149
+
150
+ «ненастный» — нен+асный
151
+ «пожалуйста» — пож+алуста
152
+
153
+ ###### Добавляйте паузы
154
+ Чтобы задать паузу между словами, используйте синтаксис sil <[ количество_миллисекунд ]>. Например:
155
+
156
+ смелость sil <[500]> город+а берёт
157
+
158
+ Каждый отделенный пробелами пунктуационный знак обозначается паузой в 50-100 мс.
159
+
160
+ ###### Добавляйте [звуки из библиотеки](https://yandex.ru/dev/dialogs/alice/doc/sounds-docpage/)
161
+
162
+ <speaker audio=\"alice-sounds-game-win-1.opus\"> У вас получилось!
163
+
164
+
119
165
 
120
166
 
121
167
  #### Homekit Formatted.
@@ -191,6 +237,7 @@ Phrase to say - фраза, которую скажет Алиса вместо
191
237
  }
192
238
  ```
193
239
  10. Отправить "Текст" для TTS.
240
+ Больше не работает!
194
241
  ```json
195
242
  {
196
243
  "command" : "sendText",
@@ -259,7 +306,10 @@ Phrase to say - фраза, которую скажет Алиса вместо
259
306
  ```json
260
307
  "value": "<speaker audio='alice-sounds-game-win-1.opus'>У вас получилось!"
261
308
  ```
262
-
309
+ - Говорить шепотом
310
+ ```json
311
+ "value": "<speaker is_whisper="true"'>Я говорю тихо-тихо!"
312
+ ```
263
313
  - Совмещение эффектов
264
314
  ```json
265
315
  "value": "<speaker voice='kostya' audio='alice-sounds-game-win-1.opus' effect='megaphone'>добро пожаловать"
package/nodes/get.js CHANGED
@@ -10,7 +10,7 @@ module.exports = function(RED) {
10
10
  node.lastState = {};
11
11
  node.status({});
12
12
 
13
-
13
+
14
14
  function debugMessage(text){
15
15
  if (node.debugFlag) {
16
16
  node.log(text);
@@ -21,25 +21,37 @@ module.exports = function(RED) {
21
21
  if (node.output == 'status') {
22
22
  inputMsg.payload = message;
23
23
  } else if (node.output == 'homekit') {
24
- if (node.homekitFormat == 'speaker') {
25
- (message.playerState)? inputMsg.payload = {
26
- "CurrentMediaState": (message.playing) ? 0 : 1,
27
- "ConfiguredName": `${(message.playerState.subtitle) ? message.playerState.subtitle : 'No Artist'} - ${(message.playerState.title) ? message.playerState.title : 'No Track Name'}`
28
- } :inputMsg.payload = {
29
- "CurrentMediaState": (message.playing) ? 0 : 1,
30
- "ConfiguredName": `No Artist - No Track`
31
- }
32
- }else if (node.homekitFormat == 'tv') {
33
- inputMsg.payload = {
34
- "Active": (message.playing) ? 1 : 0
35
- }
36
-
24
+ try {
25
+ if (node.homekitFormat == 'speaker') {
26
+ let ConfiguredName = `${(message.playerState.subtitle) ? message.playerState.subtitle : 'No Artist'} - ${(message.playerState.title) ? message.playerState.title : 'No Track Name'}`;
27
+ let title = `${message.playerState.title}`;
28
+ if (ConfiguredName.length > 64 && title.length > 0 && title.length <= 64) {
29
+ ConfiguredName = title;
30
+ } else {
31
+ ConfiguredName = title.substr(0, 61) + `...`;
32
+ }
33
+ (message.playerState)? inputMsg.payload = {
34
+ "CurrentMediaState": (message.playing) ? 0 : 1,
35
+ "ConfiguredName": ConfiguredName
36
+ } :inputMsg.payload = {
37
+ "CurrentMediaState": (message.playing) ? 0 : 1,
38
+ "ConfiguredName": `No Artist - No Track Name`
39
+ }
40
+ }else if (node.homekitFormat == 'tv') {
41
+ inputMsg.payload = {
42
+ "Active": (message.playing) ? 1 : 0
43
+ }
44
+
45
+ }
46
+ } catch (error) {
47
+ debugMessage(`Error while preparing payload: `+ e);
37
48
  }
49
+
38
50
  }
39
51
  return inputMsg;
40
52
 
41
53
  }
42
-
54
+
43
55
 
44
56
  node.onStatus = function(data) {
45
57
  if (data) {
@@ -57,7 +69,7 @@ module.exports = function(RED) {
57
69
  node.onClose = function(){
58
70
  node.controller.removeListener(`message_${node.station}`, node.onMessage)
59
71
  }
60
-
72
+
61
73
  node.on('input', node.onInput);
62
74
 
63
75
  node.on('close', node.onClose)
package/nodes/in.js CHANGED
@@ -10,9 +10,9 @@ module.exports = function(RED) {
10
10
  node.homekitFormat = config.homekitFormat;
11
11
  node.lastMessage = {};
12
12
  node.status({});
13
-
14
13
 
15
-
14
+
15
+
16
16
  debugMessage(`Node settings: ID: ${node.station}, Output Format: ${node.output}, HK: ${node.homekitFormat}`);
17
17
  function debugMessage(text){
18
18
  if (node.debugFlag) {
@@ -26,20 +26,32 @@ module.exports = function(RED) {
26
26
  if (node.output == 'status') {
27
27
  payload = {'payload': message}
28
28
  } else if (node.output == 'homekit') {
29
- if (node.homekitFormat == 'speaker') {
30
- (message.playerState)? payload = {'payload': {
31
- "CurrentMediaState": (message.playing) ? 0 : 1,
32
- "ConfiguredName": `${(message.playerState.subtitle) ? message.playerState.subtitle : 'No Artist'} - ${(message.playerState.title) ? message.playerState.title : 'No Track Name'}`
33
- } }:payload = {'payload': {
34
- "CurrentMediaState": (message.playing) ? 0 : 1,
35
- "ConfiguredName": `No Artists - No Track`
36
- } }
37
- }else if (node.homekitFormat == 'tv') {
38
- payload = {'payload': {
39
- "Active": (message.playing) ? 1 : 0
40
- }
41
- }
29
+ try {
30
+ if (node.homekitFormat == 'speaker') {
31
+ let ConfiguredName = `${(message.playerState.subtitle) ? message.playerState.subtitle : 'No Artist'} - ${(message.playerState.title) ? message.playerState.title : 'No Track Name'}`;
32
+ let title = `${message.playerState.title}`;
33
+ if (ConfiguredName.length > 64 && title.length > 0 && title.length <= 64) {
34
+ ConfiguredName = title;
35
+ } else {
36
+ ConfiguredName = title.substr(0, 61) + `...`;
37
+ }
38
+ (message.playerState)? payload = {'payload': {
39
+ "CurrentMediaState": (message.playing) ? 0 : 1,
40
+ "ConfiguredName": ConfiguredName
41
+ } }:payload = {'payload': {
42
+ "CurrentMediaState": (message.playing) ? 0 : 1,
43
+ "ConfiguredName": `No Artists - No Track Name`
44
+ } }
45
+ }else if (node.homekitFormat == 'tv') {
46
+ payload = {'payload': {
47
+ "Active": (message.playing) ? 1 : 0
48
+ }
49
+ }
50
+ }
51
+ } catch(e) {
52
+ debugMessage(`Error while preparing payload: `+ e)
42
53
  }
54
+
43
55
  }
44
56
  return payload;
45
57
 
@@ -50,8 +62,8 @@ module.exports = function(RED) {
50
62
  if ((JSON.stringify(node.lastMessage.payload) != JSON.stringify(message.payload))) {
51
63
  node.send(message)
52
64
  node.lastMessage = message
53
- debugMessage(`Sended message to HK: ${JSON.stringify(message)}`);
54
-
65
+ debugMessage(`Sended message to HK: ${JSON.stringify(message)}`);
66
+
55
67
  }
56
68
  } else {
57
69
  node.send(message);
@@ -77,7 +89,7 @@ module.exports = function(RED) {
77
89
  node.controller.on(`message_${node.station}`, node.onMessage);
78
90
  node.controller.on(`statusUpdate_${node.station}`, node.onStatus);
79
91
  }
80
-
92
+
81
93
  node.on('close', node.onClose);
82
94
 
83
95
 
@@ -1,169 +1,281 @@
1
1
  <script type="text/javascript">
2
-
3
- RED.nodes.registerType('alice-local-out',{
4
- category: 'Yandex Station',
5
- color: '#b89fcc',
6
- defaults: {
7
- name:{value: ""},
8
- token: {
9
- type: "yandex-login",
10
- required: true
11
- },
12
- station_id: {
13
- required: true
14
- },
15
- debugFlag: {
16
- value: false
17
- },
18
- input: {
19
- value: "command",
20
- required: true
21
- },
22
- volume: {},
23
- volumeFlag: {
24
- value: false
25
- },
26
- stopListening: {
27
- value: true
28
- },
29
- pauseMusic: {
30
- value: false
31
- },
32
- noTrack: {
33
- }
34
- },
35
- inputs:1,
36
- outputs:0,
37
- icon: "station.png",
38
- label: function() {
39
- return this.name||this.station_id
40
- },
41
- paletteLabel: "Yandex OUT",
42
- oneditprepare: onOpen
2
+
3
+ RED.nodes.registerType('alice-local-out', {
4
+ category: 'Yandex Station',
5
+ color: '#b89fcc',
6
+ defaults: {
7
+ name: {value: ''},
8
+ token: {
9
+ type: 'yandex-login',
10
+ required: true,
11
+ },
12
+ station_id: {
13
+ required: true,
14
+ },
15
+ debugFlag: {
16
+ value: false,
17
+ },
18
+ input: {
19
+ value: 'command',
20
+ required: true,
21
+ },
22
+ payload: {
23
+ value: 'payload',
24
+ },
25
+ payloadType: {
26
+ value: 'msg',
27
+ },
28
+ volume: {},
29
+ volumeFlag: {
30
+ value: false,
31
+ },
32
+ stopListening: {
33
+ value: true,
34
+ },
35
+ pauseMusic: {
36
+ value: false,
37
+ },
38
+ noTrack: {},
39
+ whisper: {
40
+ value: false,
41
+ },
42
+ ttsVoice: {
43
+ value: null,
44
+ },
45
+ ttsEffect: {
46
+ value: null,
47
+ required: false,
48
+ },
49
+ },
50
+ inputs: 1,
51
+ outputs: 0,
52
+ icon: 'station.png',
53
+ label: function() {
54
+ return this.name || this.station_id;
55
+ },
56
+ paletteLabel: 'Yandex OUT',
57
+ oneditprepare: onOpen,
58
+ });
59
+
60
+ function onOpen() {
61
+ var command = $('#node-input-input').val();
62
+
63
+ $('#node-input-payload').typedInput({
64
+ types: ['msg', 'str', 'flow', 'global', 'json'],
65
+ default: 'msg',
66
+ value: 'payload',
67
+ typeField: $('#node-input-payloadType'),
43
68
  });
44
- function onOpen() {
45
- let config = RED.nodes.node($('#node-input-token').val());
46
- let selector = $('#node-input-station_id');
47
- selector.empty();
48
- let currentId = this.station_id;
49
- $.getJSON('yandexdevices_'+config.id,function(data) {
50
- data.devices.forEach(device => {
51
- selector.append(`<option value="${device.id}">
69
+
70
+
71
+
72
+ $("#node-input-ttsEffect").typedInput({type:"ttsEffect", types:[{
73
+ value: 'ttsEffect',
74
+ multiple: false,
75
+ options: [
76
+ { value: "", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.none")},
77
+ { value: "behind_the_wall", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.behind_the_wall")},
78
+ { value: "hamster", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.hamster")},
79
+ { value: "megaphone", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.megaphone")},
80
+ { value: "pitch_down", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.pitch_down")},
81
+ { value: "psychodelic", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.psychodelic")},
82
+ { value: "pulse", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.pulse")},
83
+ { value: "train_announce", label: RED._("node-red-contrib-yandex-station-management/alice-local-out:effect.train_announce")}
84
+ ]
85
+ }]})
86
+
87
+ let config = RED.nodes.node($('#node-input-token').val());
88
+ let selector = $('#node-input-station_id');
89
+ selector.empty();
90
+ let currentId = this.station_id;
91
+ $.getJSON('yandexdevices_' + config.id, function(data) {
92
+ data.devices.forEach(device => {
93
+ selector.append(`<option value="${device.id}">
52
94
  ${device.name}(${device.id})
53
- </option>`)
54
- $(`#node-input-station_id :contains(${currentId})`).attr("selected", "selected");
55
- });
56
- });
57
- $('#node-input-input').on('change', function(type, value) {
58
- //var val = $(this).val();
59
- //alert(val);
60
- if ($(this).val() == 'tts') {
61
- $('#volume-range').show();
62
- $('#listening-checkbox').show();
63
- $('#pause-checkbox').show();
64
- if ($('#node-input-volumeFlag').prop("checked")){$('#node-input-volume').show();$('#range-label').show();}
65
-
66
- } else {
67
- $('#volume-range').hide();
68
- $('#listening-checkbox').hide();
69
- $('#pause-checkbox').hide();
70
-
71
- }
72
- if ($(this).val() == 'homekit') {
73
- $('#default-command').show();
74
- } else {
75
- $('#default-command').hide();
76
- }
77
- });
78
- //$('#node-input-volume').hide();
79
- $('#node-input-volumeFlag').on('change', function(type, value) {
80
- let val = $(this).val();
81
- //alert($(this).val());
82
- if ($('#node-input-input').val() == 'tts' && $(this).prop("checked")) {
83
- $('#node-input-volume').show();
84
- $('#range-label').show();
85
-
86
- } else if ($('#node-input-input').val() == 'tts' && !$(this).prop("checked") ) {
87
- $('#node-input-volume').hide();
88
- $('#range-label').hide();
89
-
90
- }
91
- })
92
- $('#node-input-volume').on('change', function() {
93
- $('#volume-level').text(`${parseFloat($('#node-input-volume').val())*100}`)
94
- })
95
- }
96
- function updateDevices() {
97
-
98
-
99
- }
95
+ </option>`);
96
+ $(`#node-input-station_id :contains(${currentId})`).attr('selected', 'selected');
97
+ });
98
+ });
99
+
100
+ $('.command_options').hide();
101
+ $('.command_options-' + command).show();
102
+
103
+ $('#node-input-input').on('change', function(type, value) {
104
+ $('.command_options').hide();
105
+ $('.command_options-' + $(this).val()).show();
106
+
107
+ if ($(this).val() == 'tts') {
108
+ if ($('#node-input-volumeFlag').prop('checked')) {
109
+ $('#node-input-volume').show();
110
+ $('#range-label').show();
111
+ }
112
+ }
113
+ });
114
+
115
+ $('#node-input-volumeFlag').on('change', function(type, value) {
116
+ let val = $(this).val();
117
+ if ($('#node-input-input').val() == 'tts' && $(this).prop('checked')) {
118
+ $('#node-input-volume').show();
119
+ $('#range-label').show();
120
+
121
+ } else if ($('#node-input-input').val() == 'tts' && !$(this).prop('checked')) {
122
+ $('#node-input-volume').hide();
123
+ $('#range-label').hide();
124
+
125
+ }
126
+ });
127
+ $('#node-input-volume').on('change', function() {
128
+ $('#volume-level').text(`${parseFloat($('#node-input-volume').val())}`);
129
+ });
130
+ }
100
131
  </script>
101
132
 
102
133
  <script type="text/html" data-template-name="alice-local-out">
103
134
  <style>
104
- .label-long {min-width: 150px;width: 20%;}
105
- .online {display: inline-block; width: auto; vertical-align: middle;}
106
- </style>
135
+ label.label {
136
+ line-height: 1em;
137
+ }
138
+ label.label-long {
139
+ min-width: 150px;
140
+ width: 20%;
141
+ }
142
+ .online {
143
+ display: inline-block;
144
+ width: auto;
145
+ vertical-align: middle;
146
+ }
147
+ </style>
107
148
 
108
149
  <div class="form-row">
109
- <label for="node-input-name"><i class="fa fa-bookmark"></i>Name</label>
110
- <input type="text" id="node-input-name" placeholder="">
150
+ <label for="node-input-name"><i class="fa fa-bookmark"></i>&nbsp;<span data-i18n="label.name"></span></label>
151
+ <input type="text" id="node-input-name">
111
152
  </div>
112
153
  <div class="form-row">
113
- <label for="node-input-token"><i class="fa fa-globe"></i>Login</label>
114
- <input type="text" id="node-input-token" placeholder="token">
154
+ <label for="node-input-token"><i class="fa fa-globe"></i>&nbsp;<span data-i18n="label.login"></span></label>
155
+ <input type="text" id="node-input-token" placeholder="">
115
156
  </div>
116
157
  <div class="form-row">
117
- <label for="node-input-station_id"><i class="fa fa-database">Station</i></label>
158
+ <label for="node-input-station_id"><i class="fa fa-database"></i>&nbsp;<span data-i18n="label.station"></span></label>
118
159
  <div style="display: inline-block;position: relative;width: 70%;height: 20px;">
119
- <div style="position: absolute;left: 0px; right: 40px;">
160
+ <div style="position: absolute;left: 0; right: 40px;">
120
161
  <select id="node-input-station_id" data-single="true" style="width: 100%"></select>
121
- </div>
162
+ </div>
122
163
  <div style="text-align: end; display: inline; float: right">
123
- <button onclick="onOpen()" class="red-ui-button" style="position: absolute;right: 0px;top: 0px;"><i class="fa fa-refresh"></i></button>
164
+ <button onclick="onOpen()" class="red-ui-button" style="position: absolute;right: 0px;top: 0px;">
165
+ <i class="fa fa-refresh"></i></button>
124
166
  </div>
125
167
  </div>
126
168
  </div>
127
169
  <div class="form-row">
128
- <label for="node-input-input"><i class="fa fa-arrow-circle-o-right">Node Input</i></label>
170
+ <label for="node-input-input"><i class="fa fa-arrow-circle-o-right"></i>&nbsp;<span data-i18n="label.command"></span></label>
129
171
  <div style="display: inline-block;position: relative;width: 70%;height: 20px;">
130
172
  <div style="position: absolute;left: 0px; right: 0px;">
131
173
  <select id="node-input-input" data-single="true" style="width: 100%">
132
- <option value="command">Player Command</option>
133
- <option value="voice">Voice Command</option>
134
- <option value="tts">TTS</option>
135
- <option value="homekit">Homekit Formatted</option>
136
- <option value="raw">Raw Command</option>
137
- <option value="stopListening">Stop Listening</option>
174
+ <option value="command" data-i18n="command.player"></option>
175
+ <option value="voice" data-i18n="command.voice"></option>
176
+ <option value="tts" data-i18n="command.tts"></option>
177
+ <option value="homekit" data-i18n="command.homekit"></option>
178
+ <option value="stopListening" data-i18n="command.stop_listening"></option>
179
+ <option value="raw" data-i18n="command.raw"></option>
138
180
  </select>
139
- </div>
181
+ </div>
140
182
  </div>
141
183
  </div>
142
184
 
143
- <div class="form-row" id="volume-range">
144
- <label for="node-input-volume" class="label-long"><i class="fa fa-volume-up"></i>Fixed volume Level</label>
145
- <input type="checkbox" id="node-input-volumeFlag" style="display: inline-block; width: auto; vertical-align: baseline;">
146
- <input type="range" id="node-input-volume" name="volume" min="0" max="1" step="0.1" style="display: inline-block; width: 150px; margin-left: 10px; vertical-align: middle;">
185
+ <!--for homekit-->
186
+ <div class="form-row command_options command_options-homekit">
187
+ <label for="node-input-noTrack" class="label-long" style="display: inline-block"><i class="fa fa-podcast"></i>&nbsp;<span data-i18n="label.default_command"></span></label>
188
+ <input type="text" id="node-input-noTrack" data-i18n="[placeholder]placeholder.play_music" style="display: inline-block;left: 0px; right: 0px; width: 60%; vertical-align: middle;">
189
+ </div>
190
+
191
+ <!--for tts-->
192
+ <div class="form-row command_options command_options-tts">
193
+ <label for="node-input-payload" class="l-width"><i class="fa fa-envelope"></i> <span data-i18n="label.text"></span></label>
194
+ <input type="text" id="node-input-payload" style="width:70%">
195
+ <input type="hidden" id="node-input-payloadType">
196
+ </div>
197
+ <div class="form-row command_options command_options-tts">
198
+ <label class="label" for='node-input-ttsVoice'>
199
+ <i class='fa fa-user'></i>&nbsp;<span data-i18n="label.voice"></span>
200
+ <div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.voice</div></label>
201
+ </label>
202
+ <div style="display: inline-block;position: relative;width: 70%;height: 30px;">
203
+ <div style="position: absolute;left: 0px; right: 0px;">
204
+ <select id="node-input-ttsVoice" style="width: 100%;vertical-align: top;">
205
+ <option value="" data-i18n="voice.default"></option>
206
+ <!-- <option value="alena" data-i18n="voice.alena"></option>-->
207
+ <option value="alyss" data-i18n="voice.alyss"></option>
208
+ <option value="anton_samokhvalov" data-i18n="voice.anton_samokhvalov"></option>
209
+ <option value="dude" data-i18n="voice.dude"></option>
210
+ <option value="ermil" data-i18n="voice.ermil"></option>
211
+ <option value="ermilov" data-i18n="voice.ermilov"></option>
212
+ <option value="ermil_with_tuning" data-i18n="voice.ermil_with_tuning"></option>
213
+ <option value="erkanyavas" data-i18n="voice.erkanyavas"></option>
214
+ <!-- <option value="filipp" data-i18n="voice.filipp"></option>-->
215
+ <option value="jane" data-i18n="voice.jane"></option>
216
+ <option value="kolya" data-i18n="voice.kolya"></option>
217
+ <option value="kostya" data-i18n="voice.kostya"></option>
218
+ <option value="levitan" data-i18n="voice.levitan"></option>
219
+ <option value="nastya" data-i18n="voice.nastya"></option>
220
+ <option value="nick" data-i18n="voice.nick"></option>
221
+ <option value="oksana" data-i18n="voice.oksana"></option>
222
+ <option value="omazh" data-i18n="voice.omazh"></option>
223
+ <option value="robot" data-i18n="voice.robot"></option>
224
+ <option value="sasha" data-i18n="voice.sasha"></option>
225
+ <option value="silaerkan" data-i18n="voice.silaerkan"></option>
226
+ <option value="smoky" data-i18n="voice.smoky"></option>
227
+ <option value="tanya" data-i18n="voice.tanya"></option>
228
+ <option value="tatyana_abramova" data-i18n="voice.tatyana_abramova"></option>
229
+ <option value="zahar" data-i18n="voice.zahar"></option>
230
+ <option value="zhenya" data-i18n="voice.zhenya"></option>
231
+ <option value="zombie" data-i18n="voice.zombie"></option>
232
+ <option value="voicesearch" data-i18n="voice.voicesearch"></option>
233
+ </select>
234
+ </div>
235
+ </div>
236
+ </div>
237
+ <div class="form-row command_options command_options-tts">
238
+ <label for="node-input-ttsEffect" class="label l-width" style="vertical-align: bottom;">
239
+ <i class="fa fa-magic"></i> <span data-i18n="label.effect"></span>
240
+ <div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.effect</div></label>
241
+ </label>
242
+ <input type="text" id="node-input-ttsEffect">
243
+ </div>
244
+ <div class="form-row command_options command_options-tts">
245
+ <label for="node-input-volume" class="label label-long">
246
+ <i class="fa fa-volume-up"></i>&nbsp;<span data-i18n="label.volume"></span>
247
+ <div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.volume</div></label>
248
+ </label>
249
+ <input type="checkbox" id="node-input-volumeFlag" style="display: inline-block; width: auto; vertical-align: top;">
250
+ <input type="range" id="node-input-volume" name="volume" min="0" max="100" step="1" style="display: inline-block; width: 150px; margin-left: 10px; vertical-align: middle;">
147
251
  <div id="range-label" class='online'><span id="volume-level" class="online"></span><span class="online">%</span></div>
148
252
  </div>
149
- <div class="form-row" id="listening-checkbox">
150
- <label for="node-input-stopListening" class="label-long"><i class="fa fa-deaf"></i>Prevent Listening</label>
151
- <input type="checkbox" id="node-input-stopListening" style="display: inline-block; width: auto; vertical-align: baseline;">
253
+ <div class="form-row command_options command_options-tts">
254
+ <label for="node-input-whisper" class="label label-long">
255
+ <i class="fa fa-deaf"></i>&nbsp;<span data-i18n="label.whisper"></span>
256
+ <div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.whisper</div></label>
257
+ </label>
258
+ <input type="checkbox" id="node-input-whisper" style="display: inline-block; width: auto; vertical-align: top;">
152
259
  </div>
153
- <div class="form-row" id="pause-checkbox">
154
- <label for="node-input-pauseMusic" class="label-long"><i class="fa fa-pause"></i>Pause while TTS</label>
155
- <input type="checkbox" id="node-input-pauseMusic" style="display: inline-block; width: auto; vertical-align: baseline;">
260
+ <div class="form-row command_options command_options-tts">
261
+ <label for="node-input-stopListening" class="label label-long">
262
+ <i class="fa fa-deaf"></i>&nbsp;<span data-i18n="label.prevent_listening"></span>
263
+ <div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.prevent_listening</div></label>
264
+ </label>
265
+ <input type="checkbox" id="node-input-stopListening" style="display: inline-block; width: auto; vertical-align: top;">
156
266
  </div>
157
- <div class="form-row" id="default-command">
158
- <label for="node-input-noTrack" class="label-long" style="display: inline-block"><i class="fa fa-podcast"></i>Default command</label>
159
- <input type="text" id="node-input-noTrack" placeholder="Включи музыку" style="display: inline-block;left: 0px; right: 0px; width: 60%; vertical-align: middle;">
267
+ <div class="form-row command_options command_options-tts">
268
+ <label for="node-input-pauseMusic" class="label label-long">
269
+ <i class="fa fa-pause"></i>&nbsp;<span data-i18n="label.pause_while_tts"></span>
270
+ <div class="red-ui-debug-msg-type-string" style="font-size: 10px;">msg.pause_music</div></label>
271
+ </label>
272
+ <input type="checkbox" id="node-input-pauseMusic" style="display: inline-block; width: auto; vertical-align: top;">
160
273
  </div>
274
+
275
+
161
276
  <div class="form-row" style="vertical-align:bottom;">
162
- <label for='node-input-debugFlag'><i class='fa fa-share-square'>Debug</i></label>
163
- <input type="checkbox" id="node-input-debugFlag" checked="checked" style="display: inline-block; width: auto; vertical-align: top;">
277
+ <label for='node-input-debugFlag' class="label-long"><i class='fa fa-share-square'></i>&nbsp;<span data-i18n="label.debug"></span></label>
278
+ <input type="checkbox" id="node-input-debugFlag" checked="checked" style="display: inline-block; width: auto; vertical-align: top;">
164
279
  </div>
165
280
  </script>
166
281
 
167
- <script type="text/html" data-help-name="alice-local-out">
168
- <p> Output node for Yandex Station local management</p>
169
- </script>
@@ -2,7 +2,9 @@ module.exports = function(RED) {
2
2
  function AliceLocalOutNode(config) {
3
3
  RED.nodes.createNode(this,config);
4
4
  let node = this;
5
+ node.config = config;
5
6
  node.controller = RED.nodes.getNode(config.token);
7
+
6
8
  node.input = config.input;
7
9
  node.station = config.station_id;
8
10
  node.debugFlag = config.debugFlag;
@@ -11,6 +13,9 @@ module.exports = function(RED) {
11
13
  node.stopListening = config.stopListening;
12
14
  node.noTrackPhrase = config.noTrack;
13
15
  node.pauseMusic = config.pauseMusic;
16
+ node.ttsVoice = config.ttsVoice;
17
+ node.ttsEffect = config.ttsEffect;
18
+ node.whisper = config.whisper;
14
19
  node.status({});
15
20
 
16
21
  function debugMessage(text){
@@ -19,15 +24,106 @@ module.exports = function(RED) {
19
24
  }
20
25
  }
21
26
  debugMessage(node.station);
22
- node.on('input', (data) => {
23
- debugMessage(`input: ${JSON.stringify(data)}`)
24
- if (node.volumeFlag) {data.volume = node.volume}
25
- if (node.stopListening) {data.stopListening = node.stopListening}
26
- if (node.noTrackPhrase) {data.noTrackPhrase = node.noTrackPhrase}
27
- if (node.pauseMusic) {data.pauseMusic = node.pauseMusic}
27
+ node.on('input', (input) => {
28
+ debugMessage(`input: ${JSON.stringify(input)}`)
29
+
28
30
  if (node.station) {
29
- node.controller.sendMessage(node.station, node.input, data);
30
- debugMessage(`Sending data: station: ${node.station}, input type: ${node.input}, data: ${JSON.stringify(data)}`);
31
+ var data = {};
32
+ //data.payload = input.payload;
33
+
34
+ //apply node's config
35
+ if (node.volumeFlag) {data.volume = node.volume/100}
36
+ if (node.whisper) {data.whisper = node.whisper}
37
+ if (node.stopListening) {data.stopListening = node.stopListening}
38
+ if (node.noTrackPhrase) {data.noTrackPhrase = node.noTrackPhrase}
39
+ if (node.pauseMusic) {data.pauseMusic = node.pauseMusic}
40
+
41
+
42
+ //redefine options from input
43
+ if ("volume" in input) {data.volume = input.volume/100}
44
+ if ("whisper" in input) {data.whisper = input.whisper?true:false}
45
+ if ("voice" in input) {node.ttsVoice = input.voice}
46
+ if ("effect" in input) {node.ttsEffect = input.effect}
47
+ if ("prevent_listening" in input) {node.noTrackPhrase = input.prevent_listening}
48
+ if ("pause_music" in input) {data.pauseMusic = input.pause_music}
49
+
50
+
51
+ if ('tts' === node.input) {
52
+ let payload;
53
+ switch (node.config.payloadType) {
54
+ case 'flow': {
55
+ if (typeof(node.context().flow.get(node.config.payload)) != "undefined") {
56
+ payload = node.context().flow.get(node.config.payload)
57
+ } else {
58
+ debugMessage('Empty flow context with key '+ node.config.payload)
59
+ }
60
+ break;
61
+ }
62
+ case 'global': {
63
+ if (typeof(node.context().global.get(node.config.payload)) != "undefined") {
64
+ payload = node.context().global.get(node.config.payload)
65
+ } else {
66
+ debugMessage('Empty global context with key '+ node.config.payload)
67
+ }
68
+ break;
69
+
70
+ }
71
+ case 'str': {
72
+ payload = node.config.payload;
73
+ break;
74
+ }
75
+ case 'json': {
76
+ let arr = [];
77
+
78
+ try {
79
+ arr = JSON.parse(node.config.payload)
80
+ payload = arr[(Math.random() * arr.length) | 0];
81
+ } catch (e) {
82
+ debugMessage("Error on parsing input JSON: "+ e);
83
+ }
84
+
85
+ break;
86
+ }
87
+ case 'msg': {
88
+ payload = node.input[node.config.payload]
89
+ }
90
+ default: {
91
+ payload = input[node.config.payload];
92
+ break;
93
+ }
94
+ }
95
+ if (typeof(payload) != "undefined" ) {
96
+ data.payload = payload;
97
+ if (node.ttsVoice) {
98
+ data.payload = "<speaker voice='" + node.ttsVoice + "'>" + data.payload;
99
+ }
100
+ if (node.ttsEffect) {
101
+ let effectsArr = node.ttsEffect.split(',');
102
+ for (let ind in effectsArr) {
103
+ data.payload = "<speaker effect='" + effectsArr[ind] + "'>" + data.payload;
104
+ }
105
+ }
106
+
107
+ if (data.whisper) {
108
+ data.payload = "<speaker is_whisper='"+data.whisper+"'>" + data.payload;
109
+ }
110
+ } else {
111
+ data.payload = ""
112
+ }
113
+ if (data.payload.length > 0) {node.controller.sendMessage(node.station, node.input, data)
114
+ debugMessage(`Sending data: station: ${node.station}, input type: ${node.input}, data: ${JSON.stringify(data)}`);
115
+ } else {
116
+ debugMessage("Nothing to send. Check input and parameters")
117
+ }
118
+ } else {
119
+ data.payload = input.payload;
120
+ data.hap = input.hap;
121
+ node.controller.sendMessage(node.station, node.input, data);
122
+ debugMessage(`Sending data: station: ${node.station}, input type: ${node.input}, data: ${JSON.stringify(data)}`);
123
+ }
124
+
125
+ } else {
126
+ debugMessage('node.station is empty');
31
127
  }
32
128
  });
33
129
 
@@ -37,10 +133,10 @@ module.exports = function(RED) {
37
133
  node.status({fill: data.color,shape:"dot",text: data.text});
38
134
  }
39
135
  }
40
-
136
+
41
137
 
42
138
  node.on('close', () => {
43
- node.controller.removeListener(`statusUpdate_${node.station}`, node.onStatus)
139
+ node.controller.removeListener(`statusUpdate_${node.station}`, node.onStatus)
44
140
  });
45
141
 
46
142
  if (node.controller) {
@@ -49,4 +145,4 @@ module.exports = function(RED) {
49
145
  }
50
146
  }
51
147
  RED.nodes.registerType("alice-local-out",AliceLocalOutNode);
52
- }
148
+ }
@@ -0,0 +1,46 @@
1
+ <script type="text/html" data-help-name="alice-local-out">
2
+ <p>Output node for Yandex Station local management</p>
3
+
4
+
5
+ <h3>TTS</h3>
6
+ <p>Воспросизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.</p>
7
+ <dl class="message-properties">
8
+ <dt>Текст</dt><dd>Откуда брать текст: msg.payload, flow, global или Json - выбрать сообщение случайным образом из массива вида <code>["один", "два", "три"]</code>.</dd>
9
+ <dt class="optional">Whisper</dt><dd>Ответить шёпотом.</dd>
10
+ <dt class="optional">Эффект</dt><dd>Наложить эффект на голос.</dd>
11
+ <dt class="optional">Громкость</dt><dd>Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения, уровень громкости вернется в изначальный.</dd>
12
+ <dt class="optional">Voice</dt><dd>Say a phrase in a whisper.</dd>
13
+ <dt class="optional">Не ждать ответ</dt><dd>Если выбрано, то колонка, после воспроизведения, не "слушает", что ей ответят.</dd>
14
+ <dt class="optional">Плеер на паузу</dt><dd>Ставит воспроизведение плеера на паузу на время речи. Воспроизведение будет продолжено, только если что-то играло на момент поступления команды.</dd>
15
+ </dl>
16
+
17
+ <h4>Отмечайте ударения</h4>
18
+ <p>При необходимости ударные гласные в словах следует отмечать знаком «+», например:</p>
19
+ <ul>
20
+ <li><code>остр+ота</code></li>
21
+ <li><code>м+ука</code></li>
22
+ </ul>
23
+
24
+ <h4>Разделяйте слова</h4>
25
+ <p>Длинные слова можно разбить на слова покороче и проставить ударения для каждого из этих коротких слов, например:</p>
26
+ <ul>
27
+ <li><code>мн+ого пр+офильный</code></li>
28
+ <li><code>с+еми пал+атинск</code></li>
29
+ </ul>
30
+
31
+ <h4>Меняйте написание слов</h4>
32
+ <p>Некоторые слова можно попробовать писать так, как они слышатся:</p>
33
+ <ul>
34
+ <li><code>«ненастный» — нен+асный</code></li>
35
+ <li><code>«пожалуйста» — пож+алуста</code></li>
36
+ </ul>
37
+
38
+ <h4>Добавляйте паузы</h4>
39
+ <p>Чтобы задать паузу между словами, используйте синтаксис sil <[ количество_миллисекунд ]>. Например:</p>
40
+ <ul>
41
+ <li><code>смелость sil <[500]> город+а берёт</code></li>
42
+ </ul>
43
+ <p>Каждый отделенный пробелами пунктуационный знак обозначается паузой в 50-100 мс.</p>
44
+ </script>
45
+
46
+
@@ -0,0 +1,72 @@
1
+ {
2
+ "label": {
3
+ "name": "Name",
4
+ "text": "Text",
5
+ "effect": "Effect",
6
+ "login": "Login",
7
+ "station": "Station",
8
+ "voice": "Voice",
9
+ "debug": "Debug",
10
+ "command": "Command",
11
+ "volume": "Volume",
12
+ "prevent_listening": "Prevent listening",
13
+ "pause_while_tts": "Pause while TTS",
14
+ "whisper": "Whisper",
15
+ "default_command": "Default command"
16
+ },
17
+ "placeholder": {
18
+ "name": "Name",
19
+ "play_music": "Play music"
20
+ },
21
+ "command": {
22
+ "player": "Player",
23
+ "tts": "Text-to-Speech",
24
+ "voice": "Voice",
25
+ "raw": "Raw",
26
+ "stop_listening": "Stop listening",
27
+ "homekit": "Homekit"
28
+ },
29
+ "voice": {
30
+ "default": "Default",
31
+ "alena": "Alena",
32
+ "alyss": "Alyss",
33
+ "anton_samokhvalov": "Anton Samokhvalov",
34
+ "dude": "Dude",
35
+ "ermil": "Ermil",
36
+ "ermilov": "Ermilov",
37
+ "ermil_with_tuning": "Ermil (tuning)",
38
+ "erkanyavas": "Erkanyavas",
39
+ "filipp": "Filipp",
40
+ "jane": "Jane",
41
+ "kolya": "Kolya",
42
+ "kostya": "Kostya",
43
+ "levitan": "Levitan",
44
+ "nastya": "Nastya",
45
+ "nick": "Nick",
46
+ "oksana": "Oksana",
47
+ "omazh": "Omazh",
48
+ "robot": "Robot",
49
+ "sasha": "Sasha",
50
+ "silaerkan": "Silaerkan",
51
+ "smoky": "Smoky",
52
+ "tanya": "Tanya",
53
+ "tatyana_abramova": "Tatyana Abramova",
54
+ "zahar": "Zahar",
55
+ "zhenya": "Zhenya",
56
+ "zombie": "Zombie",
57
+ "voicesearch": "Voicesearch"
58
+ },
59
+ "effect": {
60
+ "none": "None",
61
+ "behind_the_wall": "behind_the_wall",
62
+ "hamster": "hamster",
63
+ "megaphone": "megaphone",
64
+ "pitch_down": "pitch_down",
65
+ "psychodelic": "psychodelic",
66
+ "pulse": "pulse",
67
+ "train_announce": "train_announce"
68
+ }
69
+ }
70
+
71
+
72
+
@@ -0,0 +1,43 @@
1
+ <script type="text/html" data-help-name="alice-local-out">
2
+ <p>Команда для яндекс станции</p>
3
+
4
+ <h3>TTS</h3>
5
+ <p>Воспросизведение голосом отправленных фраз - Text to Speech. Не имеет ограничения по символам.</p>
6
+ <dl class="message-properties">
7
+ <dt>Текст</dt><dd>Откуда брать текст: msg.payload, flow, global или Json - выбрать сообщение случайным образом из массива вида <code>["один", "два", "три"]</code>.</dd>
8
+ <dt class="optional">Голос</dt><dd>Изменить голос.</dd>
9
+ <dt class="optional">Эффект</dt><dd>Наложить эффект на голос.</dd>
10
+ <dt class="optional">Громкость</dt><dd>Позволяет произносить фразу заданной громкостью. Если не выбрано, то фраза произносится с текущим уровнем громкости. После произнесения, уровень громкости вернется в изначальный.</dd>
11
+ <dt class="optional">Шёпот</dt><dd>Ответить шёпотом.</dd>
12
+ <dt class="optional">Не ждать ответ</dt><dd>Если выбрано, то колонка, после воспроизведения, не "слушает", что ей ответят.</dd>
13
+ <dt class="optional">Плеер на паузу</dt><dd>Ставит воспроизведение плеера на паузу на время речи. Воспроизведение будет продолжено, только если что-то играло на момент поступления команды.</dd>
14
+ </dl>
15
+
16
+ <h4>Отмечайте ударения</h4>
17
+ <p>При необходимости ударные гласные в словах следует отмечать знаком «+», например:</p>
18
+ <ul>
19
+ <li><code>остр+ота</code></li>
20
+ <li><code>м+ука</code></li>
21
+ </ul>
22
+
23
+ <h4>Разделяйте слова</h4>
24
+ <p>Длинные слова можно разбить на слова покороче и проставить ударения для каждого из этих коротких слов, например:</p>
25
+ <ul>
26
+ <li><code>мн+ого пр+офильный</code></li>
27
+ <li><code>с+еми пал+атинск</code></li>
28
+ </ul>
29
+
30
+ <h4>Меняйте написание слов</h4>
31
+ <p>Некоторые слова можно попробовать писать так, как они слышатся:</p>
32
+ <ul>
33
+ <li><code>«ненастный» — нен+асный</code></li>
34
+ <li><code>«пожалуйста» — пож+алуста</code></li>
35
+ </ul>
36
+
37
+ <h4>Добавляйте паузы</h4>
38
+ <p>Чтобы задать паузу между словами, используйте синтаксис sil <[ количество_миллисекунд ]>. Например:</p>
39
+ <ul>
40
+ <li><code>смелость sil <[500]> город+а берёт</code></li>
41
+ </ul>
42
+ <p>Каждый отделенный пробелами пунктуационный знак обозначается паузой в 50-100 мс.</p>
43
+ </script>
@@ -0,0 +1,69 @@
1
+ {
2
+ "label": {
3
+ "name": "Название",
4
+ "text": "Текст",
5
+ "effect": "Эффект",
6
+ "login": "Логин",
7
+ "station": "Станция",
8
+ "voice": "Голос",
9
+ "debug": "Дебаг",
10
+ "command": "Команда",
11
+ "volume": "Громкость",
12
+ "prevent_listening": "Не ждать ответ",
13
+ "pause_while_tts": "Плеер на паузу",
14
+ "whisper": "Шёпот",
15
+ "default_command": "Команда"
16
+ },
17
+ "placeholder": {
18
+ "name": "Название",
19
+ "play_music": "Включи музыку"
20
+ },
21
+ "command": {
22
+ "player": "Плеер",
23
+ "tts": "Синтез речи из текста",
24
+ "voice": "Голосовая команда",
25
+ "stop_listening": "Перестать слушать",
26
+ "homekit": "Homekit",
27
+ "raw": "Сырая команда"
28
+ },
29
+ "voice": {
30
+ "default": "Алиса (стандартный)",
31
+ "alena": "Алёна (alena)",
32
+ "alyss": "Элис (alyss)",
33
+ "anton_samokhvalov": "Антон Самохвалов (anton_samokhvalov)",
34
+ "dude": "Чувак (dude)",
35
+ "ermil": "Ермил (ermil)",
36
+ "ermilov": "Ermilov (ermilov)",
37
+ "ermil_with_tuning": "Ермил Т (ermil_with_tuning)",
38
+ "erkanyavas": "Ерканявас (erkanyavas)",
39
+ "filipp": "Филипп (filipp)",
40
+ "jane": "Джейн (jane)",
41
+ "kolya": "Коля (kolya)",
42
+ "kostya": "Костя (kostya)",
43
+ "levitan": "Левитан (levitan)",
44
+ "nastya": "Настя (nastya)",
45
+ "nick": "Ник (nick)",
46
+ "oksana": "Оксана (oksana)",
47
+ "omazh": "Омаж (omazh)",
48
+ "robot": "Робот (robot)",
49
+ "sasha": "Саша (sasha)",
50
+ "silaerkan": "Силаеркан (silaerkan)",
51
+ "smoky": "Смоки (smoky)",
52
+ "tanya": "Таня (tanya)",
53
+ "tatyana_abramova": "Татьяна Абрамова (tatyana_abramova)",
54
+ "zahar": "Захар (zahar)",
55
+ "zhenya": "Женя (zhenya)",
56
+ "zombie": "Зомби (zombie)",
57
+ "voicesearch": "voicesearch"
58
+ },
59
+ "effect": {
60
+ "none": "Без эффекта",
61
+ "behind_the_wall": "Из-за стены (behind_the_wall)",
62
+ "hamster": "Хомяк (hamster)",
63
+ "megaphone": "Мегафон (megaphone)",
64
+ "pitch_down": "Низкий (pitch_down)",
65
+ "psychodelic": "Психоделический (psychodelic)",
66
+ "pulse": "С прерыванием (pulse)",
67
+ "train_announce": "Громкоговоритель (train_announce)"
68
+ }
69
+ }
@@ -45,7 +45,7 @@ module.exports = function(RED) {
45
45
  debugMessage(`Ready event fo ${device.id}`);
46
46
  node.emit(`deviceReady`, device);
47
47
  node.readyList.push({ 'name': device.name, 'id': device.id, 'platform': device.platform, 'address': device.address, 'port': device.port, 'host': device.host, 'parameters': device.parameters});
48
- node.emit('refreshHttp', node.activeStationList, node.readyList)
48
+ node.emit('refreshHttp', node.activeStationList, node.readyList);
49
49
  statusUpdate({"color": "yellow", "text": "connecting..."}, device);
50
50
  }
51
51
  }
@@ -97,15 +97,19 @@ module.exports = function(RED) {
97
97
  });
98
98
  //node.emit('refreshHttp', node.activeStationList, node.readyList)
99
99
  deviceListProcessing(node.deviceList)
100
+ try {
101
+ discoverDevices(node.deviceList)
102
+ .then(() => {
103
+ //debugMessage(`calling processing for ${node.deviceList.length} devices`);
104
+ deviceListProcessing(node.deviceList)
105
+
106
+ });
107
+ //debugMessage(node.id);
108
+ return node.deviceList;
109
+ } catch (error) {
110
+ debugMessage(`Error while searching: ${error}`);
111
+ }
100
112
 
101
- discoverDevices(node.deviceList)
102
- .then(() => {
103
- //debugMessage(`calling processing for ${node.deviceList.length} devices`);
104
- deviceListProcessing(node.deviceList)
105
-
106
- });
107
- //debugMessage(node.id);
108
- return node.deviceList;
109
113
  })
110
114
  .catch(function (err) {
111
115
  //node.emit('refreshHttp', node.readyList);
@@ -122,20 +126,28 @@ module.exports = function(RED) {
122
126
  await mDnsSd.discover({
123
127
  name: '_yandexio._tcp.local'
124
128
  }).then((result) => {
125
- //debugMessage(`Found ${result.length} devices`);
129
+ debugMessage(`MDNS. Found ${result.length} devices`);
130
+ node.emit('refreshHttpDNS', result);
126
131
  if (result.length != 0){
127
132
  for (const device of deviceList) {
128
133
  result.forEach(element => {
129
134
  let checkConnType = device.parameters.network || {}
130
135
  //если режим автоопределения адреса или набор параметров пустой, то записывать значния из результатов mdns поиска
131
136
  if (checkConnType.mode == "auto" || JSON.stringify(checkConnType) == "{}") {
132
- let srvEls = element.packet.answers.find(el => el.type == "SRV");
133
- let txtEls = element.packet.answers.find(el => el.type == "TXT");
137
+ //need remove to REGEXP!
138
+ //let mdnsId = element.fqdn.split("-")[1].split(".")[0];
139
+ let srvEls = element.packet.answers.find(el => el.type == "SRV") || element.packet.additionals.find(el => el.type == "SRV");
140
+ let txtEls = element.packet.answers.find(el => el.type == "TXT") || element.packet.additionals.find(el => el.type == "TXT");
134
141
  if (typeof(txtEls) !== 'undefined' ) {
135
142
  if (txtEls.rdata.deviceId == device.id) {
136
143
  device.address = element.address;
137
144
  device.port = element.service.port;
138
- device.host = srvEls.rdata.target;
145
+ try {
146
+ device.host = srvEls.rdata.target;
147
+ } catch(e) {
148
+ debugMessage(`Error searching hostname in mDNS answer`)
149
+ }
150
+
139
151
  }
140
152
  }
141
153
  }
@@ -200,8 +212,8 @@ module.exports = function(RED) {
200
212
  function connect(device) {
201
213
  //connect only if !device.ws
202
214
  //debugMessage(`device.ws = ${JSON.stringify(device.ws)}`);
203
- if (device.connection == true || typeof(device.connection) == "undefined") {
204
- debugMessage(`Connecting to device ${device.id}. ws is ${JSON.stringify(device.ws)}`)
215
+ if ( (device.connection == true || typeof(device.connection) == "undefined") && node.listenerCount(`statusUpdate_${device.id}`) > 0 ) {
216
+ debugMessage(`Connecting to device ${device.id}. ws is ${JSON.stringify(device.ws)}. Listeners: ` + node.listenerCount(`statusUpdate_${device.id}`));
205
217
  if (!device.ws) {
206
218
  debugMessage('recieving conversation token...');
207
219
  getLocalToken(device)
@@ -246,7 +258,7 @@ module.exports = function(RED) {
246
258
  //}
247
259
  }
248
260
  } else {
249
- debugMessage(`${device.id} connection is disabled by settings in manager node ${device.manager}`)
261
+ debugMessage(`${device.id} connection is disabled by settings in manager node ${device.manager} or you have not use any node for this station`)
250
262
  statusUpdate({"color": "red", "text": "disconnected"}, device);
251
263
  device.timer = setTimeout(connect, 60000, device);
252
264
 
@@ -275,16 +287,19 @@ module.exports = function(RED) {
275
287
  device.watchDog = setTimeout(() => device.ws.close(), 10000);
276
288
  device.pingInterval = setInterval(onPing,300,device);
277
289
  clearTimeout(device.timer);
290
+ debugMessage(`readyState: ${device.ws.readyState}`)
278
291
  });
279
292
  device.ws.on('message', function incoming(data) {
280
293
  //debugMessage(`${device.id}: ${JSON.stringify(data)}`);
281
294
  let dataRecieved = JSON.parse(data);
282
295
  device.lastState = dataRecieved.state;
296
+ device.fullMessage = JSON.stringify(dataRecieved);
283
297
  //debugMessage(checkSheduler(device, JSON.parse(data).sentTime));
284
298
  node.emit(`message_${device.id}`, device.lastState);
285
299
  if (device.lastState.aliceState == 'LISTENING' && device.waitForListening) {node.emit(`stopListening`, device)}
286
300
  if (device.lastState.aliceState == 'LISTENING' && device.playAfterTTS) {node.emit('startPlay', device)}
287
301
  if (device.lastState.aliceState == 'LISTENING' && device.waitForIdle) {node.emit('setVolume', device)}
302
+ //if (device.lastState.aliceState != 'SPEAKING' && device.ttsBuffer.length > 0) {node.emit('nextTts', device)}
288
303
  // if (device.parameters.hasOwnProperty(sheduler)) {
289
304
  // let resultSheduler = checkSheduler(device, dataRecieved.sentTime)
290
305
  // device.canPlay = resultSheduler[0]
@@ -313,6 +328,7 @@ module.exports = function(RED) {
313
328
  //device.ws.on('ping', function);
314
329
  device.ws.on('close', function close(code, reason){
315
330
  statusUpdate({"color": "red", "text": "disconnected"}, device);
331
+ //debugMessage(`readyState: ${device.ws.readyState}`)
316
332
  device.lastState = {};
317
333
  clearTimeout(device.watchDog);
318
334
  switch(code) {
@@ -499,10 +515,11 @@ module.exports = function(RED) {
499
515
  return result;
500
516
  break;
501
517
  case 'homekit':
502
- debugMessage('HAP: ' + JSON.stringify(message.hap.context) + ' PL: ' + JSON.stringify(message.payload) );
503
- if (message.hap.context != undefined) {
518
+ debugMessage('HAP: ' + JSON.stringify(message) + ' PL: ' + JSON.stringify(message.payload) );
519
+ if ("session" in message.hap) {
504
520
  switch(JSON.stringify(message.payload)){
505
521
  case '{"TargetMediaState":1}':
522
+ return messageConstructor('command', {'payload': 'stop'})
506
523
  case '{"Active":0}':
507
524
  return messageConstructor('command', {'payload': 'stop'})
508
525
  case '{"TargetMediaState":0}':
@@ -758,6 +775,12 @@ module.exports = function(RED) {
758
775
  res.json({"devices": activeList});
759
776
  });
760
777
  });
778
+
779
+ node.on('refreshHttpDNS', function(dnsList) {
780
+ RED.httpAdmin.get("/mdns/"+node.id, RED.auth.needsPermission('yandex-login.read'), function(req,res) {
781
+ res.json({"SearchResult": dnsList});
782
+ });
783
+ });
761
784
  node.on('close', onClose)
762
785
 
763
786
 
@@ -772,7 +795,7 @@ module.exports = function(RED) {
772
795
  let device = searchDeviceByID(id);
773
796
  if (device) {
774
797
 
775
- res.json({"id": device.id,"name": device.name, "platform": device.platform, "address": device.address, "port": device.port, "manager": device.manager, "ws": device.ws, "parameters": device.parameters});
798
+ res.json({"id": device.id,"name": device.name, "platform": device.platform, "address": device.address, "port": device.port, "manager": device.manager, "ws": device.ws, "parameters": device.parameters, "fullMessage": device.fullMessage });
776
799
  } else {
777
800
  res.json({"error": 'no device found'});
778
801
  }
@@ -780,8 +803,9 @@ module.exports = function(RED) {
780
803
 
781
804
  // main init
782
805
  if (typeof(node.token) !== 'undefined') {
806
+ debugMessage(`Starting server with id ${node.id}`)
783
807
  getDevices(node.token);
784
- node.interval = setInterval(getDevices, 300000, node.token);
808
+ node.interval = setInterval(getDevices, 60000, node.token);
785
809
  }
786
810
 
787
811
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "node-red-contrib-yandex-station-management",
3
- "version": "0.2.7",
3
+ "version": "0.3.3",
4
4
  "description": "Local management of YandexStation using API on websockets",
5
5
  "main": "index.js",
6
6
  "scripts": {