UNPKG

node-red-contrib-alexa-remote2-applestrudel

Version:
1,310 lines (1,221 loc) 62.4 kB
<script type="text/x-red" data-template-name="alexa-remote-routine"> <div class="form-row"> <label for="node-input-name"><i class="icon-tag" style="width: 14px; text-align: center"></i> Name</label> <input type="text" id="node-input-name" placeholder="Optional"> </div> <div class="form-row"> <label for="node-input-account"><i class="fa fa-amazon" style="width: 14px; text-align: center"></i> Account</label> <input id="node-input-account"> </div> <div id="node-input-routineNode_div" /> </script> <script type="text/x-red" data-help-name="alexa-remote-routine"> <style> table, th, td { border-collapse: collapse; border: 1px solid rgb(204, 204, 204); padding: 4px 8px; } </style> <p>Emulates Alexa Routine behaviour.</p> <hr> <h3><strong>Info</strong></h3> <ul> <li> <p>Echo devices can be referenced by id or name (not case sensitive)</p> </li> <li> <p>Announcement and SSML speak options will speak to all devices if you don't specify any device (does not work with <em>Speak At Volume</em>)</p> </li> <li> <p><strong>Speak At Volume</strong> or <strong>Volume</strong> with the <em>Add</em> mode can only change the volume if the echo has recently been active playing music!</p> </li> <li> <p><strong>Text Command</strong> allows for sending anything you would otherwise say to Alexa (more details <a href="https://github.com/thorsten-gehrig/alexa-remote-control">here</a>)</p> </li> <li> <p>With the <strong>Custom</strong> option, you can feed in a routine node as js object for completely dynamic routines. The objects can look like this:</p> <ul> <li><code>{ type: 'speak', payload: { type: 'regular', text: 'Hello!', devices: ['My Echo']}</code></li> <li> <code>{ type: 'speakAtVolume', payload: { type: 'regular', text: 'Hello!', volume: 50 devices: ['My Echo']}</code> <ul> <li>type: <code>regular</code>, <code>ssml</code>, <code>announcement</code></li> <li>devices: string or array, can be falsy to send to all devices (only for type speak with announcement or ssml type)</li> </ul> </li> <li><code>{ type: 'stop', payload: { devices: ['My Echo']}</code></li> <li><code>{ type: 'stop', payload: { devices: ['My Echo']}</code></li> <li><code>{ type: 'prompt', payload: { type: 'goodMorning', devices: ['My Echo']}</code> <ul> <li>prompt: <code>goodMorning</code>, <code>weather</code>, <code>traffic</code>, <code>flashBriefing</code>, <code>singSong</code>, <code>joke</code>, <code>tellStory</code>, <code>calendarToday</code>, <code>calendarTomorrow</code>, <code>calendarNext</code>, <code>funFact</code>, <code>cleanUp</code>, <code>imHome</code></li> </ul> </li> <li><code>{ type: 'phrase', payload: { category: 'confirmations', devices: ['My Echo']}</code> <ul> <li>category: <code>birthday</code>, <code>compliments</code>, <code>confirmations</code>, <code>goodbye</code>, <code>goodmorning</code>, <code>goodnight</code>, <code>iamhome</code></li> </ul> </li> <li><code>{ type: 'volume', payload: { value: 50, devices: ['My Echo']}</code> <ul> <li>value 0..100</li> </ul> </li> <li> <code>{ type: 'music', payload: { provider: 'AMAZON_MUSIC', search: '', device: 'My Echo', duration: 300}</code> <ul> <li>provider: <code>AMAZON_MUSIC</code>, <code>TUNEIN</code>, <code>CLOUDPLAYER</code>, <code>SPOTIFY</code> </li> <li>duration is optional</li> </ul> </li> <li><code>{ type: 'wait', payload: { time: 3 }</code> <ul> <li>time in seconds</li> </ul> </li> <li><code>{ type: 'smarthome', payload: { entity: 'Lamp', action: 'setColor', value: '#FF00FF' }</code> <em>(seconds)</em> <ul> <li>entity can be an id or name (case insensitive)</li> <li>action: <code>turnOn</code>, <code>turnOff</code>, <code>setColor</code>, <code>setColorTemperature</code>, <code>setBrightness</code>, <code>setPercentage</code>, <code>lockAction</code>, <code>setTargetTemperature</code></li> </ul> </li> <li><code>{ type: 'routine', payload: { routine: 'hello' }</code> <ul> <li>routine can be an id or utterance (case insensitive)</li> </ul> </li> <li><code>{ type: 'pushNotification', payload: { text: 'Hello from Node-RED!', title: 'Node-RED' }</code></li> <li><code>{ type: 'node', payload: { type: 'serial', children: [ { type: 'speak', payload: {...}}] }</code> <ul> <li>type: <code>serial</code>, <code>parallel</code></li> </ul> </li> </ul> </li> </ul> <hr> <h3><strong>References</strong></h3> <ul> <li><a href="https://npmjs.com/package/node-red-contrib-alexa-remote2">npm</a> - the nodes npm repository</li> <li><a href="https://github.com/586837r/node-red-contrib-alexa-remote2">GitHub</a> - the nodes GitHub repository</li> </ul> </script> <style> #dialog-form .red-ui-editableList-item-sortable.red-ui-editableList-item-removable:last-child { border-bottom: none !important; } #dialog-form .red-ui-editableList-border.red-ui-editableList-container { overflow-y: auto !important; } .alexaRemote-arRow { flex: 1 !important; display: flex !important; margin-bottom: 12px !important; } .alexaRemote-arRow > *:first-child { min-width: 150px !important; flex: 1 !important; } .alexaRemote-arRow_label { flex: 1 !important; display: flex !important; align-items: center !important; justify-content: flex-end !important; white-space: nowrap !important; overflow: hidden !important; text-overflow: ellipsis !important; } .alexaRemote-arSelect, .alexaRemote-arBaseOrSelect > select { font-family: FontAwesome, "Helvetica Neue", Helvetica, Arial, sans-serif !important; } .alexaRemote-arBaseOrSelect { display: flex !important; } .alexaRemote-arBaseOrSelect > select, .alexaRemote-arSelect { flex: 1 !important; width: 100% !important; } .alexaRemote-arTypedInputOrInputList .hidden { display: none !important; } .alexaRemote-arTypedInput .red-ui-typedInput-container { width: 100% !important; } </style> <script type="text/javascript"> RED.nodes.registerType('alexa-remote-routine', { category: 'alexa', color: '#6fbad8', defaults: { name: { value: '' }, account: { value: '', type: 'alexa-remote-account', required: true }, routineNode: { value: { type: 'speak', payload: undefined } }, }, inputs: 1, outputs: 1, icon: 'alexa-remote-icon.png', paletteLabel: 'Alexa Routine', label: function () { function keyToLabel(str) { str = String(str); const match = str.match(/(?:[A-Z]?[a-z]+)|[A-Z]|[0-9]+/g); if (match) str = match.map(s => s.slice(0, 1).toUpperCase() + s.slice(1)).join(' '); return str; } if(this.name) return this.name; const type = this.routineNode && this.routineNode.type; if(type === 'routine') return `Routine Execute`; if(type) return `Routine ${keyToLabel(type)}`; return `Routine`; }, labelStyle: function () { return this.name ? 'node_label_italic' : ''; }, oneditprepare: function () { console.log('loaded', this.routineNode); class EventEmitter { constructor() { this.off(); } once(type, callback) { let set = this.onceListeners.get(type); if (!set) { set = new Set(); this.onceListeners.set(type, set); } set.add(callback); } on(type, callback) { let set = this.listeners.get(type); if (!set) { set = new Set(); this.listeners.set(type, set); } set.add(callback); } // listens until element has left the document listen(type, element, callback) { if (typeof type !== 'string' || !(element instanceof $ || element instanceof Node) || typeof callback !== 'function') throw new Error('dumbass'); let map = this.elementListeners.get(type); if (!map) { map = new Map(); this.elementListeners.set(type, map); } map.set(callback, element instanceof $ ? element.get(0) : element); } off(type, callback) { if (arguments.length === 0) { this.listeners = new Map(); // { type => Set { callback } } this.onceListeners = new Map(); // { type => Set { callback } } this.elementListeners = new Map(); // { type => Map { callback => element } } } if (arguments.length === 1) { this.listeners.delete(type); this.onceListeners.delete(type); return; } let set, map; if (set = this.listeners.get(type)) { set.delete(callback); if (set.length === 0) this.listeners.delete(type); } if (set = this.onceListeners.get(type)) { set.delete(callback); if (set.length === 0) this.onceListeners.delete(type); } if (map = this.elementListeners.get(type)) { map.delete(callback); if (map.length === 0) this.elementListeners.delete(type); } } emit(type) { const args = [...arguments].slice(1); let set, map; if (set = this.listeners.get(type)) { for (const cb of set) cb(...args); } if (set = this.onceListeners.get(type)) { this.onceListeners.delete(type); for (const cb of set) cb(...args); } if (map = this.elementListeners.get(type)) { for (const [cb, e] of map) document.contains(e) ? cb(...args) : map.delete(cb); } } } function keyToLabel(str) { str = String(str); const match = str.match(/(?:[A-Z]?[a-z]+)|[A-Z]|[0-9]+/g); if (match) str = match.map(s => s.slice(0, 1).toUpperCase() + s.slice(1)).join(' '); return str; } function template(source, templ, depth = Infinity, fillArray = false) { function isObject(x) { return typeof x == 'object' && x !== null && !Array.isArray(x) } if (templ === undefined) { return source; } // are they different types? if (typeof templ !== typeof source || Array.isArray(templ) !== Array.isArray(source) || isObject(templ) !== isObject(source)) { return templ; } if (depth !== 0) { if (Array.isArray(templ)) { if (templ.length === 0) { return source; } const result = []; for (let i = 0; i < source.length; i++) { result[i] = template(source[i], templ[0], depth - 1); } return result; } if (isObject(templ)) { const result = {}; for (const k of Object.keys(templ)) { result[k] = template(source[k], templ[k], depth - 1); } return result; } } return source; } function inDocument(element) { return document.contains(element instanceof $ ? element[0] : element); } function zip(a, b) { const arr = []; for (let i = 0; i < a.length; i++) { arr[i] = [a[i], b[i]]; } return arr; } function expand(a) { return zip(a, a); } function clean(obj) { return Object.entries(obj).filter(([k, v]) => v || (typeof v === 'number')).reduce((o, [k, v]) => (o[k] = v, o), {}); } $.widget('alexaRemote.arInput', { _create: function () { this.element.attr('type', 'text').addClass('alexaRemote-arInput'); }, value: function (value) { if (arguments.length === 0) return this.element.val(); this.element.val(value); } }); $.widget('alexaRemote.arSelect', { _create: function () { this.element.addClass('alexaRemote-arSelect'); this.selectOptions([]); }, selectOptions: function (entries) { if (arguments.length === 0) { return this.element.children().toArray().map(o => [$(o).val(), $(o).html()]); } this.element.empty(); for (const entry of entries) { const [v, t] = Array.isArray(entry) ? entry : [entry, keyToLabel(entry)]; $('<option>').val(v).html(t).appendTo(this.element); } }, value: function (value) { if (arguments.length === 0) { return this.element.val(); } this.element.val(value); } }); $.widget('alexaRemote.arTypedInput', { options: { types: [], placeholder: '' }, _create: function () { this.input = $('<input>').appendTo(this.element) .typedInput({ types: this.options.types.concat(['msg', 'flow', 'global', 'jsonata', 'env']) }) .typedInput('width', '100%'); if (this.options.placeholder) { this.element.find('.red-ui-typedInput-input > input') .attr('placeholder', this.options.placeholder); } this.element.addClass('alexaRemote-arTypedInput'); }, value: function (value) { if (arguments.length === 0) { return this.input.typedInput('value'); } this.input.typedInput('value', value); }, type: function (type) { if (arguments.length === 0) { return this.input.typedInput('type'); } this.input.typedInput('type', type); }, types: function (types) { this.input.typedInput('types', types.concat(['msg', 'flow', 'global', 'jsonata', 'env'])); }, refresh: function () { // fix display bug const type = this.type(); this.type(type === 'msg' ? 'flow' : 'msg'); this.type(type); }, data: function (data) { if (arguments.length === 0) { return { type: this.type(), value: this.value() } } this.type(data.type); this.value(data.value); } }); $.widget('alexaRemote.arBaseOrSelect', { options: { button: false, }, _setup: function (element) { this.element.addClass('alexaRemote-arBaseOrSelect'); this.notSelect = element; this.select = $('<select>').css({ width: 50 }); this._on(this.select, { 'change': function (event) { event.stopPropagation(); this._inputValue(this.select.val()); this._change(); } }); this._on(this.notSelect, { 'change': function (event) { event.stopPropagation(); this._change(); } }); if (this.options.button) { this.button = $('<a>').addClass('editor-button'); this.element.css({ display: 'flex' }).append( this.notSelect.css({ flex: '1' }), this.select.css({ flex: '1' }), $('<div>').css({ width: 8 }), this.button.append($('<i>').attr('class', 'fa fa-list')) ); this.button.click(() => { const active = this.selectActive(); this.selectActive(!active); }); } else { this.element.append(this.notSelect, this.select); } this.selectActive(false); this.selectOptions([]); }, _inputValue: function (value) { throw 'abstract widget you dingus'; }, _selectValue: function (value) { if (arguments.length === 0) return this.select.val(); const found = Array.from(this.select.get(0).options).find(o => o.value === value); this.select.val(found ? value : ''); }, selectActive: function (active) { if (arguments.length === 0) { return this.select.is(':visible'); } if (active) { this.notSelect.hide(); this.select.show(); } else { this.select.hide(); this.notSelect.show(); } }, selectOptions: function (optionsArrayOrObj) { if (arguments.length === 0) { throw new Error('NYI'); } this.select.empty(); this.select.append('<option hidden disabled selected value>???</option>'); // allows [['myval', 'My Label'], 'myValAndLabel_special'] => [.., ['myValAndLabel_special', 'My Val And Label Special']] if(Array.isArray(optionsArrayOrObj)) { for(const option of optionsArrayOrObj) { const [value, label] = Array.isArray(option) ? option : [option, keyToLabel(option)]; $('<option>').val(value).html(label).appendTo(this.select); } } else { for(const [groupName, options] of Object.entries(optionsArrayOrObj)) { const group = $('<optgroup>').attr('label', groupName).appendTo(this.select); for(const option of options) { const [value, label] = Array.isArray(option) ? option : [option, keyToLabel(option)]; $('<option>').val(value).html(label).appendTo(group); } } } this._selectValue(this._inputValue()); }, selectIndex: function (index) { if (arguments.length === 0) return this.select.get(0).selectedIndex - 1; const option = this.select.get(0).options[index + 1]; this.selectActive(true); this.value(option && option.value || ''); }, selectSomething: function () { if (this.selectIndex() < 0) this.selectIndex(0); }, choose: function (some = true) { const value = this.value(); if (some && !value) return this.selectSomething(); const select = this.select.get(0); const found = value && Array.from(select.options).find(o => o.value === value); this.selectActive(!!found); }, value: function (value) { if (arguments.length === 0) return this._inputValue(); this._selectValue(value); this._inputValue(value); this._change(); }, _change: function () { this.element.trigger('change', this.value()); } }) $.widget('alexaRemote.arInputOrSelect', $.alexaRemote.arBaseOrSelect, { options: { placeholder: '' }, _create: function () { this.element.addClass('alexaRemote-arInputOrSelect'); this.input = $('<input>').css({ width: '100%' }).attr('type', 'text'); if (this.options.placeholder) this.input.attr('placeholder', this.options.placeholder); this._setup(this.input); }, _inputValue: function (value) { if (arguments.length === 0) return this.input.val(); this.input.val(value); } }); $.widget('alexaRemote.arTypedInputOrSelect', $.alexaRemote.arBaseOrSelect, { options: { optionType: 'str', placeholder: '', }, _create: function () { this.element.addClass('alexaRemote-arTypedInputOrSelect'); this.input = $('<div>').arTypedInput({ placeholder: this.options.placeholder, types: [this.options.optionType] }); if (typeof this.options.optionType === 'object') this.options.optionType = this.options.optionType.value; this._setup(this.input); }, _change: function () { this.element.trigger('change', this.data()); }, _inputValue: function (value) { if (arguments.length === 0) return this.input.arTypedInput('value'); this.input.arTypedInput('value', value); }, selectActive: function (active) { if (arguments.length === 0) { return this._super(...arguments); } this._super(...arguments); if (active) this.type(this.options.optionType); this.refresh(); }, selectActiveMaybe: function (active) { if (active && this.type() !== this.options.optionType) return; return this.selectActive(...arguments); }, choose: function () { const type = this.input.arTypedInput('type'); type === this.options.optionType ? this._super(...arguments) : this.selectActive(false); }, type: function (type) { if (arguments.length === 0) { return this.input.arTypedInput('type'); } this.input.arTypedInput('type', type); if (this.type() !== this.options.optionType) this.selectActive(false); }, types(types) { this.input.arTypedInput('types', types.concat(['msg', 'flow', 'global', 'jsonata', 'env'])); }, refresh: function () { this.input.arTypedInput('refresh'); }, data: function (property) { if (arguments.length === 0) { return { type: this.type(), value: this.value() } } this.type(property.type); this.value(property.value); } }); $.widget('alexaRemote.arInputList', { options: { add: function (item, index) { return undefined; }, header: undefined }, _create: function () { this.list = $('<ol>').appendTo(this.element).editableList({ removable: true, sortable: true, scrollOnAdd: true, header: this.options.header && $('<div>').css({ display: 'flex', paddingTop: 4, paddingLeft: 22 + 5, paddingRight: 28 + 5 }).append(this.options.header), addItem: (row, index, data) => { row.css({ display: 'flex' }); const callback = this.options.add.bind(row)(data, index); row.data('callback', callback); } }); this.element.css({ flex: '1', display: 'flex' }); this.element.find('.red-ui-editableList').css({ flex: '1', display: 'flex', flexDirection: 'column' }); this.element.find('.red-ui-editableList-border').css({ flex: '1', display: 'flex', flexDirection: 'column' }); this.element.find('.red-ui-editableList-container').css({ flex: '1' }); this.element.find('.red-ui-editableList-addButton').css({ alignSelf: 'flex-start' }) }, add: function (item) { this.list.editableList('addItem', item) }, value: function (items) { if (arguments.length === 0) { return this.list.editableList('items').toArray().map(row => { const callback = row.data('callback'); return callback && callback(); }); } this.list.empty(); for (const item of items) { this.list.editableList('addItem', item); } } }); $.widget('alexaRemote.arInputGroups', { options: { clean: true }, _create: function () { this.element.addClass('alexaRemote-arInputGroups'); this.element.hide(); }, groups: function (groups) { if (arguments.length === 0) return this.groupMap; if (this.groupMap) this.groupValue = this.groupObject = undefined; this.groupMap = groups; this.update(); }, group: function (group) { if (arguments.length === 0) return this.groupKey; if (this.groupKey === group) return; this.groupKey = group; if (this.options.clean) this.groupValue = undefined; this.update(); }, value: function (value) { if (arguments.length === 0) return this.groupCallbacks && this.groupCallbacks.getter() || null; this.groupValue = value; this.update(); }, update: function () { this.element.empty(); if (!this.groupMap) return; this.groupObject = this.groupMap.hasOwnProperty(this.groupKey) ? this.groupMap[this.groupKey] : undefined; if (typeof this.groupObject === 'function') this.groupObject = { create: this.groupObject }; this.groupCallbacks = this.groupObject && this.groupObject.create && this.groupObject.create.bind(this.element)(this.groupValue); if (typeof this.groupCallbacks === 'function') this.groupCallbacks = { getter: this.groupCallbacks }; if (this.groupObject) this.element.show(); else this.element.hide(); this.element.trigger('change', this.groupKey); } }); $.widget('alexaRemote.arTypedInputOrInputList', { _create: function() { this.input = $('<div>').arTypedInput({ types: ['str', 'json'] }).css({ flex: '1' }); this.list = $('<div>').arInputList(this.options); this.button = $('<a>').addClass('editor-button'); this.element.addClass('alexaRemote-arTypedInputOrInputList').css({ display: 'flex'}).append( this.input, this.list, $('<div>').css({ width: 8 }), this.button.css({ alignSelf: 'center' }).append($('<i>').attr('class', 'fa fa-list')), ); this.button.click(() => { this.listActive(!this.listActive()); }); this._listVisible(false); }, _change: function(data) { this.element.trigger('change'); }, _listVisible: function(visible) { if(arguments.length === 0) return !this.list.hasClass('hidden'); if(visible) { this.input.addClass('hidden'); this.list.removeClass('hidden'); } else { this.list.addClass('hidden'); this.input.removeClass('hidden'); this.refresh(); } this.button.css({ marginBottom: visible ? 24 : 0 }); }, listActive: function(active) { if(arguments.length === 0) return this._listVisible(); if(active) { const data = this.input.arTypedInput('data'); let array; if(data.type === 'str') array = [data.value]; if(data.type === 'json') { try { array = JSON.parse(data.value); } catch(err) {}; } this.list.arInputList('value', Array.isArray(array) ? array : [undefined]); } else { const array = this.list.arInputList('value'); const data = array.length <= 1 ? { type: 'str', value: array[0] || '' } : { type: 'json', value: JSON.stringify(array) }; this.input.arTypedInput('data', data); } this._listVisible(active); this._change(); }, data: function(data) { if(arguments.length === 0) { return this.listActive() ? this.list.arInputList('value') : this.input.arTypedInput('data'); } if(data === undefined) { data = [undefined]; } if(Array.isArray(data)) { this.list.arInputList('value', data); this._listVisible(true); } else { this.input.arTypedInput('data', data); this._listVisible(false); } }, refresh: function() { this.input.arTypedInput('refresh'); } }); $.widget('alexaRemote.arTips', { _create: function() { this.element.addClass('form-tips').css({marginBottom: 12, maxWidth: 'unset'}); }, show: function(arg = true) { if(typeof arg === 'string') this.element.html(arg); arg ? this.element.show() : this.element.hide(); }, hide: function(arg = true) { this.show(!arg); } }); function arFormRow(element, label = '', icon = '') { return $('<div>').addClass('form-row arFormRow').append( $('<label>').text(' ' + label).prepend( $('<i>').attr('class', icon).css({ width: 14, textAlign: 'center' }) ), $(document.createTextNode(' ')), $('<div>').css({ width: '70%', display: 'inline-block' }).append( element.css({ width: '100%' }) ) ); } function arRow(element, label, flex = '3') { return $('<div>').addClass('alexaRemote-arRow').append( $('<label>').addClass('alexaRemote-arRow_label').html(label), $('<div>').css({ width: 10, minWidth: 10 }), element.css({ flex: flex }), ); } function arGroup(element) { return $('<div>').css({display: 'flex'}).append( ...arguments ); } function arInput(data = '') { return $('<input>').arInput() .arInput('value', data) .css({ flex: '1' }); } function arSelect(data = '', options = []) { return $('<select>').arSelect() .arSelect('selectOptions', options) .arSelect('value', data) .css({ flex: '1', width: 100 }); } function arTypedInput(data = { type: 'str', value: '' }, types = ['str'], { placeholder = '' } = {}) { return $('<div>') .arTypedInput({ types: types, placeholder: placeholder }) .arTypedInput('data', data) .css({ flex: '1' }); } function arInputList(data = [], add = () => { }, { header } = {}) { return $('<div>') .arInputList({ add: add, header: header }) .arInputList('value', data); } function arInputOrSelect(data = '', options = [], { button = false, placeholder = '', choose = true, some = true } = {}) { const div = $('<div>').css({ flex: '1' }) .arInputOrSelect({ button: button, placeholder: placeholder }) .arInputOrSelect('selectOptions', options) .arInputOrSelect('value', data); if (choose) div.arInputOrSelect('choose', some); return div; } function arTypedInputOrSelect(data = { type: 'str', value: '' }, options = [], { button = true, placeholder = '', optionType = 'str', choose = true, some = true } = {}) { const div = $('<div>').css({ flex: '1' }) .arTypedInputOrSelect({ button: button, placeholder: placeholder, optionType: optionType }) .arTypedInputOrSelect('selectOptions', options) .arTypedInputOrSelect('data', data); if (choose) div.arTypedInputOrSelect('choose', some); return div; } function arInputGroups(group, value, groups = {}, clean = true) { return $('<div>') .arInputGroups({ clean: clean }) .arInputGroups('group', group) .arInputGroups('value', value) .arInputGroups('groups', groups); } function arTypedInputOrInputList(data, add, { header } = {}) { return $('<div>').arTypedInputOrInputList({add: add, header: header}) .arTypedInputOrInputList('data', data); } function arTips(text = true) { return $('<div>').arTips().arTips('show', text); } function arRoutineNode(data = { node: 'speak' }, loader){ return $('<div>') .arRoutineNode({loader: loader}) .arRoutineNode('data', data); } $.widget('alexaRemote.arRoutineNode', { options: { loader: null }, _create: function () { const rowDiv = $('<div>').css({display: 'flex', flexDirection: 'row', marginBottom: 12}).appendTo(this.element); const loader = this.options.loader; let channel = {}; // for bottom <-> right communication this.loader = loader; this.channel = channel; const options = [ ['speak', '&#xf04b; Speak'], // play ['speakAtVolume', '&#xf0fe; Speak At Volume'], ['textCommand', '&#xf0e7; Text Command'], // bolt ['wait', '&#xf017; Wait'], // clock-o ['stop', '&#xf04d; Stop'], // stop ['prompt', '&#xf27a; Prompt'], // commenting ['phrase', '&#xf075; Phrase'], // comment ['sound', '&#xf001; Sound'], // music ['volume', '&#xf028; Volume'], // volume-up ['music', '&#xf001; Music'], // music ['smarthome', '&#xf015; Smarthome'], // home ['skill', '&#xf12e; Launch Skill'], // puzzle-piece ['routine', '&#xf0e7; Execute Routine'], // bolt ['pushNotification', '&#xf10b; Push Notification'], // mobile ['node', '&#xf126; Node'], // code-fork ['custom', '&#xf061; Custom'], // arrow-right ]; const common = { input: {}, right: { group: {}, }, bottom: { group: {}, row: {} } }; common.input.device = function(data, all) { data = template(data, { type: 'str', value: '' }); const input = arTypedInputOrSelect(data); const updateInput = () => { input.arTypedInputOrSelect('selectOptions', all ? [['ALEXA_ALL_DSN', '&#xf0ac; All Devices']].concat(loader.devices) : loader.devices); if(loader.success) { input.arTypedInputOrSelect('choose', true); } else { input.arTypedInputOrSelect('selectActive', false); } } loader.listen('change', input, updateInput); updateInput(); return input; } common.input.deviceList = function(data, all) { return arTypedInputOrInputList(data, function(data) { data = template(data, ''); const input = arInputOrSelect(data).appendTo(this); const updateInput = () => { input.arInputOrSelect('selectOptions', all ? [['ALEXA_ALL_DSN', '&#xf0ac; All Devices']].concat(loader.devices) : loader.devices); input.arInputOrSelect('selectActive', loader.success); if (!input.arInputOrSelect('value') && loader.success) input.arInputOrSelect('selectSomething'); } loader.listen('change', input, updateInput); updateInput(); return () => input.arInputOrSelect('value'); }); } common.right.group.device = function(all) { return function(data) { data = template(data, { device: undefined }); const input = common.input.device(data.device, all).appendTo(this); return () => ({ device: input.arTypedInputOrSelect('data') }); } } common.bottom.row.deviceList = function(data, all) { const input = common.input.deviceList(data, all); const row = arRow(input, 'Devices'); const updateRow = () => { // to center it to the list (without add button); const active = input.arTypedInputOrInputList('listActive') row.children('label').css({ paddingBottom: active ? 24 : 0 }); } input.on('change', updateRow); updateRow(); return [input, row]; } common.bottom.group.lockAction = function () { return function (data) { data = template(data, { value: { type: 'str', value: 'locked' } }); const value = arTypedInputOrSelect(data.value, ['locked', 'unlocked', 'jammed']); const valueRow = arRow(value, 'Lock State').appendTo(this); return () => ({ value: value.arTypedInputOrSelect('data') }); } } common.bottom.group.device = function(all) { return function(data) { data = template(data, { device: undefined }); const input = common.input.device(data.device, all); arRow(input, 'Device').appendTo(this); return () => ({ device: input.arTypedInputOrSelect('data') }); } } common.bottom.group.deviceList = function(all) { return function(data) { data = template(data, { devices: undefined }); const [devices, devicesRow] = common.bottom.row.deviceList(data.devices, all); devicesRow.appendTo(this); return () => ({ devices: devices.arTypedInputOrInputList('data') }); } } common.bottom.group.number = function(label) { return function(data) { data = template(data, { value: { type: 'num', value: '50' } }); const value = arTypedInput(data.value, ['num']); arRow(value, label).appendTo(this); return () => ({ value: value.arTypedInput('data') }); } } common.bottom.group.color = function (labelHtml, loaderMapProperty, loaderListProperty) { return function (data) { data = template(data, { value: { type: 'str', value: '' } }); const value = arTypedInputOrSelect(data.value); const row = arRow(value, labelHtml).appendTo(this); const label = row.children('label'); const separator = $('<div>').css({ width: 8 }).hide(); const preview = $('<div>').css({ width: 34, height: 34, border: '1px solid #ccc', borderRadius: 4, boxSizing: 'border-box' }).hide(); const wrap = $('<div>').css({ flex: '1', display: 'flex' }).insertAfter(label).append(label, separator, preview); const updatePreview = () => { const type = value.arTypedInputOrSelect('type'); if (type !== 'str') return; const name = value.arTypedInputOrSelect('value'); const color = loader[loaderMapProperty].get(name); if (!color) { preview.add(separator).hide(); return; } preview.add(separator).show(); preview.css({ background: color }); } const updateValue = () => { value.arTypedInputOrSelect('selectOptions', loader.smarthome[loaderListProperty]).arTypedInputOrSelect('choose'); } value.on('change', updatePreview); loader.listen('change', value, updateValue); updateValue(); updatePreview(); return () => ({ value: value.arTypedInputOrSelect('data') }); } } const groups = { right: {}, bottom: {} }; groups.right.speak = function(data) { data = template(data, { type: 'regular' }); const type = arSelect(data.type, ['regular', ['ssml', 'SSML'], 'announcement']).appendTo(this); channel.type = type; return () => ({ type: type.arSelect('value') }); } groups.right.speakAtVolume = groups.right.speak; groups.right.textCommand = function(data) { data = template(data, { text: { type: 'str', value: 'Hello from Alexa Remote!'}}); const input = arTypedInput(data.text, ['str']).appendTo(this); return () => ({ text: input.arTypedInput('data') }); } groups.right.wait = function(data) { data = template(data, { time: { type: 'num', value: '1' }}); const type = arTypedInput(data.time, ['num']).appendTo(this); $('<label>').text('seconds').css({display: 'flex', alignItems: 'center', justifyContent: 'center', padding: '0px 15px'}).appendTo(this); return () => ({ time: type.arTypedInput('data') }); } groups.right.volume = function(data) { data = template(data, { value: { type: 'str', value: '50' } }); const input = arTypedInput(data.value, ['num']).appendTo(this); return () => ({ value: input.arTypedInput('data') }); } groups.right.music = common.right.group.device(false); groups.right.prompt = function(data) { data = template(data, { type: { type: 'str', value: 'goodMorning' }}); const select = arTypedInputOrSelect(data.type, [ ['goodMorning', 'Good Morning'], ['goodNight', 'Good Night'], ['weather', 'Weather'], ['traffic', 'Traffic'], ['flashBriefing', 'News Flash Briefing'], ['singSong', 'Sing a Song'], ['joke', 'Tell a Joke'], ['tellStory', 'Tell me a Story'], ['calendarToday', `Today's Calendar`], ['calendarTomorrow', `Tomorrow's Calendar`], ['calendarNext', `Next Event`], ['funFact', `Fun Fact`], ['cleanUp', `Clean Up`], ['imHome', `I'm Home`], ]).appendTo(this); return () => ({ type: select.arTypedInputOrSelect('data') }); } groups.right.phrase = function(data) { data = template(data, { category: { type: 'str', value: 'confirmations' }}); const select = arTypedInputOrSelect(data.category, [ ['birthday', `Birthday`], ['compliments', `Compliments`], ['confirmations', `Confirmations`], ['goodbye', `Goodbye`], ['goodmorning', `Good Morning`], ['goodnight', `Good Night`], ['iamhome', `I'm Home`], ]).appendTo(this); return () => ({ category: select.arTypedInputOrSelect('data') }); } groups.right.sound = function(data) { data = template(data, { sound: { type: 'str', value: 'amzn_sfx_doorbell_01' }}); const select = arTypedInputOrSelect(data.sound, { 'Animals': [ // paw ['amzn_sfx_cat_meow_1x_01', 'Cat meow'], ['amzn_sfx_dog_med_bark_1x_02', 'Dog bark'], ['amzn_sfx_lion_roar_02', 'Lion roar'], ['amzn_sfx_rooster_crow_01', 'Rooster crow'], ['amzn_sfx_wolf_howl_02', 'Wolf howl'], ], 'Bells and Buzzers': [ ['bell_02', 'Bells howl'], ['buzzers_pistols_01', 'Buzzer'], ['amzn_sfx_church_bell_1x_02', 'Church bell'], ['amzn_sfx_doorbell_01', 'Doorbell 1'], ['amzn_sfx_doorbell_chime_01', 'Doorbell 2'], ['amzn_sfx_doorbell_chime_02', 'Doorbell 3'], ], 'Crowds': [ ['amzn_sfx_crowd_applause_01', 'Crowd applause'], ['amzn_sfx_large_crowd_cheer_01', 'Crowd cheers'], ], 'Festive Season': [ ['christmas_05', 'Christmas bells'], ['horror_10', 'Halloween creepy door'], ], 'Miscellaneous': [ ['air_horn_03', 'Air horn'], ['boing_01', 'Boing 1'], ['boing_03', 'Boing 2'], ['camera_01', 'Camera'], ['squeaky_12', 'Squeaky door'], ['clock_01', 'Ticking clock'], ['amzn_sfx_trumpet_bugle_04', 'Trumpet'], ], 'Sci-fi': [ ['futuristic_10', 'Aircraft'], ['amzn_sfx_scifi_engines_on_02', 'Engines on'], ['amzn_sfx_scifi_alarm_04', 'Red alert'], ['amzn_sfx_scifi_sheilds_up_01', 'Shields up'], ['amzn_sfx_scifi_alarm_01', 'Sirens'], ['zap_01', 'Zap'], ], }).appendTo(this); return () => ({ sound: select.arTypedInputOrSelect('data') }); } groups.right.smarthome = function(data) { data = template(data, {entity: '' }); const entity = arInputOrSelect(data.entity).appendTo(this); channel.entity = entity; const updateEntity = () => { const entities = loader.smarthome.entitiesWithActions; entity.arInputOrSelect('selectOptions', entities); entity.arInputOrSelect('selectActive', loader.success); if (!entity.arInputOrSelect('value') && loader.success) entity.arInputOrSelect('selectSomething'); } loader.listen('change', entity, updateEntity); updateEntity(); return () => ({ entity: entity.arInputOrSelect('value') }); } groups.right.routine = function(data) { data = template(data, { routine: { type: 'str', value: ''} }); const routine = arTypedInputOrSelect(data.routine).appendTo(this); const updateRoutine = () => { routine.arTypedInputOrSelect('selectOptions', loader.routines); routine.arTypedInputOrSelect('selectActiveMaybe', loader.success); if(!routine.arTypedInputOrSelect('value') && loader.success) routine.arTypedInputOrSelect('selectSomething'); } loader.listen('change', routine, updateRoutine); updateRoutine(); return () => ({ routine: routine.arTypedInputOrSelect('data') }); } groups.right.skill = function(data) { data = template(data, { skill: { type: 'str', value: ''} }); const skill = arTypedInputOrSelect(data.skill).appendTo(this); const updateSkill = () => { skill.arTypedInputOrSelect('selectOptions', loader.skills); skill.arTypedInputOrSelect('selectActiveMaybe', loader.success); if(!skill.arTypedInputOrSelect('value') && loader.success) skill.arTypedInputOrSelect('selectSomething'); } loader.listen('change', skill, updateSkill); updateSkill(); return () => ({ skill: skill.arTypedInputOrSelect('data') }); } groups.right.pushNotification = function(data) { data = template(data, { text: { type: 'str', value: 'Hello from Alexa Remote!'}}); const input = arTypedInput(data.text, ['str']).appendTo(this); return () => ({ text: input.arTypedInput('data') }); } groups.right.node = function(data) { data = template(data, { type: 'serial' }); const input = arSelect(data.type, ['serial', 'parallel']).appendTo(this); return () => ({ type: input.arSelect('value') }); } groups.right.custom = function(data) { data = template(data, { type: 'msg', value: 'payload' }); const input = arTypedInput(data, ['json']).appendTo(this); return () => input.arTypedInput('data'); } groups.bottom.speak = function(data) { data = template(data, { text: { type: '', value: '' }, devices: undefined }); const ssml = { value: "ssml", label: "SSML", expand: function () { RED.editor.editMarkdown({ value: this.value().replace(/\t/g, "\n"), complete: (value) => { this.value(value.replace(/\n/g, "\t")); } }) } } const text = arTypedInput(data.text, ['str']); const [devices, devicesRow] = common.bottom.row.deviceList(data.devices); const updateType = () => { const value = channel.type.arSelect('value'); const type = value === 'ssml' ? ssml : 'str' text.arTypedInput('types', [type]); } channel.type.on('change', updateType); updateType(); arRow(text, 'Text').appendTo(this); devicesRow.appendTo(this); return () => ({ text: text.arTypedInput('data'), devices: devices.arTypedInputOrInputList('data') }); }; groups.bottom.speakAtVolume = function(data) { data = template(data, { text: { type: 'str', value: '' }, volume: {type: 'num', value: '50'}, mode: undefined, devices: undefined }); const ssml = { value: "ssml", label: "SSML", expand: function () { RED.editor.editMarkdown({ value: this.value().replace(/\t/g, "\n"), complete: (value) => { this.value(value.replace(/\n/g, "\t")); } }) } } const text = arTypedInput(data.text, ['str']); const [devices, devicesRow] = common.bottom.row.deviceList(data.devices); const volume = arTypedInput(data.volume, ['num']); const separator = $('<div>').css({ width: 10 }); const mode = arSelect(data.mode || 'set', ['set', 'add']).attr('style', 'flex: 1 !important; max-width: 150px !important;'); channel.type.on('change', () => { const value = channel.type.arSelect('value'); const type = value === 'ssml' ? ssml : 'str' text.arTypedInput('types', [type]); }); //arTips('<b>Warning:</b> Only works if the echo has recently been playing music!').appendTo(this); arRow(text, 'Text').appendTo(this); arRow(arGroup(volume, separator, mode), 'Volume').appendTo(this); devicesRow.appendTo(this).children('label'); return () => ({ text: text.arTypedInput('data'), volume: volume.arTypedInput('data'), mode: mode.arSelect('value'), devices: devices.arTypedInputOrInputList('data'), }); } groups.bottom.stop = common.bottom.group.deviceList(true); groups.bottom.prompt = common.bottom.group.deviceList(false); groups.bottom.phrase = common.bottom.group.deviceList(false); groups.bottom.sound = common.bottom.group.deviceList(false); groups.bottom.textCommand = common.bottom.group.deviceList(false); groups.bottom.volume = function(data) { data = template(data, { mode: { type: 'str', value: 'set' }, devices: undefined }); const mode = arTypedInputOrSelect(data.mode || 'set', ['set', 'add']); const [devices, devicesRow] = common.bottom.row.deviceList(data.devices); arRow(mode, 'Mode').appendTo(this); devicesRow.appendTo(this); return () => ({ mode: mode.arTypedInputOrSelect('data'), devices: devices.arTypedInputOrInputList('data') }); }; groups.bottom.music = function(data) { data = template(data, {provider: { type: 'str', value: ''}, search: {type: 'str', value: ''}, duration: { type: 'num', value: ''}}); const provider = arTypedInputOrSelect(data.provider); const search = arTypedInput(data.search); const duration = arTypedInput(data.duration, ['num'], { placeholder: 'Optional' }); this.append( arRow(provider, 'Provider'), arRow(search, 'Search'), arRow(duration, 'Duration'), ); const updateProvider = () => { provider.arTypedInputOrSelect('selectOptions', loader.musicProviders); provider.arTypedInputOrSelect('selectActiveMaybe', loader.success); if (!provider.arTypedInputOrSelect('value') && loader.success) provider.arTypedInputOrSelect('selectSomething'); } loader.listen('change', provider, updateProvider); updateProvider(); return () => ({ provider: provider.arTypedInputOrSelect('data'), search: search.arTypedInput('data'), duration: duration.arTypedInput('data')}); } groups.bottom.smarthome = function(data) { data = template(data, { entity: '', action: '', value: undefined, scale: undefined }); const action = arInputOrSelect(data.action); const parameters = arInputGroups(data.action, data, { setColor: common.bottom.group.color('Color', 'colorNameToHex', 'colorNames'), setColorTemperature: common.bottom.group.color('Color Temp.', 'colorTemperatureNameToHex', 'colorTemperatureNames'), setBrightness: common.bottom.group.number('Brightness'), setPercentage: common.bottom.group.number('Percentage'), setLockState: common.bottom.group.lockAction(), lockAction: common.bottom.group.lockAction(), setTargetTemperature: function (data) { data = template(data, { value: { type: 'num', value: '20' }, scale: { type: 'str', value: 'celsius' } }); const scale = arTypedInputOrSelect(data.scale, ['celsius', 'fahrenheit']); const value = arTypedInput(data.value, ['num']); arRow(scale, 'Scale').appendTo(this); arRow(value, 'Value').appendTo(this); return () => ({ scale: scale.arTypedInputOrSelect('data'), value: value.arTypedInput('data') }); }, }); const updateAction = (user) => { const entityId = channel.entity.arInputOrSelect('value'); const [label, properties, actions] = loader.smarthome