Frontend plugin for Philips Hue

This commit is contained in:
Fabio Manganiello 2018-03-27 23:13:42 +02:00
parent 7dd3bb9915
commit 10a78a1f21
7 changed files with 489 additions and 11 deletions

View file

@ -0,0 +1,81 @@
#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-ctrl-switch-container {
float: right;
margin-top: -5px;
}

View file

@ -0,0 +1,159 @@
@import url(https://fonts.googleapis.com/css?family=Francois+One);
@import url(https://fonts.googleapis.com/css?family=PT+Sans);
@font-face {
font-family: 'Audiowide';
font-style: normal;
font-weight: 400;
src: local("Audiowide"), local("Audiowide-Regular"), url(http://themes.googleusercontent.com/static/fonts/audiowide/v2/8XtYtNKEyyZh481XVWfVOj8E0i7KZn-EPnyo3HZu7kw.woff) format("woff"); }
.toggle {
display: inline-block;
text-align: center;
user-select: none; }
.toggle--checkbox {
display: none !important; }
.toggle--btn {
display: block;
margin: 0 auto;
font-size: 1.4em;
transition: all 350ms ease-in; }
.toggle--btn:hover {
cursor: pointer; }
.toggle--btn, .toggle--btn:before, .toggle--btn:after,
.toggle--checkbox,
.toggle--checkbox:before,
.toggle--checkbox:after,
.toggle--feature,
.toggle--feature:before,
.toggle--feature:after {
transition: all 250ms ease-in; }
.toggle--btn:before, .toggle--btn:after,
.toggle--checkbox:before,
.toggle--checkbox:after,
.toggle--feature:before,
.toggle--feature:after {
content: '';
display: block; }
/* =====================================================
Toggle - switch stylee
===================================================== */
.toggle--switch .toggle--btn {
position: relative;
width: 120px;
height: 44px;
font-family: 'PT Sans', Sans Serif;
text-transform: uppercase;
color: #fff;
background: linear-gradient(90deg, #a4bf4d 0%, #a4bf4d 50%, #ca5046 50%, #ca5046 200%);
background-position: -80px 0;
background-size: 200% 100%;
box-shadow: inset 0 0px 22px -8px #111; }
.toggle--switch .toggle--btn, .toggle--switch .toggle--btn:before {
border-radius: 4px; }
.toggle--switch .toggle--btn:before {
display: block;
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
width: 52px;
height: 44px;
border: 2px solid #202027;
background-image: linear-gradient(90deg, transparent 50%, rgba(255, 255, 255, 0.15) 100%);
background-color: #2b2e3a;
background-size: 5px 5px;
text-indent: -100%; }
.toggle--switch .toggle--feature {
position: relative;
display: block;
overflow: hidden;
height: 44px;
text-shadow: 0 1px 2px #666; }
.toggle--switch .toggle--feature:before, .toggle--switch .toggle--feature:after {
position: absolute;
top: 50%;
transform: translateY(-50%); }
.toggle--switch .toggle--feature:before {
content: attr(data-label-on);
left: -60%; }
.toggle--switch .toggle--feature:after {
content: attr(data-label-off);
right: 16%; }
.toggle--switch .toggle--checkbox:checked + .toggle--btn {
background-position: 0 0; }
.toggle--switch .toggle--checkbox:checked + .toggle--btn:before {
left: calc(100% - 52px); }
.toggle--switch .toggle--checkbox:checked + .toggle--btn .toggle--feature:before {
left: 20%; }
.toggle--switch .toggle--checkbox:checked + .toggle--btn .toggle--feature:after {
right: -60%; }
/* ======================================================
Push button toggle
====================================================== */
.toggle--push .toggle--btn {
position: relative;
width: 50px;
height: 50px;
background-color: #f9f8f6;
border-radius: 50%;
box-shadow: 0 5px 10px 0px #333, 0 15px 20px 0px #cccccc; }
.toggle--push .toggle--btn, .toggle--push .toggle--btn:before, .toggle--push .toggle--btn:after {
transition-duration: 150ms; }
.toggle--push .toggle--btn:before {
position: absolute;
left: 50%;
top: 50%;
transform: translate(-50%, -50%);
width: 22.7272727273px;
height: 22.7272727273px;
border-radius: 50%;
background-color: #38ffa0;
box-shadow: inset 0 0 0 5px #ccc, inset 0 0 0 14px #f9f8f6; }
.toggle--push .toggle--btn:after {
position: absolute;
left: 50%;
top: 35%;
transform: translate(-50%, -50%);
width: 4px;
height: 12px;
background-color: #ccc;
box-shadow: 0 0 0 2.5px #f9f8f6; }
.toggle--push .toggle--btn:hover:before {
box-shadow: inset 0 0 0 5px #b3b3b3, inset 0 0 0 14px #f9f8f6; }
.toggle--push .toggle--btn:hover:after {
background-color: #b3b3b3; }
.toggle--push .toggle--checkbox:checked + .toggle--btn {
box-shadow: 0 2px 5px 0px gray, 0 15px 20px 0px transparent; }
.toggle--push .toggle--checkbox:checked + .toggle--btn:before {
box-shadow: inset 0 0 0 5px #38ffa0, inset 0 0 0 14px #f9f8f6; }
.toggle--push .toggle--checkbox:checked + .toggle--btn:after {
background-color: #38ffa0; }
.toggle--push--glow {
background: #111;
padding: 50px 0;
margin-bottom: -50px; }
.toggle--push--glow .toggle--btn {
background-color: #dfdfdf;
box-shadow: 0 5px 10px 0px #333, 0 0 0 3px #444444, 0 0 8px 2px transparent, 0 0 0 6px #919191; }
.toggle--push--glow .toggle--btn:before {
box-shadow: inset 0 0 0 5px #aaa, inset 0 0 0 14px #dfdfdf; }
.toggle--push--glow .toggle--btn:after {
background-color: #aaa;
box-shadow: 0 0 0 2.5px #dfdfdf; }
.toggle--push--glow .toggle--btn:hover:before {
box-shadow: inset 0 0 0 5px #777777, inset 0 0 0 14px #dfdfdf; }
.toggle--push--glow .toggle--btn:hover:after {
background-color: #777777; }
.toggle--push--glow .toggle--checkbox:checked + .toggle--btn {
box-shadow: 0 0px 8px 0 #0072ad, 0 0 0 3px #0094e0, 0 0 30px 0 #0094e0, 0 0 0 6px #777777; }
.toggle--push--glow .toggle--checkbox:checked + .toggle--btn:before {
box-shadow: inset 0 0 0 5px #0094e0, inset 0 0 0 14px #dfdfdf; }
.toggle--push--glow .toggle--checkbox:checked + .toggle--btn:after {
background-color: #0094e0; }

View file

@ -77,8 +77,31 @@ $(document).ready(function() {
eventListeners.push(listener); eventListeners.push(listener);
}; };
var initElements = function() {
var $tabItems = $('main').find('.plugin-tab-item');
var $tabContents = $('main').find('.plugin-tab-content');
// Set the active tag on the first plugin tab
$tabContents.removeClass('active');
$tabContents.first().addClass('active');
$tabItems.removeClass('active');
$tabItems.first().addClass('active');
$tabItems.on('click', function() {
var pluginId = $(this).attr('href').substr(1);
$tabContents.removeClass('active');
$tabContents.filter(function(i, content) {
return $(content).attr('id') === pluginId
}).addClass('active');
});
};
var init = function() { var init = function() {
initWebsocket(); initWebsocket();
initElements();
initDateTime(); initDateTime();
}; };

View file

@ -0,0 +1,189 @@
$(document).ready(function() {
var lights,
groups,
scenes,
$roomsList = $('#rooms-list'),
$lightsList = $('#lights-list'),
$scenesList = $('#scenes-list');
var execute = function(request, onSuccess, onError, onComplete) {
request['target'] = 'localhost';
return $.ajax({
type: 'POST',
url: '/execute',
contentType: 'application/json',
dataType: 'json',
data: JSON.stringify(request),
complete: function() {
if (onComplete) {
onComplete();
}
},
error: function(xhr, status, error) {
if (onError) {
onError(xhr, status, error);
}
},
success: function(response, status, xhr) {
if (onSuccess) {
onSuccess(response, status, xhr);
}
},
beforeSend: function(xhr) {
if (window.token) {
xhr.setRequestHeader('X-Token', window.token);
}
},
});
};
var createPowerToggleElement = function(data) {
var $powerToggle = $('<div></div>').addClass('toggle toggle--push light-ctrl-switch-container');
var $input = $('<input></input>').attr('type', 'checkbox')
.attr('id', 'toggle--push').addClass('toggle--checkbox light-ctrl-switch');
data = data || {};
for (var attr of Object.keys(data)) {
$input.data(attr, data[attr]);
}
var $label = $('<label></label>').attr('for', 'toggle--push').addClass('toggle--btn');
$input.appendTo($powerToggle);
$label.appendTo($powerToggle);
return $powerToggle;
};
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)
.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 = $('<div></div>')
.addClass('light-item')
.data('id', light)
.text(lights[light].name);
var $powerToggle = createPowerToggleElement({
type: 'light',
id: light,
});
roomByLight[light] = room;
$powerToggle.appendTo($light);
$light.appendTo($roomLights);
}
}
for (var scene of Object.keys(scenes)) {
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 refreshStatus = 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);
});
};
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')
}
});
});
};
var init = function() {
refreshStatus();
initBindings();
};
init();
});

View file

@ -7,6 +7,7 @@
<link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}"></script> <link rel="stylesheet" href="{{ url_for('static', filename='css/normalize.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}"></script> <link rel="stylesheet" href="{{ url_for('static', filename='font-awesome/css/font-awesome.min.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}"></script> <link rel="stylesheet" href="{{ url_for('static', filename='css/application.css') }}"></script>
<link rel="stylesheet" href="{{ url_for('static', filename='css/toggles.css') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/jquery-3.3.1.min.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/skeleton-tabs.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/skeleton-tabs.js') }}"></script>
@ -39,24 +40,21 @@
<main> <main>
<ul class="tab-nav"> <ul class="tab-nav">
{% set first_plugin = True %} {% for plugin in plugins.keys()|sort() %}
{% for plugin in plugins.keys() %}
<li> <li>
<a class="button {% print('active' if first_plugin else '') %}" href="#{% print plugin %}"> <a class="button plugin-tab-item" href="#{% print plugin %}">
{% print plugin %} {% print plugin %}
</a> </a>
</li> </li>
{% set first_plugin = False %}
{% endfor %} {% endfor %}
</ul> </ul>
<div class="tab-content"> <div class="tab-content">
{% set first_plugin = True %} {% for plugin in plugins.keys()|sort() %}
{% for plugin, configuration in plugins.items() %} {% set configuration = plugins[plugin] %}
<div class="tab-pane {% print('active' if first_plugin else '') %}" id="{% print plugin %}"> <div class="tab-pane plugin-tab-content" id="{% print plugin %}">
{% include 'plugins/' + plugin + '.html' %} {% include 'plugins/' + plugin + '.html' %}
</div> </div>
{% set first_plugin = False %}
{% endfor %} {% endfor %}
</div> </div>
</main> </main>

View file

@ -0,0 +1,20 @@
<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>
<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>

View file

@ -65,8 +65,16 @@ class LightHuePlugin(LightPlugin):
def get_scenes(self): def get_scenes(self):
scenes = [s.name for s in self.bridge.scenes] return Response(output=self.bridge.get_scene())
# TODO Expand it with custom scenes specified in config.yaml as in #14
def get_lights(self):
return Response(output=self.bridge.get_light())
def get_groups(self):
return Response(output=self.bridge.get_group())
def _exec(self, attr, *args, **kwargs): def _exec(self, attr, *args, **kwargs):
try: try:
@ -80,7 +88,7 @@ class LightHuePlugin(LightPlugin):
if 'lights' in kwargs and kwargs['lights']: if 'lights' in kwargs and kwargs['lights']:
lights = kwargs['lights'].split(',') \ lights = kwargs['lights'].split(',') \
if isinstance(lights, str) else kwargs['lights'] if isinstance(lights, str) else kwargs['lights']
elif 'groups' in kwargs and kwargs['lights']: elif 'groups' in kwargs and kwargs['groups']:
groups = kwargs['groups'].split(',') \ groups = kwargs['groups'].split(',') \
if isinstance(groups, str) else kwargs['groups'] if isinstance(groups, str) else kwargs['groups']
else: else: