forked from platypush/platypush
Added events and websocket support in webpanel refactoring and removed old light.hue templates and scripts
This commit is contained in:
parent
561ce4c8aa
commit
c751a24790
8 changed files with 155 additions and 876 deletions
|
@ -1,127 +0,0 @@
|
||||||
#hue-container {
|
|
||||||
background-color: #f8f8f8;
|
|
||||||
padding: 12px;
|
|
||||||
border: 1px solid #ddd;
|
|
||||||
border-radius: 10px;
|
|
||||||
font-family: "Raleway", "HelveticaNeue", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
|
||||||
font-weight: 400;
|
|
||||||
line-height: 38px;
|
|
||||||
letter-spacing: .1rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hue-container > .columns {
|
|
||||||
margin-left: 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hue-container > .three.columns {
|
|
||||||
width: 24%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#hue-container > .six.columns {
|
|
||||||
width: 52%;
|
|
||||||
}
|
|
||||||
|
|
||||||
#rooms-list {
|
|
||||||
border-right: 1px solid #e4e4e4;
|
|
||||||
}
|
|
||||||
|
|
||||||
#lights-list {
|
|
||||||
margin-left: 0;
|
|
||||||
border: 1px solid #e4e4e4;
|
|
||||||
border-radius: 8px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.hue-section-title {
|
|
||||||
padding: 7.5px;
|
|
||||||
background: #ececec;
|
|
||||||
border: 1px solid #e4e4e4;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-item {
|
|
||||||
color: #333;
|
|
||||||
padding: 10px 5px;
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
text-transform: uppercase;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.scene-item {
|
|
||||||
color: #333;
|
|
||||||
padding: 5px 10px;
|
|
||||||
border-bottom: 1px solid #e4e4e4;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-item:hover, .light-item:hover, .scene-item:hover {
|
|
||||||
background-color: #daf8e2 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-item.selected, .light-item.selected, .scene-item.selected {
|
|
||||||
background-color: #c8ffd0 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.room-lights-item, .room-scenes-item {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-item {
|
|
||||||
color: #333;
|
|
||||||
padding: 20px;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-item:not(:last-child) {
|
|
||||||
border-bottom: 1px solid #ddd;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-item-name {
|
|
||||||
display: inline-block;
|
|
||||||
}
|
|
||||||
|
|
||||||
.all-lights-item, .animation-item {
|
|
||||||
background-color: #ececec;
|
|
||||||
}
|
|
||||||
|
|
||||||
.all-lights-item * > .light-item-name {
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-item * > .light-item-name {
|
|
||||||
font-style: italic;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-container {
|
|
||||||
padding: 1rem .5rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.slider-container > .columns {
|
|
||||||
line-height: initial;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-type-container * > label {
|
|
||||||
line-height: initial;
|
|
||||||
font-weight: 100;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-ctrl-switch-container {
|
|
||||||
float: right;
|
|
||||||
margin-top: -5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-color-selector,
|
|
||||||
.animation-selector {
|
|
||||||
display: none;
|
|
||||||
background-color: white;
|
|
||||||
padding: 10px;
|
|
||||||
margin: 10px -10px -10px -10px;
|
|
||||||
border-radius: 10px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.animation-container {
|
|
||||||
display: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
.light-slider {
|
|
||||||
margin-top: 10px;
|
|
||||||
}
|
|
||||||
|
|
|
@ -45,6 +45,8 @@ var app = new Vue({
|
||||||
setInterval(() => {
|
setInterval(() => {
|
||||||
self.now = new Date();
|
self.now = new Date();
|
||||||
}, 1000)
|
}, 1000)
|
||||||
|
|
||||||
|
initEvents();
|
||||||
},
|
},
|
||||||
updated: function() {},
|
updated: function() {},
|
||||||
destroyed: function() {},
|
destroyed: function() {},
|
||||||
|
|
119
platypush/backend/http/static/js/events.js
Normal file
119
platypush/backend/http/static/js/events.js
Normal file
|
@ -0,0 +1,119 @@
|
||||||
|
var websocket = {
|
||||||
|
ws: undefined,
|
||||||
|
instance: undefined,
|
||||||
|
pending: false,
|
||||||
|
opened: false,
|
||||||
|
timeout: undefined,
|
||||||
|
reconnectMsecs: 30000,
|
||||||
|
handlers: {},
|
||||||
|
};
|
||||||
|
|
||||||
|
function initEvents() {
|
||||||
|
try {
|
||||||
|
url_prefix = window.config.has_ssl ? 'wss://' : 'ws://';
|
||||||
|
websocket.ws = new WebSocket(url_prefix + window.location.hostname + ':' + window.config.websocket_port);
|
||||||
|
} catch (err) {
|
||||||
|
console.error("Websocket initialization error");
|
||||||
|
console.log(err);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket.pending = true;
|
||||||
|
|
||||||
|
var onWebsocketTimeout = function(self) {
|
||||||
|
return function() {
|
||||||
|
console.log('Websocket reconnection timed out, retrying');
|
||||||
|
websocket.pending = false;
|
||||||
|
self.close();
|
||||||
|
self.onclose();
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.timeout = setTimeout(
|
||||||
|
onWebsocketTimeout(websocket.ws), websocket.reconnectMsecs);
|
||||||
|
|
||||||
|
websocket.ws.onmessage = function(event) {
|
||||||
|
console.debug(event);
|
||||||
|
handlers = [];
|
||||||
|
event = event.data;
|
||||||
|
|
||||||
|
if (typeof event === 'string') {
|
||||||
|
event = JSON.parse(event);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.type !== 'event') {
|
||||||
|
// Discard non-event messages
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (null in websocket.handlers) {
|
||||||
|
handlers.push(websocket.handlers[null]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (event.args.type in websocket.handlers) {
|
||||||
|
handlers.push(...websocket.handlers[event.args.type]);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var handler of handlers) {
|
||||||
|
handler(event.args);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.ws.onopen = function(event) {
|
||||||
|
if (websocket.instance) {
|
||||||
|
console.log("There's already an opened websocket connection, closing the newly opened one");
|
||||||
|
this.onclose = function() {};
|
||||||
|
this.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('Websocket connection successful');
|
||||||
|
websocket.instance = this;
|
||||||
|
|
||||||
|
if (websocket.pending) {
|
||||||
|
websocket.pending = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (websocket.timeout) {
|
||||||
|
clearTimeout(websocket.timeout);
|
||||||
|
websocket.timeout = undefined;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.ws.onerror = function(event) {
|
||||||
|
console.error(event);
|
||||||
|
};
|
||||||
|
|
||||||
|
websocket.ws.onclose = function(event) {
|
||||||
|
if (event) {
|
||||||
|
console.log('Websocket closed - code: ' + event.code + ' - reason: ' + event.reason);
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket.instance = undefined;
|
||||||
|
|
||||||
|
if (!websocket.pending) {
|
||||||
|
websocket.pending = true;
|
||||||
|
initEvents();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
function registerEventHandler(handler, ...events) {
|
||||||
|
if (events.length) {
|
||||||
|
// Event type filter specified
|
||||||
|
for (var event of events) {
|
||||||
|
if (!(event in websocket.handlers)) {
|
||||||
|
websocket.handlers[event] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket.handlers[event].push(handler);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// No event type specified, listen to all events
|
||||||
|
if (!(null in websocket.handlers)) {
|
||||||
|
websocket.handlers[null] = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
websocket.handlers[null].push(handler);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -1,712 +0,0 @@
|
||||||
$(document).ready(function() {
|
|
||||||
var lights,
|
|
||||||
groups,
|
|
||||||
scenes,
|
|
||||||
$roomsList = $('#rooms-list'),
|
|
||||||
$lightsList = $('#lights-list'),
|
|
||||||
$scenesList = $('#scenes-list');
|
|
||||||
|
|
||||||
var onEvent = function(event) {
|
|
||||||
switch (event.args.type) {
|
|
||||||
case 'platypush.message.event.light.LightStatusChangeEvent':
|
|
||||||
var $lightsList = $('#lights-list');
|
|
||||||
var $light = $lightsList.find('.light-item').filter(
|
|
||||||
(i, light) => $(light).data('id') == event.args.light_id
|
|
||||||
);
|
|
||||||
|
|
||||||
var $roomLights = $light.parent('.room-lights-item').find('.light-item').filter(
|
|
||||||
(i, light) => $(light).data('type') == 'light'
|
|
||||||
);
|
|
||||||
|
|
||||||
var $allLightsItem = $light.parent('.room-lights-item').find('.light-item').filter(
|
|
||||||
(i, light) => $(light).data('type') == 'room'
|
|
||||||
);
|
|
||||||
|
|
||||||
if ('on' in event.args) {
|
|
||||||
$light.find('.light-ctrl-switch').prop('checked', event.args.on);
|
|
||||||
|
|
||||||
if ($roomLights.find('.light-ctrl-switch:checked').length > 0) {
|
|
||||||
$allLightsItem.find('.light-ctrl-switch').prop('checked', true);
|
|
||||||
} else {
|
|
||||||
$allLightsItem.find('.light-ctrl-switch').prop('checked', false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('bri' in event.args) {
|
|
||||||
$light.find('.slider.bri').val(event.args.bri);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('sat' in event.args) {
|
|
||||||
$light.find('.slider.sat').val(event.args.sat);
|
|
||||||
}
|
|
||||||
|
|
||||||
if ('hue' in event.args) {
|
|
||||||
$light.find('.slider.hue').val(event.args.hue);
|
|
||||||
}
|
|
||||||
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var createPowerToggleElement = function(data) {
|
|
||||||
var id = data['type'] + '_' + data['id'];
|
|
||||||
var $powerToggle = $('<div></div>').addClass('toggle toggle--push light-ctrl-switch-container');
|
|
||||||
var $input = $('<input></input>').attr('type', 'checkbox')
|
|
||||||
.attr('id', id).addClass('toggle--checkbox');
|
|
||||||
|
|
||||||
if (type === 'animation') {
|
|
||||||
$input.addClass('animation-switch');
|
|
||||||
} else {
|
|
||||||
$input.addClass('light-ctrl-switch');
|
|
||||||
}
|
|
||||||
|
|
||||||
data = data || {};
|
|
||||||
for (var attr of Object.keys(data)) {
|
|
||||||
$input.data(attr, data[attr]);
|
|
||||||
}
|
|
||||||
|
|
||||||
var $label = $('<label></label>').attr('for', id).addClass('toggle--btn');
|
|
||||||
|
|
||||||
$input.appendTo($powerToggle);
|
|
||||||
$label.appendTo($powerToggle);
|
|
||||||
|
|
||||||
if ('on' in data && data['on']) {
|
|
||||||
$input.prop('checked', true);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $powerToggle;
|
|
||||||
};
|
|
||||||
|
|
||||||
var createColorSelector = function(data) {
|
|
||||||
var type = data.type;
|
|
||||||
var element;
|
|
||||||
|
|
||||||
if (type === 'light') {
|
|
||||||
element = lights[data.id];
|
|
||||||
} else if (type === 'room' || type === 'animation') {
|
|
||||||
element = groups[data.id];
|
|
||||||
} else {
|
|
||||||
throw "Unknown type: " + type;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $colorSelector = $('<div></div>')
|
|
||||||
.addClass('light-color-selector');
|
|
||||||
|
|
||||||
if (type === 'animation') {
|
|
||||||
// Animation type selector
|
|
||||||
var $typeContainer = $('<div></div>')
|
|
||||||
.addClass('animation-type-container').addClass('row');
|
|
||||||
|
|
||||||
var $typeText = $('<div></div>').addClass('two columns').text('Type');
|
|
||||||
|
|
||||||
// Color transition type
|
|
||||||
var $transitionTypeContainer = $('<div></div>').addClass('five columns');
|
|
||||||
|
|
||||||
var $transitionType = $('<input></input>').addClass('animation-type')
|
|
||||||
.addClass('one column').data('type', 'color_transition')
|
|
||||||
.attr('id', 'hue-animation-color-transition').data('name', element.name)
|
|
||||||
.attr('type', 'radio').attr('name', 'animation-type');
|
|
||||||
|
|
||||||
var $transitionTypeLabel = $('<label></label>').attr('for', 'hue-animation-color-transition')
|
|
||||||
.addClass('four columns').text('Color transition');
|
|
||||||
|
|
||||||
$typeText.appendTo($typeContainer);
|
|
||||||
|
|
||||||
$transitionType.appendTo($transitionTypeContainer);
|
|
||||||
$transitionTypeLabel.appendTo($transitionTypeContainer);
|
|
||||||
$transitionTypeContainer.appendTo($typeContainer);
|
|
||||||
|
|
||||||
// Blink type
|
|
||||||
var $blinkTypeContainer = $('<div></div>').addClass('five columns');
|
|
||||||
|
|
||||||
var $blinkType = $('<input></input>').addClass('animation-type')
|
|
||||||
.addClass('one column').data('type', 'blink')
|
|
||||||
.attr('id', 'hue-animation-blink').data('name', element.name)
|
|
||||||
.attr('type', 'radio').attr('name', 'animation-type');
|
|
||||||
|
|
||||||
var $blinkTypeLabel = $('<label></label>').attr('for', 'hue-animation-blink')
|
|
||||||
.addClass('four columns').text('Blink');
|
|
||||||
|
|
||||||
$blinkType.appendTo($blinkTypeContainer);
|
|
||||||
$blinkTypeLabel.appendTo($blinkTypeContainer);
|
|
||||||
$blinkTypeContainer.appendTo($typeContainer);
|
|
||||||
|
|
||||||
$typeContainer.appendTo($colorSelector);
|
|
||||||
|
|
||||||
// Color transition container
|
|
||||||
var $animationContainer = $('<div></div>')
|
|
||||||
.addClass('animation-container row').data('animation-type', 'color_transition');
|
|
||||||
|
|
||||||
// Hue slider
|
|
||||||
var $hueContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var defaultHueRange = [0, 65535];
|
|
||||||
var $hueText = $('<div></div>').addClass('two columns').text('Hue range');
|
|
||||||
var $hueSlider = $('<div></div>')
|
|
||||||
.addClass('ten columns').data('animation-property', 'hue_range')
|
|
||||||
.data('id', data.id).val(defaultHueRange).slider({
|
|
||||||
range: true,
|
|
||||||
min: 0,
|
|
||||||
max: 65535,
|
|
||||||
values: defaultHueRange,
|
|
||||||
slide: function(event, ui) {
|
|
||||||
var values = $(event.target).slider("option", "values");
|
|
||||||
$(this).val(values);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$hueText.appendTo($hueContainer);
|
|
||||||
$hueSlider.appendTo($hueContainer);
|
|
||||||
$hueContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
// Sat slider
|
|
||||||
var $satContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var defaultSatRange = [155, 255];
|
|
||||||
var $satText = $('<div></div>').addClass('two columns').text('Sat range');
|
|
||||||
var $satSlider = $('<div></div>')
|
|
||||||
.addClass('ten columns').data('animation-property', 'sat_range')
|
|
||||||
.data('id', data.id).val(defaultSatRange).slider({
|
|
||||||
range: true,
|
|
||||||
min: 0,
|
|
||||||
max: 255,
|
|
||||||
values: defaultSatRange,
|
|
||||||
slide: function(event, ui) {
|
|
||||||
var values = $(event.target).slider("option", "values");
|
|
||||||
$(this).val(values);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$satText.appendTo($satContainer);
|
|
||||||
$satSlider.appendTo($satContainer);
|
|
||||||
$satContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
// Bri slider
|
|
||||||
var $briContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var defaultBriRange = [240, 255];
|
|
||||||
var $briText = $('<div></div>').addClass('two columns').text('Bri range');
|
|
||||||
var $briSlider = $('<div></div>')
|
|
||||||
.addClass('ten columns').data('animation-property', 'bri_range')
|
|
||||||
.data('id', data.id).val(defaultBriRange).slider({
|
|
||||||
range: true,
|
|
||||||
min: 0,
|
|
||||||
max: 255,
|
|
||||||
values: defaultBriRange,
|
|
||||||
slide: function(event, ui) {
|
|
||||||
var values = $(event.target).slider("option", "values");
|
|
||||||
$(this).val(values);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$briText.appendTo($briContainer);
|
|
||||||
$briSlider.appendTo($briContainer);
|
|
||||||
$briContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
// Hue step
|
|
||||||
var $hueStepContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var $hueStepText = $('<div></div>').addClass('two columns').text('Hue step');
|
|
||||||
var $hueStepSlider = $('<input></input>').addClass('slider light-slider')
|
|
||||||
.addClass('ten columns').addClass('hue').data('animation-property', 'hue_step')
|
|
||||||
.attr('type', 'range').attr('min', 0).attr('max', 65535)
|
|
||||||
.data('id', data.id).data('name', element.name).val(1000);
|
|
||||||
|
|
||||||
$hueStepText.appendTo($hueStepContainer);
|
|
||||||
$hueStepSlider.appendTo($hueStepContainer);
|
|
||||||
$hueStepContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
// Sat step
|
|
||||||
var $satStepContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var $satStepText = $('<div></div>').addClass('two columns').text('Sat step');
|
|
||||||
var $satStepSlider = $('<input></input>').addClass('slider light-slider')
|
|
||||||
.addClass('ten columns').addClass('sat').data('animation-property', 'sat_step')
|
|
||||||
.attr('type', 'range').attr('min', 0).attr('max', 255)
|
|
||||||
.data('id', data.id).data('name', element.name).val(2);
|
|
||||||
|
|
||||||
$satStepText.appendTo($satStepContainer);
|
|
||||||
$satStepSlider.appendTo($satStepContainer);
|
|
||||||
$satStepContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
// Bri step
|
|
||||||
var $briStepContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var $briStepText = $('<div></div>').addClass('two columns').text('Bri step');
|
|
||||||
var $briStepSlider = $('<input></input>').addClass('slider light-slider')
|
|
||||||
.addClass('ten columns').addClass('bri').data('animation-property', 'bri_step')
|
|
||||||
.attr('type', 'range').attr('min', 0).attr('max', 255)
|
|
||||||
.data('id', data.id).data('name', element.name).val(1);
|
|
||||||
|
|
||||||
$briStepText.appendTo($briStepContainer);
|
|
||||||
$briStepSlider.appendTo($briStepContainer);
|
|
||||||
$briStepContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
// Transition seconds
|
|
||||||
var $transitionContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var $transitionText = $('<div></div>').addClass('two columns').text('Transition seconds');
|
|
||||||
var $transitionInput = $('<input></input>').data('animation-property', 'transition_seconds')
|
|
||||||
.addClass('two columns pull-right').attr('type', 'text')
|
|
||||||
.data('id', data.id).data('name', element.name).val(1);
|
|
||||||
|
|
||||||
$transitionText.appendTo($transitionContainer);
|
|
||||||
$transitionInput.appendTo($transitionContainer);
|
|
||||||
$transitionContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
// Duration seconds
|
|
||||||
var $durationContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var $durationText = $('<div></div>').addClass('two columns').text('Duration seconds');
|
|
||||||
var $durationInput = $('<input></input>').data('animation-property', 'duration')
|
|
||||||
.addClass('two columns pull-right').attr('type', 'text')
|
|
||||||
.data('id', data.id).data('name', element.name);
|
|
||||||
|
|
||||||
$durationText.appendTo($durationContainer);
|
|
||||||
$durationInput.appendTo($durationContainer);
|
|
||||||
$durationContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
$animationContainer.appendTo($colorSelector);
|
|
||||||
|
|
||||||
// Blink animation container
|
|
||||||
$animationContainer = $('<div></div>')
|
|
||||||
.addClass('animation-container row').data('animation-type', 'blink');
|
|
||||||
|
|
||||||
// Transition seconds
|
|
||||||
$transitionContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
$transitionText = $('<div></div>').addClass('two columns').text('Transition seconds');
|
|
||||||
$transitionInput = $('<input></input>').data('animation-property', 'transition_seconds')
|
|
||||||
.addClass('two columns pull-right').attr('type', 'text')
|
|
||||||
.data('id', data.id).data('name', element.name).val(1);
|
|
||||||
|
|
||||||
$transitionText.appendTo($transitionContainer);
|
|
||||||
$transitionInput.appendTo($transitionContainer);
|
|
||||||
$transitionContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
// Duration seconds
|
|
||||||
$durationContainer = $('<div></div>').data('animation-property', 'duration')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
$durationText = $('<div></div>').addClass('two columns').text('Duration seconds');
|
|
||||||
$durationInput = $('<input></input>').data('animation-property', 'duration')
|
|
||||||
.addClass('two columns pull-right').attr('type', 'text')
|
|
||||||
.data('id', data.id).data('name', element.name);
|
|
||||||
|
|
||||||
$durationText.appendTo($durationContainer);
|
|
||||||
$durationInput.appendTo($durationContainer);
|
|
||||||
$durationContainer.appendTo($animationContainer);
|
|
||||||
|
|
||||||
$animationContainer.appendTo($colorSelector);
|
|
||||||
} else {
|
|
||||||
// Hue slider
|
|
||||||
var $hueContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var $hueText = $('<div></div>').addClass('two columns').text('Hue');
|
|
||||||
var $hueSlider = $('<input></input>').addClass('slider light-slider')
|
|
||||||
.addClass('ten columns').addClass('hue').data('type', type)
|
|
||||||
.attr('type', 'range').attr('min', 0).attr('max', 65535).data('property', 'hue')
|
|
||||||
.data('id', data.id).data('name', element.name).val(type === 'light' ? element.state.hue : 0);
|
|
||||||
|
|
||||||
$hueText.appendTo($hueContainer);
|
|
||||||
$hueSlider.appendTo($hueContainer);
|
|
||||||
$hueContainer.appendTo($colorSelector);
|
|
||||||
|
|
||||||
// Saturation slider
|
|
||||||
var $satContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var $satText = $('<div></div>').addClass('two columns').text('Saturation');
|
|
||||||
var $satSlider = $('<input></input>').addClass('slider light-slider')
|
|
||||||
.addClass('ten columns').addClass('sat').data('type', type)
|
|
||||||
.attr('type', 'range').attr('min', 0).attr('max', 255).data('property', 'sat')
|
|
||||||
.data('id', data.id).data('name', element.name).val(type === 'light' ? element.state.sat : 0);
|
|
||||||
|
|
||||||
$satText.appendTo($satContainer);
|
|
||||||
$satSlider.appendTo($satContainer);
|
|
||||||
$satContainer.appendTo($colorSelector);
|
|
||||||
|
|
||||||
// Brightness slider
|
|
||||||
var $briContainer = $('<div></div>')
|
|
||||||
.addClass('slider-container').addClass('row');
|
|
||||||
|
|
||||||
var $briText = $('<div></div>').addClass('two columns').text('Brightness');
|
|
||||||
var $briSlider = $('<input></input>').addClass('slider light-slider')
|
|
||||||
.addClass('ten columns').addClass('bri').data('type', type)
|
|
||||||
.attr('type', 'range').attr('min', 0).attr('max', 255).data('property', 'bri')
|
|
||||||
.data('id', data.id).data('name', element.name).val(type === 'light' ? element.state.bri : 0);
|
|
||||||
|
|
||||||
$briText.appendTo($briContainer);
|
|
||||||
$briSlider.appendTo($briContainer);
|
|
||||||
$briContainer.appendTo($colorSelector);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $colorSelector;
|
|
||||||
};
|
|
||||||
|
|
||||||
var createLightCtrlElement = function(type, id) {
|
|
||||||
var element;
|
|
||||||
|
|
||||||
if (type === 'light') {
|
|
||||||
element = lights[id];
|
|
||||||
} else if (type === 'room' || type === 'animation') {
|
|
||||||
element = groups[id];
|
|
||||||
} else {
|
|
||||||
throw "Unknown type: " + type;
|
|
||||||
}
|
|
||||||
|
|
||||||
var on;
|
|
||||||
if (type === 'light') {
|
|
||||||
on = element.state.on;
|
|
||||||
} else if (type === 'room') {
|
|
||||||
on = element.state.any_on;
|
|
||||||
} else {
|
|
||||||
on = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $light = $('<div></div>')
|
|
||||||
.addClass('light-item')
|
|
||||||
.data('type', type)
|
|
||||||
.data('id', id)
|
|
||||||
.data('name', element.name)
|
|
||||||
.data('on', on);
|
|
||||||
|
|
||||||
if (type === 'room') {
|
|
||||||
$light.addClass('all-lights-item');
|
|
||||||
} else if (type === 'animation') {
|
|
||||||
$light.addClass('animation-item');
|
|
||||||
}
|
|
||||||
|
|
||||||
var $row1 = $('<div></div>').addClass('row');
|
|
||||||
var $row2 = $('<div></div>').addClass('row');
|
|
||||||
|
|
||||||
var lightName;
|
|
||||||
switch(type) {
|
|
||||||
case 'light': lightName = element.name; break;
|
|
||||||
case 'room': lightName = 'All Lights'; break;
|
|
||||||
case 'animation': lightName = 'Animate'; break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var $lightName = $('<div></div>')
|
|
||||||
.addClass('light-item-name')
|
|
||||||
.text(lightName);
|
|
||||||
|
|
||||||
var $powerToggle = createPowerToggleElement({
|
|
||||||
type: type,
|
|
||||||
id: id,
|
|
||||||
on: on,
|
|
||||||
});
|
|
||||||
|
|
||||||
var $colorSelector = createColorSelector({
|
|
||||||
type: type,
|
|
||||||
id: id,
|
|
||||||
});
|
|
||||||
|
|
||||||
$lightName.appendTo($row1);
|
|
||||||
$powerToggle.appendTo($row1);
|
|
||||||
$colorSelector.appendTo($row2);
|
|
||||||
|
|
||||||
$row1.appendTo($light);
|
|
||||||
$row2.appendTo($light);
|
|
||||||
|
|
||||||
return $light;
|
|
||||||
};
|
|
||||||
|
|
||||||
var updateRooms = function(rooms) {
|
|
||||||
var roomByLight = {};
|
|
||||||
var roomsByScene = {};
|
|
||||||
|
|
||||||
$roomsList.html('');
|
|
||||||
$lightsList.html('');
|
|
||||||
$scenesList.html('');
|
|
||||||
|
|
||||||
for (var room of Object.keys(rooms)) {
|
|
||||||
var $room = $('<div></div>')
|
|
||||||
.addClass('room-item')
|
|
||||||
.data('id', room)
|
|
||||||
.data('name', rooms[room].name)
|
|
||||||
.text(rooms[room].name);
|
|
||||||
|
|
||||||
var $roomLights = $('<div></div>')
|
|
||||||
.addClass('room-lights-item')
|
|
||||||
.data('id', room);
|
|
||||||
|
|
||||||
var $roomScenes = $('<div></div>')
|
|
||||||
.addClass('room-scenes-item')
|
|
||||||
.data('room-id', room)
|
|
||||||
.data('room-name', rooms[room].name);
|
|
||||||
|
|
||||||
$room.appendTo($roomsList);
|
|
||||||
$roomLights.appendTo($lightsList);
|
|
||||||
$roomScenes.appendTo($scenesList);
|
|
||||||
|
|
||||||
for (var light of rooms[room].lights) {
|
|
||||||
var $light = createLightCtrlElement(type='light', id=light);
|
|
||||||
$light.appendTo($roomLights);
|
|
||||||
roomByLight[light] = room;
|
|
||||||
}
|
|
||||||
|
|
||||||
roomByLight[light] = room;
|
|
||||||
|
|
||||||
var $animation = createLightCtrlElement(type='animation', id=room);
|
|
||||||
$animation.prependTo($roomLights);
|
|
||||||
|
|
||||||
var $allLights = createLightCtrlElement(type='room', id=room);
|
|
||||||
$allLights.prependTo($roomLights);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (var scene of Object.keys(scenes)) {
|
|
||||||
if (scenes[scene].name.match(/(on|off) \d+$/)) {
|
|
||||||
// Old 1.0 scenes are saved as "scene_name on <timestamp>" but aren't visible
|
|
||||||
// not settable through the app - ignore them
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
roomsByScene[scene] = new Set();
|
|
||||||
for (var light of scenes[scene].lights) {
|
|
||||||
var room = roomByLight[light];
|
|
||||||
if (roomsByScene[scene].has(room)) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
roomsByScene[scene].add(room);
|
|
||||||
|
|
||||||
var $roomScenes = $scenesList.find('.room-scenes-item')
|
|
||||||
.filter(function(i, item) {
|
|
||||||
return $(item).data('room-id') === room
|
|
||||||
});
|
|
||||||
|
|
||||||
var $scene = $('<div></div>')
|
|
||||||
.addClass('scene-item')
|
|
||||||
.data('id', scene)
|
|
||||||
.data('name', scenes[scene].name)
|
|
||||||
.text(scenes[scene].name);
|
|
||||||
|
|
||||||
$scene.appendTo($roomScenes);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var initUi = function() {
|
|
||||||
$.when(
|
|
||||||
execute({ type: 'request', action: 'light.hue.get_lights' }),
|
|
||||||
execute({ type: 'request', action: 'light.hue.get_groups' }),
|
|
||||||
execute({ type: 'request', action: 'light.hue.get_scenes' })
|
|
||||||
).done(function(l, g, s) {
|
|
||||||
lights = l[0].response.output;
|
|
||||||
groups = g[0].response.output;
|
|
||||||
scenes = s[0].response.output;
|
|
||||||
|
|
||||||
for (var group of Object.keys(groups)) {
|
|
||||||
if (groups[group].type.toLowerCase() !== 'room') {
|
|
||||||
delete groups[group];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateRooms(groups);
|
|
||||||
}).then(function() {
|
|
||||||
initBindings();
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
var refreshStatus = function() {
|
|
||||||
var onResponse = function(data) {
|
|
||||||
var response = data.response.output;
|
|
||||||
for (var light of Object.keys(response)) {
|
|
||||||
var $element = $('.light-item').filter(function(i, item) {
|
|
||||||
return $(item).data('type') === 'light' && $(item).data('id') === light
|
|
||||||
});
|
|
||||||
|
|
||||||
$element.find('input.light-ctrl-switch').prop('checked', response[light].state.on);
|
|
||||||
$element.find('input.hue').val(response[light].state.hue);
|
|
||||||
$element.find('input.sat').val(response[light].state.sat);
|
|
||||||
$element.find('input.bri').val(response[light].state.bri);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
execute({ type: 'request', action: 'light.hue.get_lights' }, onResponse);
|
|
||||||
};
|
|
||||||
|
|
||||||
var initBindings = function() {
|
|
||||||
$roomsList.on('click touch', '.room-item', function() {
|
|
||||||
$('.room-item').removeClass('selected');
|
|
||||||
$('.room-lights-item').hide();
|
|
||||||
$('.room-scenes-item').hide();
|
|
||||||
|
|
||||||
var roomId = $(this).data('id');
|
|
||||||
var $roomLights = $('.room-lights-item').filter(function(i, item) {
|
|
||||||
return $(item).data('id') === roomId
|
|
||||||
});
|
|
||||||
|
|
||||||
var $roomScenes = $('.room-scenes-item').filter(function(i, item) {
|
|
||||||
return $(item).data('room-id') === roomId
|
|
||||||
});
|
|
||||||
|
|
||||||
$(this).addClass('selected');
|
|
||||||
$roomLights.show();
|
|
||||||
$roomScenes.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
$scenesList.on('click touch', '.scene-item', function() {
|
|
||||||
$('.scene-item').removeClass('selected');
|
|
||||||
$(this).addClass('selected');
|
|
||||||
|
|
||||||
execute({
|
|
||||||
type: 'request',
|
|
||||||
action: 'light.hue.scene',
|
|
||||||
args: {
|
|
||||||
name: $(this).data('name')
|
|
||||||
}
|
|
||||||
}, refreshStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
$lightsList.on('click touch', '.light-item-name', function() {
|
|
||||||
var $lightItem = $(this).parents('.light-item');
|
|
||||||
var $colorSelector = $lightItem.find('.light-color-selector');
|
|
||||||
|
|
||||||
$('.light-color-selector').hide();
|
|
||||||
$colorSelector.toggle();
|
|
||||||
|
|
||||||
$('.light-item').removeClass('selected');
|
|
||||||
$lightItem.addClass('selected');
|
|
||||||
});
|
|
||||||
|
|
||||||
$lightsList.on('click touch', '.light-ctrl-switch', function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
var $lightItem = $($(this).parents('.light-item'));
|
|
||||||
var type = $lightItem.data('type');
|
|
||||||
var name = $lightItem.data('name');
|
|
||||||
var isOn = $lightItem.data('on');
|
|
||||||
var action = 'light.hue.' + (isOn ? 'off' : 'on');
|
|
||||||
var key = (type == 'light' ? 'lights' : 'groups');
|
|
||||||
var args = {
|
|
||||||
type: 'request',
|
|
||||||
action: action,
|
|
||||||
args: {}
|
|
||||||
};
|
|
||||||
|
|
||||||
args['args'][key] = [name];
|
|
||||||
execute(args, function() {
|
|
||||||
$lightItem.data('on', !isOn);
|
|
||||||
refreshStatus();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
$lightsList.on('click touch', '.animation-switch', function(e) {
|
|
||||||
e.stopPropagation();
|
|
||||||
|
|
||||||
var turnedOn = $(this).prop('checked');
|
|
||||||
var args = {};
|
|
||||||
args['groups'] = $(this).parents('.animation-item').data('name');
|
|
||||||
args['animation'] = $(this).parents('.animation-item')
|
|
||||||
.find('input.animation-type:checked').data('type');
|
|
||||||
|
|
||||||
var $animationCtrl = $(this).parents('.animation-item')
|
|
||||||
.find('.animation-container').filter(
|
|
||||||
(index, node) => $(node).data('animation-type') === args['animation']
|
|
||||||
);
|
|
||||||
|
|
||||||
var params = $animationCtrl.find('*').filter(
|
|
||||||
(index, node) => $(node).data('animation-property'))
|
|
||||||
.toArray().reduce(
|
|
||||||
(map, input) => {
|
|
||||||
if ($(input).val().length) {
|
|
||||||
var val = $(input).val();
|
|
||||||
val = Array.isArray(val) ? val.map((i) => parseFloat(i)) : parseFloat(val);
|
|
||||||
map[$(input).data('animation-property')] = val;
|
|
||||||
}
|
|
||||||
|
|
||||||
return map
|
|
||||||
}, {}
|
|
||||||
);
|
|
||||||
|
|
||||||
for (var p of Object.keys(params)) {
|
|
||||||
args[p] = params[p];
|
|
||||||
}
|
|
||||||
|
|
||||||
if (turnedOn) {
|
|
||||||
execute(
|
|
||||||
{
|
|
||||||
type: 'request',
|
|
||||||
action: 'light.hue.animate',
|
|
||||||
args: args,
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess = function() {
|
|
||||||
$(this).prop('checked', true);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
} else {
|
|
||||||
execute(
|
|
||||||
{
|
|
||||||
type: 'request',
|
|
||||||
action: 'light.hue.stop_animation',
|
|
||||||
},
|
|
||||||
|
|
||||||
onSuccess = function() {
|
|
||||||
$(this).prop('checked', false);
|
|
||||||
}
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
$lightsList.on('mouseup touchend', '.light-slider', function() {
|
|
||||||
var property = $(this).data('property');
|
|
||||||
var type = $(this).data('type');
|
|
||||||
var name = $(this).data('name');
|
|
||||||
var args = {
|
|
||||||
type: 'request',
|
|
||||||
action: 'light.hue.' + property,
|
|
||||||
args: { value: $(this).val() }
|
|
||||||
};
|
|
||||||
|
|
||||||
if (type === 'light') {
|
|
||||||
args.args.lights = [name];
|
|
||||||
} else {
|
|
||||||
args.args.groups = [name];
|
|
||||||
}
|
|
||||||
|
|
||||||
execute(args, refreshStatus);
|
|
||||||
});
|
|
||||||
|
|
||||||
$lightsList.on('click touch', 'input.animation-type', function(e) {
|
|
||||||
var type = $(this).data('type');
|
|
||||||
var $animationContainers = $(this).parents('.animation-item').find('.animation-container')
|
|
||||||
var $animationContainer = $(this).parents('.animation-item').find('.animation-container')
|
|
||||||
.filter(function() { return $(this).data('animationType') === type })
|
|
||||||
|
|
||||||
$animationContainers.hide();
|
|
||||||
$animationContainer.show();
|
|
||||||
});
|
|
||||||
|
|
||||||
if (window.config.light.hue.default_group) {
|
|
||||||
var $defaultRoomItem = $roomsList.find('.room-item').filter(
|
|
||||||
(i, r) => $(r).data('name') == window.config.light.hue.default_group);
|
|
||||||
|
|
||||||
$defaultRoomItem.click();
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
var initEvents = function() {
|
|
||||||
window.registerEventListener(onEvent);
|
|
||||||
};
|
|
||||||
|
|
||||||
var init = function() {
|
|
||||||
initUi();
|
|
||||||
initEvents();
|
|
||||||
};
|
|
||||||
|
|
||||||
init();
|
|
||||||
});
|
|
||||||
|
|
|
@ -107,7 +107,7 @@ Vue.component('light-hue', {
|
||||||
light.state.on = event.state.all_on;
|
light.state.on = event.state.all_on;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const attr in ['bri', 'xy', 'ct']) {
|
for (const attr in ['hue', 'sat', 'bri', 'xy', 'ct']) {
|
||||||
if (attr in event.state) {
|
if (attr in event.state) {
|
||||||
light.state[attr] = event.state[attr];
|
light.state[attr] = event.state[attr];
|
||||||
}
|
}
|
||||||
|
@ -180,19 +180,49 @@ Vue.component('light-hue', {
|
||||||
onUnitInput: function(event) {
|
onUnitInput: function(event) {
|
||||||
var groups = this.lights[event.id].groups;
|
var groups = this.lights[event.id].groups;
|
||||||
for (const [groupId, group] of Object.entries(groups)) {
|
for (const [groupId, group] of Object.entries(groups)) {
|
||||||
if (event.on) {
|
if (event.on === true) {
|
||||||
this.groups[groupId].state.any_on = true;
|
this.groups[groupId].state.any_on = true;
|
||||||
this.groups[groupId].state.all_on = Object.values(group.lights).filter((l) => l.state.on).length === Object.values(group.lights).length;
|
this.groups[groupId].state.all_on = Object.values(group.lights).filter((l) => l.state.on).length === Object.values(group.lights).length;
|
||||||
} else {
|
} else if (event.on === false) {
|
||||||
this.groups[groupId].state.all_on = false;
|
this.groups[groupId].state.all_on = false;
|
||||||
this.groups[groupId].state.any_on = Object.values(group.lights).filter((l) => l.state.on).length > 0;
|
this.groups[groupId].state.any_on = Object.values(group.lights).filter((l) => l.state.on).length > 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (var attr of ['on', 'hue', 'sat', 'bri', 'xy', 'ct']) {
|
||||||
|
if (attr in event) {
|
||||||
|
this.lights[event.id].state[attr] = event[attr];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
eventHandler: function(event) {
|
||||||
|
if ('light_id' in event) {
|
||||||
|
this.onUnitInput({
|
||||||
|
...event,
|
||||||
|
id: event.light_id,
|
||||||
|
});
|
||||||
|
} else if ('group_id' in event) {
|
||||||
|
var args = {
|
||||||
|
id: event.group_id,
|
||||||
|
state: {
|
||||||
|
...event,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
if ('on' in event) {
|
||||||
|
args.state.any_on = event.on;
|
||||||
|
args.state.all_on = event.on;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.updatedGroup(args);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
created: function() {
|
created: function() {
|
||||||
this.refresh();
|
this.refresh();
|
||||||
|
registerEventHandler(this.eventHandler, 'platypush.message.event.light.LightStatusChangeEvent');
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -13,6 +13,7 @@
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/axios.min.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/lib/axios.min.js') }}"></script>
|
||||||
|
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/api.js') }}"></script>
|
<script type="text/javascript" src="{{ url_for('static', filename='js/api.js') }}"></script>
|
||||||
|
<script type="text/javascript" src="{{ url_for('static', filename='js/events.js') }}"></script>
|
||||||
|
|
||||||
{% for style in styles.values() %}
|
{% for style in styles.values() %}
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename=style['_style_file']) }}">
|
<link rel="stylesheet" href="{{ url_for('static', filename=style['_style_file']) }}">
|
||||||
|
|
|
@ -1,33 +0,0 @@
|
||||||
<script type="text/javascript" src="{{ url_for('static', filename='js/light.hue.js') }}"></script>
|
|
||||||
<link rel="stylesheet" href="{{ url_for('static', filename='css/light.hue.css') }}"></script>
|
|
||||||
|
|
||||||
{% set default_group = configuration['groups'][0] if 'groups' in configuration else None %}
|
|
||||||
|
|
||||||
<script type="text/javascript">
|
|
||||||
window.config = window.config || {};
|
|
||||||
window.config.light = window.config.light || {};
|
|
||||||
window.config.light.hue = window.config.light.hue || {};
|
|
||||||
{% if default_group %}
|
|
||||||
window.config.light.hue.default_group = '{% print default_group %}';
|
|
||||||
{% else %}
|
|
||||||
window.config.light.hue.default_group = null;
|
|
||||||
{% endif %}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<div id="hue-container" class="row">
|
|
||||||
<div class="three columns">
|
|
||||||
<div class="hue-section-title">Rooms</div>
|
|
||||||
<div id="rooms-list"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="three columns">
|
|
||||||
<div class="hue-section-title">Scenes</div>
|
|
||||||
<div id="scenes-list"></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="six columns">
|
|
||||||
<div class="hue-section-title">Lights</div>
|
|
||||||
<div id="lights-list"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
|
@ -1,4 +1,3 @@
|
||||||
import json
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
from threading import Thread
|
from threading import Thread
|
||||||
|
|
Loading…
Reference in a new issue