forked from platypush/platypush
Frontend plugin for Philips Hue
This commit is contained in:
parent
7dd3bb9915
commit
10a78a1f21
7 changed files with 489 additions and 11 deletions
81
platypush/backend/http/static/css/light.hue.css
Normal file
81
platypush/backend/http/static/css/light.hue.css
Normal 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;
|
||||
}
|
||||
|
159
platypush/backend/http/static/css/toggles.css
Normal file
159
platypush/backend/http/static/css/toggles.css
Normal 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; }
|
||||
|
|
@ -77,8 +77,31 @@ $(document).ready(function() {
|
|||
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() {
|
||||
initWebsocket();
|
||||
initElements();
|
||||
initDateTime();
|
||||
};
|
||||
|
||||
|
|
189
platypush/backend/http/static/js/light.hue.js
Normal file
189
platypush/backend/http/static/js/light.hue.js
Normal 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();
|
||||
});
|
||||
|
|
@ -7,6 +7,7 @@
|
|||
<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='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/skeleton-tabs.js') }}"></script>
|
||||
|
@ -39,24 +40,21 @@
|
|||
|
||||
<main>
|
||||
<ul class="tab-nav">
|
||||
{% set first_plugin = True %}
|
||||
{% for plugin in plugins.keys() %}
|
||||
{% for plugin in plugins.keys()|sort() %}
|
||||
<li>
|
||||
<a class="button {% print('active' if first_plugin else '') %}" href="#{% print plugin %}">
|
||||
<a class="button plugin-tab-item" href="#{% print plugin %}">
|
||||
{% print plugin %}
|
||||
</a>
|
||||
</li>
|
||||
{% set first_plugin = False %}
|
||||
{% endfor %}
|
||||
</ul>
|
||||
|
||||
<div class="tab-content">
|
||||
{% set first_plugin = True %}
|
||||
{% for plugin, configuration in plugins.items() %}
|
||||
<div class="tab-pane {% print('active' if first_plugin else '') %}" id="{% print plugin %}">
|
||||
{% for plugin in plugins.keys()|sort() %}
|
||||
{% set configuration = plugins[plugin] %}
|
||||
<div class="tab-pane plugin-tab-content" id="{% print plugin %}">
|
||||
{% include 'plugins/' + plugin + '.html' %}
|
||||
</div>
|
||||
{% set first_plugin = False %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
</main>
|
||||
|
|
20
platypush/backend/http/templates/plugins/light.hue.html
Normal file
20
platypush/backend/http/templates/plugins/light.hue.html
Normal 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>
|
||||
|
|
@ -65,8 +65,16 @@ class LightHuePlugin(LightPlugin):
|
|||
|
||||
|
||||
def get_scenes(self):
|
||||
scenes = [s.name for s in self.bridge.scenes]
|
||||
# TODO Expand it with custom scenes specified in config.yaml as in #14
|
||||
return Response(output=self.bridge.get_scene())
|
||||
|
||||
|
||||
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):
|
||||
try:
|
||||
|
@ -80,7 +88,7 @@ class LightHuePlugin(LightPlugin):
|
|||
if 'lights' in kwargs and kwargs['lights']:
|
||||
lights = kwargs['lights'].split(',') \
|
||||
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(',') \
|
||||
if isinstance(groups, str) else kwargs['groups']
|
||||
else:
|
||||
|
|
Loading…
Reference in a new issue