node-red-contrib-smartnora
Version:
Google Smart Home integration via Smart Nora https://smart-nora.eu/
699 lines (656 loc) • 29.2 kB
HTML
<script type="text/javascript">
RED.nodes.registerType('noraf-media', {
category: 'nora',
color: 'rgb(235, 227, 141)',
icon: 'assistant.png',
defaults: {
devicename: {
value: 'Media device',
required: true,
},
roomhint: {
value: ''
},
name: {
value: ''
},
topic: {
value: ''
},
passthru: {
value: false,
},
nora: {
type: 'noraf-config',
required: true
},
twofactor: {
value: 'off',
},
twofactorpin: {
value: ''
},
filter: {
value: false,
},
deviceType: {
value: 'SPEAKER',
required: true,
},
supportOnOff: {
value: false,
},
supportVolume: {
value: false,
},
volumeCanMuteAndUnmute: {
value: true,
},
volumeLevelStepSize: {},
supportMediaState: {
value: false,
},
supportActivityState: {
value: false,
},
supportPlaybackState: {
value: true,
},
supportTransportControl: {
value: false,
},
transportControlCommands: {
value: ['PAUSE', 'RESUME', 'STOP'],
},
supportInputSelector: {
value: false,
},
language: {
value: 'en',
},
mediaInputs: {
value: [{
v: 'hdmi-1',
n: 'hdmi 1,1st hdmi,TV',
d: true,
}, {
v: 'hdmi-2',
n: 'hdmi 2,2nd hdmi,PlayStation',
d: false,
}],
},
supportAppSelector: {
value: false,
},
appsLanguage: {
value: 'en'
},
mediaApps: {
value: [{
v: 'netflix',
n: 'Netflix',
d: false
}, {
v: 'youtube',
n: 'Youtube',
d: false
}],
},
supportChannel: {
value: false,
},
mediaChannels: {
value: [{
k: 'ch-1',
n: 'CNN,channel 1',
i: '1',
}, {
k: 'ch-2',
n: 'Fox,channel 2',
i: '2',
}],
},
errorifstateunchaged: {
value: false,
},
asyncCmdOnOff: {
value: false,
},
asyncCmdVolume: {
value: false,
},
asyncCmdTransportControl: {
value: false,
},
asyncCmdInputSelector: {
value: false,
},
asyncCmdAppSelector: {
value: false,
},
asyncCmdChannel: {
value: false,
},
outputs: {
value: 2,
},
},
inputs: 1,
outputs: 2,
outputLabels: ["state", "media command", "async command"],
paletteLabel: 'media',
label: function () {
return this.name || this.devicename || 'Media device';
},
oneditprepare: function () {
$('#node-input-twofactor').change(function () {
if ($(this).val() === 'pin') {
$('#node-twofactor-pin').show();
} else {
$('#node-twofactor-pin').hide();
}
});
$('#node-input-supportVolume').change(function () {
$('.volume-trait')[$(this).is(':checked') ? 'show' : 'hide']();
});
$('#node-input-supportMediaState').change(function () {
$('.mediastate-trait')[$(this).is(':checked') ? 'show' : 'hide']();
});
var availableCommands = [
'CAPTION_CONTROL', 'SEEK_RELATIVE', 'SEEK_TO_POSITION', 'SET_REPEAT',
'NEXT', 'PREVIOUS', 'PAUSE', 'RESUME', 'STOP', 'SHUFFLE'
];
var selectedCommands = this.transportControlCommands || [];
for (var command of availableCommands) {
var commandRow = $('<div>').appendTo($(
'.transportcontrol-trait .transportcontrol-options'));
$('<input/>', {
type: 'checkbox',
id: 'input-control-command-' + command,
'data-command': command,
})
.appendTo(commandRow)
.prop('checked', selectedCommands.includes(command));
$('<label/>', {
for: 'input-control-command-' + command,
})
.text(toHummanCase(command))
.appendTo(commandRow);
}
function toHummanCase(command) {
return command.substring(0, 1) + command.substring(1)
.toLowerCase()
.replace(/_(\w)/mg, (_, g) => ' ' + g.toUpperCase());
}
$('#node-input-supportOnOff').change(function () {
$('.onoff-trait')[$(this).is(':checked') ? 'show' : 'hide']();
});
$('#node-input-supportTransportControl').change(function () {
$('.transportcontrol-trait')[$(this).is(':checked') ? 'show' : 'hide']();
});
$('#node-input-supportInputSelector').change(function () {
$('.inputselector-trait')[$(this).is(':checked') ? 'show' : 'hide']();
});
$('#node-input-inputs-container').change(function (ev) {
if (ev.target.classList.contains('node-input-inputs-input-default')) {
$('.node-input-inputs-input-default').not(ev.target).prop('checked', false);
}
});
$('#node-input-inputs-container').css('min-height', '120px').css('min-width', '350px')
.editableList({
addItem: function (container, i, opt) {
var prop = opt;
if (!prop.hasOwnProperty('v')) {
prop = {
v: '',
n: ''
};
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
var row = $('<div/>').appendTo(container);
$('<input/>', {
class: "node-input-inputs-input-value",
type: "text",
placeholder: 'Value',
required: true,
})
.css({
width: "30%",
})
.val(prop.v)
.appendTo(row);
$('<input/>', {
class: "node-input-inputs-input-name",
type: "text",
placeholder: 'Name in language',
required: true,
})
.css({
width: "calc(70% - 30px)",
marginLeft: "5px",
})
.val(prop.n)
.appendTo(row);
$('<input/>', {
class: "node-input-inputs-input-default",
type: "checkbox",
title: "Default",
})
.css("width", "30px")
.appendTo(row)
.prop('checked', !!prop.d);
},
removable: true,
sortable: true
});
if (!this.mediaInputs) {
this.mediaInputs = [];
}
for (const input of this.mediaInputs) {
$("#node-input-inputs-container").editableList('addItem', {
...input
});
}
$('#node-input-supportAppSelector').change(function () {
$('.appselector-trait')[$(this).is(':checked') ? 'show' : 'hide']();
});
$('#node-input-apps-container').change(function (ev) {
if (ev.target.classList.contains('node-input-apps-app-default')) {
$('.node-input-apps-app-default').not(ev.target).prop('checked', false);
}
});
$('#node-input-apps-container').css('min-height', '120px').css('min-width', '350px')
.editableList({
addItem: function (container, i, opt) {
var prop = opt;
if (!prop.hasOwnProperty('v')) {
prop = {
v: '',
n: ''
};
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
var row = $('<div/>').appendTo(container);
$('<input/>', {
class: "node-input-apps-app-value",
type: "text",
placeholder: 'Value',
required: true,
})
.css({
width: "30%",
})
.val(prop.v)
.appendTo(row);
$('<input/>', {
class: "node-input-apps-app-name",
type: "text",
placeholder: 'Name in language',
required: true,
})
.css({
width: "calc(70% - 30px)",
marginLeft: "5px",
})
.val(prop.n)
.appendTo(row);
$('<input/>', {
class: "node-input-apps-app-default",
type: "checkbox",
title: "Default",
})
.css("width", "30px")
.appendTo(row)
.prop('checked', !!prop.d);
},
removable: true,
sortable: true
});
if (!this.mediaApps) {
this.mediaApps = [];
}
for (const app of this.mediaApps) {
$("#node-input-apps-container").editableList('addItem', {
...app
});
}
$('#node-input-supportChannel').change(function () {
$('.channel-trait')[$(this).is(':checked') ? 'show' : 'hide']();
});
$('#node-input-channels-container').css('min-height', '120px').css('min-width', '350px')
.editableList({
addItem: function (container, i, opt) {
var prop = opt;
if (!prop.hasOwnProperty('k')) {
prop = {
k: '',
n: '',
i: '',
};
}
container.css({
overflow: 'hidden',
whiteSpace: 'nowrap'
});
var row = $('<div/>').appendTo(container);
$('<input/>', {
class: "node-input-channels-channel-key",
type: "text",
placeholder: 'Key',
required: true,
})
.css("width", "30%")
.val(prop.k)
.appendTo(row);
$('<input/>', {
class: "node-input-channels-channel-name",
type: "text",
placeholder: 'Name',
required: true,
})
.css({
width: "calc(30%)",
marginLeft: "5px",
})
.val(prop.n)
.appendTo(row);
$('<input/>', {
class: "node-input-channels-channel-number",
type: "text",
placeholder: 'Number',
})
.css({
width: "calc(30%)",
marginLeft: "5px",
})
.val(prop.i)
.appendTo(row);
},
removable: true,
sortable: true
});
if (!this.mediaChannels) {
this.mediaChannels = [];
}
for (const channel of this.mediaChannels) {
$("#node-input-channels-container").editableList('addItem', {
...channel
});
}
},
oneditsave: function () {
this.transportControlCommands = $('.transportcontrol-trait input:checked').toArray()
.map(c => c.getAttribute('data-command'));
var inputs = $("#node-input-inputs-container").editableList('items');
var node = this;
node.mediaInputs = [];
inputs.each(function (i) {
var input = $(this);
var p = {
n: input.find(".node-input-inputs-input-name").val().trim(),
v: input.find(".node-input-inputs-input-value").val().trim(),
d: input.find(".node-input-inputs-input-default").is(':checked'),
};
if (p.n && p.v) {
node.mediaInputs.push(p);
}
});
var apps = $("#node-input-apps-container").editableList('items');
var node = this;
node.mediaApps = [];
apps.each(function (i) {
var app = $(this);
var p = {
n: app.find(".node-input-apps-app-name").val().trim(),
v: app.find(".node-input-apps-app-value").val().trim(),
d: app.find(".node-input-apps-app-default").is(':checked'),
};
if (p.n && p.v) {
node.mediaApps.push(p);
}
});
var channels = $("#node-input-channels-container").editableList('items');
node.mediaChannels = [];
channels.each(function (i) {
var channel = $(this);
var p = {
n: channel.find(".node-input-channels-channel-name").val().trim(),
k: channel.find(".node-input-channels-channel-key").val().trim(),
i: channel.find(".node-input-channels-channel-number").val().trim(),
};
if (p.n && p.k) {
node.mediaChannels.push(p);
}
});
node.outputs = $('.async-cmd:checked').length ? 3 : 2;
},
});
</script>
<script type="text/x-red" data-template-name="noraf-media">
<style>
.trait-props {
margin-left: 5px;
border-left: 1px solid lightgray;
padding-left: 10px;
padding-top: 10px;
margin-top: -15px;
margin-bottom: 5px;
}
.transportcontrol-options {
display: grid;
grid-template-columns: repeat(auto-fit,minmax(120px, 1fr));
}
.transportcontrol-options > div {
display: flex;
flex-direction: row;
}
.transportcontrol-options > div > label {
margin-left: 5px;
}
</style>
<div class="form-row">
<label for="node-input-nora"><i class="fa fa-table"></i> Config</label>
<input type="text" id="node-input-nora">
</div>
<div class="form-row">
<label for="node-input-devicename"><i class="fa fa-i-cursor"></i> Media Name</label>
<input type="text" id="node-input-devicename">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-passthru"><i class="fa fa-arrow-right"></i> If <code>msg</code> arrives on input, pass through to output: </label>
<input type="checkbox" id="node-input-passthru" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-filter"><i class="fa fa-filter"></i> Ignore input messages that don't match the <code>topic</code> value: </label>
<input type="checkbox" id="node-input-filter" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-deviceType"><i class="fa fa-question-sign"></i> Device Type</label>
<select id="node-input-deviceType">
<option value="SPEAKER">Speaker</option>
<option value="AUDIO_VIDEO_RECEIVER">Audio Video Receiver</option>
<option value="REMOTECONTROL">Remote Control</option>
<option value="SETTOP">Settop</option>
<option value="SOUNDBAR">Soundbar</option>
<option value="STREAMING_BOX">Streaming Box</option>
<option value="STREAMING_SOUNDBAR">Streaming Soundbar</option>
<option value="STREAMING_STICK">Streaming Stick</option>
<option value="TV">TV</option>
</select>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-supportOnOff"><i class="fa fa-power-off"></i> On/Off Trait </label>
<input type="checkbox" id="node-input-supportOnOff" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="trait-props onoff-trait">
<div class="form-row">
<label style="width:auto" for="node-input-asyncCmdOnOff"><i class="fa fa-refresh"></i> Async command execution: </label>
<input type="checkbox" class="async-cmd" id="node-input-asyncCmdOnOff" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-errorifstateunchaged"><i class="fa fa-exclamation-triangle"></i> If on/off state doesn't change via voice, warn user: </label>
<input type="checkbox" id="node-input-errorifstateunchaged" style="display:inline-block; width:auto; vertical-align:top;">
</div>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-supportVolume"><i class="fa fa-volume-up"></i> Volume Trait </label>
<input type="checkbox" id="node-input-supportVolume" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="trait-props volume-trait">
<div class="form-row">
<label style="width:auto" for="node-input-asyncCmdVolume"><i class="fa fa-refresh"></i> Async command execution: </label>
<input type="checkbox" class="async-cmd" id="node-input-asyncCmdVolume" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-volumeCanMuteAndUnmute"> Can mute/unmute </label>
<input type="checkbox" id="node-input-volumeCanMuteAndUnmute" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-volumeLevelStepSize" style="width:unset"><i class="fa fa-i-cursor"></i> Volume level step size</label>
<input type="text" id="node-input-volumeLevelStepSize" style="width:30%" placeholder="1">
</div>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-supportMediaState"><i class="fa fa-play"></i> Media State Trait </label>
<input type="checkbox" id="node-input-supportMediaState" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="trait-props mediastate-trait">
<div class="form-row">
<label style="width:auto" for="node-input-supportActivityState"> Support activity state </label>
<input type="checkbox" id="node-input-supportActivityState" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label style="width:auto" for="node-input-supportPlaybackState"> Support playback state </label>
<input type="checkbox" id="node-input-supportPlaybackState" style="display:inline-block; width:auto; vertical-align:top;">
</div>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-supportTransportControl"><i class="fa fa-play-circle"></i> Transport Control Trait </label>
<input type="checkbox" id="node-input-supportTransportControl" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="trait-props transportcontrol-trait">
<div class="form-row">
<label style="width:auto" for="node-input-asyncCmdTransportControl"><i class="fa fa-refresh"></i> Async command execution: </label>
<input type="checkbox" class="async-cmd" id="node-input-asyncCmdTransportControl" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="transportcontrol-options">
</div>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-supportInputSelector"><i class="fa fa-play-circle"></i> Input Selector Trait </label>
<input type="checkbox" id="node-input-supportInputSelector" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="trait-props inputselector-trait">
<div class="form-row">
<label style="width:auto" for="node-input-asyncCmdInputSelector"><i class="fa fa-refresh"></i> Async command execution: </label>
<input type="checkbox" class="async-cmd" id="node-input-asyncCmdInputSelector" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-language"><i class="fa fa-i-cursor"></i> Language</label>
<select id="node-input-language">
<option value="da">da</option>
<option value="nl">nl</option>
<option value="en">en</option>
<option value="fr">fr</option>
<option value="de">de</option>
<option value="hi">hi</option>
<option value="id">id</option>
<option value="it">it</option>
<option value="ja">ja</option>
<option value="ko">ko</option>
<option value="no">no</option>
<option value="pt-BR">pt</option>
<option value="es">es</option>
<option value="sv">sv</option>
<option value="th">th</option>
</select>
</div>
<label><i class="fa fa-tag"></i> <span>Inputs:</span></label>
<div class="form-row">
<ol id="node-input-inputs-container"></ol>
</div>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-supportAppSelector"><i class="fa fa-window-maximize"></i> App Selector Trait </label>
<input type="checkbox" id="node-input-supportAppSelector" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="trait-props appselector-trait">
<div class="form-row">
<label style="width:auto" for="node-input-asyncCmdAppSelector"><i class="fa fa-refresh"></i> Async command execution: </label>
<input type="checkbox" class="async-cmd" id="node-input-asyncCmdAppSelector" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="form-row">
<label for="node-input-appsLanguage"><i class="fa fa-i-cursor"></i> Language</label>
<select id="node-input-appsLanguage">
<option value="da">da</option>
<option value="nl">nl</option>
<option value="en">en</option>
<option value="fr">fr</option>
<option value="de">de</option>
<option value="hi">hi</option>
<option value="id">id</option>
<option value="it">it</option>
<option value="ja">ja</option>
<option value="ko">ko</option>
<option value="no">no</option>
<option value="pt-BR">pt</option>
<option value="es">es</option>
<option value="sv">sv</option>
<option value="th">th</option>
</select>
</div>
<label><i class="fa fa-tag"></i> <span>Apps:</span></label>
<div class="form-row">
<ol id="node-input-apps-container"></ol>
</div>
</div>
<div class="form-row">
<label style="width:auto" for="node-input-supportChannel"><i class="fa fa-play-circle"></i> Channel Trait </label>
<input type="checkbox" id="node-input-supportChannel" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<div class="trait-props channel-trait">
<div class="form-row">
<label style="width:auto" for="node-input-asyncCmdChannel"><i class="fa fa-refresh"></i> Async command execution: </label>
<input type="checkbox" class="async-cmd" id="node-input-asyncCmdChannel" style="display:inline-block; width:auto; vertical-align:top;">
</div>
<label><i class="fa fa-tag"></i> <span>Channels:</span></label>
<div class="form-row">
<ol id="node-input-channels-container"></ol>
</div>
</div>
<div class="form-row">
<label for="node-input-roomhint"><i class="fa fa-i-cursor"></i> Room Hint</label>
<input type="text" id="node-input-roomhint">
</div>
<div class="form-row">
<label for="node-input-twofactor"><i class="fa fa-question-sign"></i> Two Factor</label>
<select id="node-input-twofactor">
<option value="off">None</option>
<option value="ack">Acknowledge</option>
<option value="pin">Pin</option>
</select>
</div>
<div id="node-twofactor-pin" class="form-row">
<label for="node-input-twofactorpin"><i class="fa fa-code"></i> Pin</label>
<input type="text" id="node-input-twofactorpin">
</div>
<div class="form-row">
<label for="node-input-topic" style="padding-left:25px; margin-right:-25px">Topic</label>
<input type="text" id="node-input-topic">
</div>
<div class="form-row">
<label for="node-input-name"><i class="fa fa-tag"></i> Name</label>
<input type="text" id="node-input-name">
</div>
</script>
<script type="text/x-red" data-help-name="noraf-media">
<p>
<a href="https://github.com/andrei-tatar/node-red-contrib-smartnora/blob/master/doc/nodes/media/README.md">https://github.com/andrei-tatar/node-red-contrib-smartnora/blob/master/doc/nodes/media/README.md</a>
</p>
</script>