From 10a78a1f214f8074aa65a5f395ac50cd8fb031ec Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Tue, 27 Mar 2018 23:13:42 +0200 Subject: [PATCH] Frontend plugin for Philips Hue --- .../backend/http/static/css/light.hue.css | 81 ++++++++ platypush/backend/http/static/css/toggles.css | 159 +++++++++++++++ .../backend/http/static/js/application.js | 23 +++ platypush/backend/http/static/js/light.hue.js | 189 ++++++++++++++++++ platypush/backend/http/templates/index.html | 14 +- .../http/templates/plugins/light.hue.html | 20 ++ platypush/plugins/light/hue/__init__.py | 14 +- 7 files changed, 489 insertions(+), 11 deletions(-) create mode 100644 platypush/backend/http/static/css/light.hue.css create mode 100644 platypush/backend/http/static/css/toggles.css create mode 100644 platypush/backend/http/static/js/light.hue.js create mode 100644 platypush/backend/http/templates/plugins/light.hue.html diff --git a/platypush/backend/http/static/css/light.hue.css b/platypush/backend/http/static/css/light.hue.css new file mode 100644 index 00000000..0e4ce40f --- /dev/null +++ b/platypush/backend/http/static/css/light.hue.css @@ -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; +} + diff --git a/platypush/backend/http/static/css/toggles.css b/platypush/backend/http/static/css/toggles.css new file mode 100644 index 00000000..95e23d07 --- /dev/null +++ b/platypush/backend/http/static/css/toggles.css @@ -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; } + diff --git a/platypush/backend/http/static/js/application.js b/platypush/backend/http/static/js/application.js index 57d13c60..abb5f6b8 100644 --- a/platypush/backend/http/static/js/application.js +++ b/platypush/backend/http/static/js/application.js @@ -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(); }; diff --git a/platypush/backend/http/static/js/light.hue.js b/platypush/backend/http/static/js/light.hue.js new file mode 100644 index 00000000..c10efe08 --- /dev/null +++ b/platypush/backend/http/static/js/light.hue.js @@ -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 = $('
').addClass('toggle toggle--push light-ctrl-switch-container'); + var $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 = $('').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 = $('
') + .addClass('room-item') + .data('id', room) + .text(rooms[room].name); + + var $roomLights = $('
') + .addClass('room-lights-item') + .data('id', room); + + var $roomScenes = $('
') + .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 = $('
') + .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 = $('
') + .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(); +}); + diff --git a/platypush/backend/http/templates/index.html b/platypush/backend/http/templates/index.html index cf74ea8a..281f84e8 100644 --- a/platypush/backend/http/templates/index.html +++ b/platypush/backend/http/templates/index.html @@ -7,6 +7,7 @@ + @@ -39,24 +40,21 @@
- {% set first_plugin = True %} - {% for plugin, configuration in plugins.items() %} -
+ {% for plugin in plugins.keys()|sort() %} + {% set configuration = plugins[plugin] %} +
{% include 'plugins/' + plugin + '.html' %}
- {% set first_plugin = False %} {% endfor %}
diff --git a/platypush/backend/http/templates/plugins/light.hue.html b/platypush/backend/http/templates/plugins/light.hue.html new file mode 100644 index 00000000..1b395874 --- /dev/null +++ b/platypush/backend/http/templates/plugins/light.hue.html @@ -0,0 +1,20 @@ + + + +
+
+
Rooms
+
+
+ +
+
Scenes
+
+
+ +
+
Lights
+
+
+
+ diff --git a/platypush/plugins/light/hue/__init__.py b/platypush/plugins/light/hue/__init__.py index d43b6b61..adbe9010 100644 --- a/platypush/plugins/light/hue/__init__.py +++ b/platypush/plugins/light/hue/__init__.py @@ -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: