node-red-contrib-alexa-remote2-applestrudel
Version:
node-red nodes for interacting with alexa
1,310 lines (1,221 loc) • 62.4 kB
HTML
<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', ' Speak'], // play
['speakAtVolume', ' Speak At Volume'],
['textCommand', ' Text Command'], // bolt
['wait', ' Wait'], // clock-o
['stop', ' Stop'], // stop
['prompt', ' Prompt'], // commenting
['phrase', ' Phrase'], // comment
['sound', ' Sound'], // music
['volume', ' Volume'], // volume-up
['music', ' Music'], // music
['smarthome', ' Smarthome'], // home
['skill', ' Launch Skill'], // puzzle-piece
['routine', ' Execute Routine'], // bolt
['pushNotification', ' Push Notification'], // mobile
['node', ' Node'], // code-fork
['custom', ' 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', ' 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', ' 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