Webpanel refactoring in progress

This commit is contained in:
Fabio Manganiello 2019-05-26 03:53:48 +02:00
parent 897338399f
commit 01b111f436
28 changed files with 660 additions and 44 deletions

View File

@ -2,7 +2,6 @@ import os
from flask import Flask from flask import Flask
from platypush.backend.http.utils import HttpUtils
from platypush.backend.http.app.utils import get_routes from platypush.backend.http.app.utils import get_routes

View File

@ -15,5 +15,6 @@
@import 'common/elements/button'; @import 'common/elements/button';
@import 'common/elements/switch'; @import 'common/elements/switch';
@import 'common/elements/range-slider';
@import 'common/elements/slider'; @import 'common/elements/slider';

View File

@ -0,0 +1,54 @@
@supports (--css: variables) {
.input-range-container {
position: relative;
}
input[type="range"].multirange {
padding: 0;
margin: 0;
display: inline-block;
vertical-align: top;
opacity: 1 !important;
&.original {
position: absolute;
&::-webkit-slider-thumb {
position: relative;
z-index: 2;
}
&::-moz-range-thumb {
transform: scale(1); /* FF doesn't apply position it seems */
z-index: 1;
}
}
&::-moz-range-track {
border-color: transparent; /* needed to switch FF to "styleable" control */
}
&.ghost {
position: relative;
background: var(--track-background);
--track-background: linear-gradient(to right,
transparent var(--low), var(--range-color) 0,
var(--range-color) var(--high), transparent 0
) no-repeat 0 45% / 100% 40%;
--range-color: $slider-thumb-bg;
&::-webkit-slider-runnable-track {
background: var(--track-background);
}
&::-moz-range-track {
background: var(--track-background);
}
}
&[disabled]::-webkit-slider-thumb {
display: none;
}
}
}

View File

@ -6,11 +6,6 @@
border-radius: 5px; border-radius: 5px;
background: $slider-bg; background: $slider-bg;
outline: none; outline: none;
opacity: 0.7;
&:hover {
opacity: 1;
}
&::-webkit-slider-thumb { &::-webkit-slider-thumb {
-webkit-appearance: none; -webkit-appearance: none;
@ -26,6 +21,10 @@
display: none; display: none;
} }
&.disabled {
opacity: 0.3;
}
&::-moz-range-thumb { &::-moz-range-thumb {
width: 25px; width: 25px;
height: 25px; height: 25px;

View File

@ -29,8 +29,11 @@ $widths: (
} }
@if $i < 12 { @if $i < 12 {
.col-offset-#{$i} { .col-offset-#{$i}:first-child {
margin-left: (8.66666666667%*$i); margin-left: (8.66666666667%*$i) !important;
}
.col-offset-#{$i}:not(first-child) {
margin-left: 4% + (8.66666666667%*$i) !important;
} }
} }
} }

View File

@ -12,8 +12,11 @@ $default-link-fg: #5f7869 !default;
$selected-bg: #c8ffd0 !default; $selected-bg: #c8ffd0 !default;
$hover-bg: #def6ea !default; $hover-bg: #def6ea !default;
$header-bg: $default_bg !default; $header-bg: $default_bg !default;
$nav-bg: #e8e8e8 !default; $nav-bg: #e8e8e8 !default;
$nav-fg: $default-link-fg; $nav-fg: $default-link-fg;
$nav-date-time-shadow: 2px 2px 2px #ccc !default;
$modal-bg: #f0f0f0 !default; $modal-bg: #f0f0f0 !default;
//// Switch element //// Switch element
@ -42,7 +45,9 @@ $switch-shadow-glow-checked-2: inset 0 0 0 5px #00e094, inset 0 0 0 14px #fff !d
//// Slier element //// Slier element
$slider-bg: #e4e4e4 !default; $slider-bg: #e4e4e4 !default;
$slider-thumb-bg: #4caf50 !default; $slider-thumb-bg: rgba(0,215,80,1.0) !default;
$slider-thumb-disabled-bg: rgba(0,215,80,0.3) !default;
$slider-hover-on-hover-bg: #d2d2d2 !default;
//// Header style //// Header style
$header-bottom: $default-bottom; $header-bottom: $default-bottom;

View File

@ -2,6 +2,7 @@ nav {
margin-bottom: 1.2rem; margin-bottom: 1.2rem;
ul { ul {
position: relative;
margin: 0; margin: 0;
padding: 0; padding: 0;
list-style-type: none; list-style-type: none;
@ -35,6 +36,14 @@ nav {
} }
} }
.date-time {
position: absolute;
right: 0;
margin-right: .7rem;
font-size: 14pt;
text-shadow: $nav-date-time-shadow;
}
.decorator { .decorator {
width: 0; width: 0;
height: 0; height: 0;
@ -55,6 +64,10 @@ nav {
li.selected { li.selected {
border-radius: 2rem; border-radius: 2rem;
} }
.date-time {
@extend .hidden;
}
} }
} }
} }

View File

@ -9,6 +9,16 @@
line-height: 3.8rem; line-height: 3.8rem;
letter-spacing: .1rem; letter-spacing: .1rem;
%panel {
margin: 1.5rem auto;
padding: 1.5rem;
font-weight: 100;
border: $default-border-2;
border-radius: 1.5rem;
background: $light-hue-properties-bg;
box-shadow: $light-hue-properties-shadow;
}
.groups, .groups,
.scenes, .scenes,
.units { .units {
@ -29,6 +39,7 @@
.group, .group,
.scene, .scene,
.unit, .unit,
.animations,
.group-controller { .group-controller {
padding: 1rem; padding: 1rem;
cursor: pointer; cursor: pointer;
@ -48,13 +59,7 @@
} }
* > .properties { * > .properties {
margin: 1.5rem auto; @extend %panel;
padding: 1.5rem;
font-weight: 100;
border: $default-border-2;
border-radius: 1.5rem;
background: $light-hue-properties-bg;
box-shadow: $light-hue-properties-shadow;
.slider-container { .slider-container {
@extend .vertical-center; @extend .vertical-center;
@ -90,6 +95,54 @@
.group-controller { .group-controller {
font-weight: 600; font-weight: 600;
} }
.animations {
.row {
.caption {
font-style: italic;
}
.animation-container {
@extend %panel;
cursor: auto;
.animation {
.row {
padding: 1rem .3333rem;
&:hover {
background: $hover-bg;
border-radius: 1.5rem;
* > input[type=range] {
background: $slider-hover-on-hover-bg;
}
}
}
}
}
select[name=animation-type] {
width: 100%;
}
}
* > .input-range-container {
margin-top: 1rem;
margin-bottom: -1rem;
}
* > input[type="text"] {
width: 100%;
}
&:hover {
.row {
.animation-container {
background: $light-hue-properties-hover-bg;
}
}
}
}
} }
.groups { .groups {

View File

@ -35,11 +35,17 @@ var app = new Vue({
return { return {
config: window.config, config: window.config,
selectedPlugin: undefined, selectedPlugin: undefined,
now: new Date(),
}; };
}, },
mounted: function() {}, mounted: function() {},
created: function() {}, created: function() {
const self = this;
setInterval(() => {
self.now = new Date();
}, 1000)
},
updated: function() {}, updated: function() {},
destroyed: function() {}, destroyed: function() {},
}); });

View File

@ -0,0 +1,89 @@
Vue.component('range-slider', {
template: '#tmpl-range-slider',
props: ['min','max','value'],
mounted: function() {
var input = this.$el.querySelector('input[type=range]');
var supportsMultiple = self.HTMLInputElement && "valueLow" in HTMLInputElement.prototype;
var descriptor = Object.getOwnPropertyDescriptor(HTMLInputElement.prototype, "value");
if (supportsMultiple || input.classList.contains("multirange")) {
return;
}
var values = [];
if (Array.isArray(this.value)) {
values = this.value;
} else if (this.value !== null) {
values = this.value.split(",");
}
var min = +(this.min || 0);
var max = +(this.max || 100);
var ghost = input.cloneNode();
input.classList.add("multirange", "original");
ghost.classList.add("multirange", "ghost");
input.value = values[0] !== undefined ? values[0] : min + (max - min) / 2;
ghost.value = values[1] !== undefined ? values[1] : min + (max - min) / 2;
input.parentNode.insertBefore(ghost, input.nextSibling);
Object.defineProperty(input, "originalValue", descriptor.get ? descriptor : {
// Fuck you Safari >:(
get: function() { return this.value; },
set: function(v) { this.value = v; }
});
Object.defineProperties(input, {
valueLow: {
get: function() { return Math.min(this.originalValue, ghost.value); },
set: function(v) { this.originalValue = v; },
enumerable: true
},
valueHigh: {
get: function() { return Math.max(this.originalValue, ghost.value); },
set: function(v) { ghost.value = v; },
enumerable: true
}
});
if (descriptor.get) {
// Again, fuck you Safari
Object.defineProperty(input, "value", {
get: function() { return this.valueLow + "," + this.valueHigh; },
set: function(v) {
var values = v.split(",");
this.valueLow = values[0];
this.valueHigh = values[1];
update();
},
enumerable: true
});
}
input.oninput = this.changed;
ghost.oninput = this.changed;
function update() {
ghost.style.setProperty("--low", 100 * ((input.valueLow - min) / (max - min)) + 1 + "%");
ghost.style.setProperty("--high", 100 * ((input.valueHigh - min) / (max - min)) - 1 + "%");
}
input.addEventListener("input", update);
ghost.addEventListener("input", update);
update();
},
methods: {
changed: function(event) {
const value = this.$el.querySelectorAll('input[type=range]')[0].value
.split(',').map(_ => parseFloat(_));
this.$emit('changed', value);
},
},
});

View File

@ -0,0 +1,39 @@
Vue.component('light-hue-animations-container', {
template: '#tmpl-light-hue-animations-container',
props: ['groupId','animation','collapsed'],
data: function() {
return {
selectedAnimation: 'color_transition',
};
},
methods: {
animationsCollapsedToggled: function() {
this.$emit('animations-collapsed-toggled', {
type: 'animation',
id: this.groupId,
});
},
toggled: async function(event) {
if (event.value) {
var args = {
...this.$refs[this.selectedAnimation].value,
animation: this.selectedAnimation,
groups: [this.groupId],
}
await request('light.hue.on', {groups: [this.groupId]});
await request('light.hue.animate', args);
this.$emit('animation-started', {
...this.$refs[this.selectedAnimation].value,
type: this.selectedAnimation,
});
} else {
await request('light.hue.stop_animation');
this.$emit('animation-stopped', {});
}
},
},
});

View File

@ -0,0 +1,28 @@
Vue.component('light-hue-animation-blink', {
template: '#tmpl-light-hue-animation-blink',
data: function() {
return {
value: {
transition_seconds: 1,
duration: undefined,
},
transitionSecondsRange: [0.1, 60],
durationRange: [0, 600],
};
},
methods: {
onTransitionSecondsChange: function(event) {
this.value.transition_seconds = event.target.value;
},
onDurationChanged: function(event) {
var value = event.target.value;
if (value == null || value.length === 0 || parseFloat(value) == 0) {
value = undefined;
}
this.value.duration = value;
},
},
});

View File

@ -0,0 +1,59 @@
Vue.component('light-hue-animation-color_transition', {
template: '#tmpl-light-hue-animation-color_transition',
data: function() {
return {
value: {
hue_range: [0,65535],
sat_range: [150,255],
bri_range: [190,255],
hue_step: 150,
sat_step: 5,
bri_step: 2,
transition_seconds: 1,
duration: undefined,
},
};
},
computed: {
hueStepRange: function() {
return [1, parseInt((this.value.hue_range[1]-this.value.hue_range[0])/2)-1];
},
satStepRange: function() {
return [1, parseInt((this.value.sat_range[1]-this.value.sat_range[0])/2)-1];
},
briStepRange: function() {
return [1, parseInt((this.value.bri_range[1]-this.value.bri_range[0])/2)-1];
},
transitionSecondsRange: function() {
return [0.1, 60];
},
durationRange: function() {
return [0, 600];
},
},
methods: {
hueRangeChanged: function(value) {
this.value.hue_range = value;
},
satRangeChanged: function(value) {
this.value.sat_range = value;
},
briRangeChanged: function(value) {
this.value.bri_range = value;
},
onTransitionSecondsChange: function(event) {
this.value.transition_seconds = event.target.value;
},
onDurationChanged: function(event) {
var value = event.target.value;
if (value == null || value.length === 0 || parseFloat(value) == 0) {
value = undefined;
}
this.value.duration = value;
},
},
});

View File

@ -6,6 +6,7 @@ Vue.component('light-hue', {
groups: {}, groups: {},
lights: {}, lights: {},
scenes: {}, scenes: {},
animations: {},
selectedGroup: undefined, selectedGroup: undefined,
selectedScene: undefined, selectedScene: undefined,
selectedProperties: { selectedProperties: {
@ -89,8 +90,10 @@ Vue.component('light-hue', {
const getLights = request('light.hue.get_lights'); const getLights = request('light.hue.get_lights');
const getGroups = request('light.hue.get_groups'); const getGroups = request('light.hue.get_groups');
const getScenes = request('light.hue.get_scenes'); const getScenes = request('light.hue.get_scenes');
const getAnimations = request('light.hue.get_animations');
[this.lights, this.groups, this.scenes] = await Promise.all([getLights, getGroups, getScenes]); [this.lights, this.groups, this.scenes, this.animations] = await Promise.all(
[getLights, getGroups, getScenes, getAnimations]);
this._prepareGroups(); this._prepareGroups();
this._prepareScenes(); this._prepareScenes();
@ -112,6 +115,14 @@ Vue.component('light-hue', {
} }
}, },
startedAnimation: function(value) {
this.animations.groups[this.selectedGroup] = value;
},
stoppedAnimation: function() {
this.animations.groups[this.selectedGroup] = undefined;
},
selectScene: async function(event) { selectScene: async function(event) {
await request( await request(
'light.hue.scene', { 'light.hue.scene', {

View File

@ -1,2 +1,3 @@
{% include 'elements/switch.html' %} {% include 'elements/switch.html' %}
{% include 'elements/range-slider.html' %}

View File

@ -0,0 +1,8 @@
<script type="text/x-template" id="tmpl-range-slider">
<div class="input-range-container">
<input type="range" class="slider" multiple="multiple" :value="value" :min="min" :max="max" @input="changed">
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/elements/range-slider.js') }}"></script>

View File

@ -5,3 +5,5 @@
</div> </div>
</script> </script>
<script type="application/javascript" src="{{ url_for('static', filename='js/elements/switch.js') }}"></script>

View File

@ -1,15 +1,15 @@
<script type="text/x-template" id="tmpl-app-header"> <script type="text/x-template" id="tmpl-app-header">
<header class="s-hidden m-hidden"> <header class="s-hidden m-hidden">
<div class="row"> <div class="row">
<div class="logo col-9"> <div class="logo col-9">
<span class="logo-1">Platypush</span> <span class="logo-1">Platypush</span>
<span class="logo-2">Web Panel</span> <span class="logo-2">Web Panel</span>
</div> </div>
<div class="date-time col-3"> <div class="date-time col-3">
<div class="date" v-text="now.toDateString().substring(0,10)"></div> <div class="date" v-text="now.toDateString().substring(0,10)"></div>
<div class="time" v-text="now.toTimeString().substring(0,8)"></div> <div class="time" v-text="now.toTimeString().substring(0,8)"></div>
</div> </div>
</div> </div>
</header> </header>
</script> </script>

View File

@ -42,7 +42,7 @@
<body> <body>
<div id="app"> <div id="app">
{% include 'header.html' %} {# include 'header.html' #}
{% with plugins=templates.keys() %} {% with plugins=templates.keys() %}
{% include 'nav.html' %} {% include 'nav.html' %}
@ -67,7 +67,6 @@
<script type="text/javascript" src="{{ url_for('static', filename=script['_script_file']) }}"></script> <script type="text/javascript" src="{{ url_for('static', filename=script['_script_file']) }}"></script>
{% endfor %} {% endfor %}
<script type="text/javascript" src="{{ url_for('static', filename='js/elements.js') }}"></script>
<script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script> <script type="text/javascript" src="{{ url_for('static', filename='js/application.js') }}"></script>
</body> </body>

View File

@ -1,5 +1,5 @@
<nav> <nav>
<ul class="row"> <ul>
{% for plugin in plugins|sort %} {% for plugin in plugins|sort %}
<li :class="{selected: '{{ plugin }}' == selectedPlugin}"> <li :class="{selected: '{{ plugin }}' == selectedPlugin}">
<a href="#{{ plugin }}" @click="selectedPlugin = '{{ plugin }}'"> <a href="#{{ plugin }}" @click="selectedPlugin = '{{ plugin }}'">
@ -15,6 +15,11 @@
<li> <li>
<a href="#{{ plugin }}">Test tab 3</a> <a href="#{{ plugin }}">Test tab 3</a>
</li> </li>
<div class="date-time pull-right">
<!--<div class="date" v-text="now.toDateString().substring(0,10)"></div>-->
<div class="time" v-text="now.toTimeString().substring(0,8)"></div>
</div>
</ul> </ul>
</nav> </nav>

View File

@ -0,0 +1,41 @@
{% with templates = utils.find_templates_in_dir('plugins/light.hue/animations') %}
{% for template in templates %}
{% include template %}
{% endfor %}
{% endwith %}
<script type="text/x-template" id="tmpl-light-hue-animations-container">
<div class="animations">
<div class="row vertical-center">
<div class="col-10 caption" @click="animationsCollapsedToggled">Animate</div>
<div class="col-2 pull-right">
<toggle-switch :glow="true" :value="animation != null" @toggled="toggled"></toggle-switch>
</div>
</div>
<div class="row" :class="{hidden: collapsed}">
<div class="row">
{% with templates = utils.find_templates_in_dir('plugins/light.hue/animations') %}
<select name="animation-type" v-model="selectedAnimation"
v-if="{{ templates|length }} > 0">
{% for template_file in templates %}
{% with name = template_file.split('/')[-1].split('.')[0] %}
<option value="{{ name }}">{{ name }}</option>
{% endwith %}
{% endfor %}
</select>
</div>
{% for template_file in templates %}
{% with name = template_file.split('/')[-1].split('.')[0] %}
<div class="animation-container" :class="{hidden: selectedAnimation != '{{name}}'}">
<component :is="'light-hue-animation-{{ name }}'" ref="{{ name }}"></component>
</div>
{% endwith %}
{% endfor %}
{% endwith %}
</div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/animations.js') }}"></script>

View File

@ -0,0 +1,28 @@
<script type="text/x-template" id="tmpl-light-hue-animation-blink">
<div class="animation">
<div class="row">
<div class="col-4">Transition</div>
<div class="col-2">
<input type="text" v-model="value.transition_seconds">
</div>
<div class="col-6 slider-container">
<input class="slider" type="range" :min="transitionSecondsRange[0]" :max="transitionSecondsRange[1]"
v-model="value.transition_seconds" @input="onTransitionSecondsChange" placeholder="secs">
</div>
</div>
<div class="row">
<div class="col-4">Duration</div>
<div class="col-2">
<input type="text" v-model="value.duration" @input="onDurationChanged">
</div>
<div class="col-6 slider-container">
<input class="slider" type="range" step="1" :min="durationRange[0]" :max="durationRange[1]" placeholder="secs"
:class="{disabled: value.duration == undefined}" v-model="value.duration" @input="onDurationChanged">
</div>
</div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/animations/blink.js') }}"></script>

View File

@ -0,0 +1,73 @@
<script type="text/x-template" id="tmpl-light-hue-animation-color_transition">
<div class="animation">
<div class="row">
<div class="col-4">Hue range</div>
<div class="col-8">
<range-slider min="0" max="65535" v-model="value.hue_range" @changed="hueRangeChanged"></range-slider>
</div>
</div>
<div class="row">
<div class="col-4">Sat range</div>
<div class="col-8">
<range-slider min="0" max="255" v-model="value.sat_range" @changed="satRangeChanged"></range-slider>
</div>
</div>
<div class="row">
<div class="col-4">Bri range</div>
<div class="col-8">
<range-slider min="0" max="255" v-model="value.bri_range" @changed="briRangeChanged"></range-slider>
</div>
</div>
<div class="row">
<div class="col-4">Hue step</div>
<div class="col-8 slider-container">
<input class="slider" type="range" :min="hueStepRange[0]" :max="hueStepRange[1]" step="1"
v-model="value.hue_step">
</div>
</div>
<div class="row">
<div class="col-4">Sat step</div>
<div class="col-8 slider-container">
<input class="slider" type="range" :min="satStepRange[0]" :max="satStepRange[1]" step="1"
v-model="value.sat_step">
</div>
</div>
<div class="row">
<div class="col-4">Bri step</div>
<div class="col-8 slider-container">
<input class="slider" type="range" step="1" :min="briStepRange[0]" :max="briStepRange[1]"
v-model="value.bri_step">
</div>
</div>
<div class="row">
<div class="col-4">Transition</div>
<div class="col-2">
<input type="text" v-model="value.transition_seconds">
</div>
<div class="col-6 slider-container">
<input class="slider" type="range" :min="transitionSecondsRange[0]" :max="transitionSecondsRange[1]"
v-model="value.transition_seconds" @input="onTransitionSecondsChange" placeholder="secs">
</div>
</div>
<div class="row">
<div class="col-4">Duration</div>
<div class="col-2">
<input type="text" v-model="value.duration" @input="onDurationChanged">
</div>
<div class="col-6 slider-container">
<input class="slider" type="range" step="1" :min="durationRange[0]" :max="durationRange[1]" placeholder="secs"
:class="{disabled: value.duration == undefined}" v-model="value.duration" @input="onDurationChanged">
</div>
</div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/light.hue/animations/color_transition.js') }}"></script>

View File

@ -4,6 +4,7 @@
{% include 'plugins/light.hue/groups.html' %} {% include 'plugins/light.hue/groups.html' %}
{% include 'plugins/light.hue/scenes.html' %} {% include 'plugins/light.hue/scenes.html' %}
{% include 'plugins/light.hue/units.html' %} {% include 'plugins/light.hue/units.html' %}
{% include 'plugins/light.hue/animations.html' %}
<script type="text/x-template" id="tmpl-light-hue"> <script type="text/x-template" id="tmpl-light-hue">
<div class="row light-hue-container"> <div class="row light-hue-container">
@ -33,6 +34,7 @@
<div class="units col-no-margin-6 col-s-12"> <div class="units col-no-margin-6 col-s-12">
<div class="title">Lights</div> <div class="title">Lights</div>
<light-hue-group-controller <light-hue-group-controller
v-if="selectedGroup" v-if="selectedGroup"
v-model="groups[selectedGroup]" v-model="groups[selectedGroup]"
@ -43,6 +45,16 @@
@input="updatedGroup"> @input="updatedGroup">
</light-hue-group-controller> </light-hue-group-controller>
<light-hue-animations-container
v-if="selectedGroup"
:group-id="selectedGroup"
:animation="animations.groups[selectedGroup]"
:collapsed="!(selectedProperties.type == 'animation' && selectedProperties.id == selectedGroup)"
@animations-collapsed-toggled="collapsedToggled"
@animation-started="startedAnimation"
@animation-stopped="stoppedAnimation">
</light-hue-animations-container>
<light-hue-unit <light-hue-unit
v-for="(light, id) in lights" v-for="(light, id) in lights"
v-model="light.state" v-model="light.state"

View File

@ -3,6 +3,8 @@ import os
import re import re
from platypush.config import Config from platypush.config import Config
from platypush.backend.http.app import template_folder
class HttpUtils(object): class HttpUtils(object):
@staticmethod @staticmethod
@ -100,5 +102,19 @@ class HttpUtils(object):
def plugin_name_to_tag(cls, module_name): def plugin_name_to_tag(cls, module_name):
return module_name.replace('.','-') return module_name.replace('.','-')
@classmethod
def find_templates_in_dir(cls, directory):
return [
os.path.join(directory, file)
for root, path, files in os.walk(os.path.abspath(os.path.join(template_folder, directory)))
for file in files
if file.endswith('.html') or file.endswith('.htm')
]
@classmethod
def readfile(cls, file):
with open(file) as f:
return f.read()
# vim:sw=4:ts=4:et: # vim:sw=4:ts=4:et:

View File

@ -70,12 +70,26 @@ class LightHuePlugin(LightPlugin):
self.redis = None self.redis = None
self.animation_thread = None self.animation_thread = None
self.animations = {}
self._init_animations()
self.logger.info('Configured lights: "{}"'. format(self.lights)) self.logger.info('Configured lights: "{}"'. format(self.lights))
def _expand_groups(self): def _expand_groups(self):
groups = [g for g in self.bridge.groups if g.name in self.groups] groups = [g for g in self.bridge.groups if g.name in self.groups]
for g in groups: for g in groups:
self.lights.extend([l.name for l in g.lights]) for l in g.lights:
self.lights += [l.name]
def _init_animations(self):
self.animations = {
'groups': {},
'lights': {},
}
for g in self.bridge.groups:
self.animations['groups'][g.group_id] = None
for l in self.bridge.lights:
self.animations['lights'][l.light_id] = None
@action @action
def connect(self): def connect(self):
@ -239,6 +253,36 @@ class LightHuePlugin(LightPlugin):
return self.bridge.get_group() return self.bridge.get_group()
@action
def get_animations(self):
"""
Get the list of running light animations.
:returns: A dictionary with the following structure:
{
"groups": {
"id_1": {
"type": "color_transition",
"hue_range": [0,65535],
"sat_range": [0,255],
"bri_range": [0,255],
"hue_step": 10,
"sat_step": 10,
"bri_step": 2,
"transition_seconds": 2
},
...
},
"lights": {
"id_1": { ... },
...
}
}
"""
return self.animations
def _exec(self, attr, *args, **kwargs): def _exec(self, attr, *args, **kwargs):
try: try:
self.connect() self.connect()
@ -654,6 +698,7 @@ class LightHuePlugin(LightPlugin):
if self.animation_thread and self.animation_thread.is_alive(): if self.animation_thread and self.animation_thread.is_alive():
redis = self._get_redis() redis = self._get_redis()
redis.rpush(self.ANIMATION_CTRL_QUEUE_NAME, 'STOP') redis.rpush(self.ANIMATION_CTRL_QUEUE_NAME, 'STOP')
self._init_animations()
@action @action
def animate(self, animation, duration=None, def animate(self, animation, duration=None,
@ -669,27 +714,27 @@ class LightHuePlugin(LightPlugin):
:param duration: Animation duration in seconds (default: None, i.e. continue until stop) :param duration: Animation duration in seconds (default: None, i.e. continue until stop)
:type duration: float :type duration: float
:param hue_range: If you selected a color transition, this will specify the hue range of your color transition. :param hue_range: If you selected a color color_transition.html, this will specify the hue range of your color color_transition.html.
Default: [0, 65535] Default: [0, 65535]
:type hue_range: list[int] :type hue_range: list[int]
:param sat_range: If you selected a color transition, this will specify the saturation range of your color :param sat_range: If you selected a color color_transition.html, this will specify the saturation range of your color
transition. Default: [0, 255] color_transition.html. Default: [0, 255]
:type sat_range: list[int] :type sat_range: list[int]
:param bri_range: If you selected a color transition, this will specify the brightness range of your color :param bri_range: If you selected a color color_transition.html, this will specify the brightness range of your color
transition. Default: [254, 255] :type bri_range: list[int] color_transition.html. Default: [254, 255] :type bri_range: list[int]
:param lights: Lights to control (names or light objects). Default: plugin default lights :param lights: Lights to control (names, IDs or light objects). Default: plugin default lights
:param groups: Groups to control (names or group objects). Default: plugin default groups :param groups: Groups to control (names, IDs or group objects). Default: plugin default groups
:param hue_step: If you selected a color transition, this will specify by how much the color hue will change :param hue_step: If you selected a color color_transition.html, this will specify by how much the color hue will change
between iterations. Default: 1000 :type hue_step: int between iterations. Default: 1000 :type hue_step: int
:param sat_step: If you selected a color transition, this will specify by how much the saturation will change :param sat_step: If you selected a color color_transition.html, this will specify by how much the saturation will change
between iterations. Default: 2 :type sat_step: int between iterations. Default: 2 :type sat_step: int
:param bri_step: If you selected a color transition, this will specify by how much the brightness will change :param bri_step: If you selected a color color_transition.html, this will specify by how much the brightness will change
between iterations. Default: 1 :type bri_step: int between iterations. Default: 1 :type bri_step: int
:param transition_seconds: Time between two transitions or blinks in seconds. Default: 1.0 :param transition_seconds: Time between two transitions or blinks in seconds. Default: 1.0
@ -703,13 +748,34 @@ class LightHuePlugin(LightPlugin):
if hue_range is None: if hue_range is None:
hue_range = [0, self.MAX_HUE] hue_range = [0, self.MAX_HUE]
if groups: if groups:
groups = [g for g in self.bridge.groups if g.name in groups] groups = [g for g in self.bridge.groups if g.name in groups or g.group_id in groups]
lights = lights or [] lights = lights or []
for g in groups: for g in groups:
lights.extend([l.name for l in g.lights]) lights.extend([l.name for l in g.lights])
elif not lights: elif lights:
lights = [l.name for l in self.bridge.lights if l.name in lights or l.light_id in lights]
else:
lights = self.lights lights = self.lights
info = {
'type': animation,
'duration': duration,
'hue_range': hue_range,
'sat_range': sat_range,
'bri_range': bri_range,
'hue_step': hue_step,
'sat_step': sat_step,
'bri_step': bri_step,
'transition_seconds': transition_seconds,
}
for g in groups:
self.animations['groups'][g.group_id] = info
for l in self.bridge.lights:
if l.name in lights:
self.animations['lights'][l.light_id] = info
def _initialize_light_attrs(lights): def _initialize_light_attrs(lights):
if animation == self.Animation.COLOR_TRANSITION: if animation == self.Animation.COLOR_TRANSITION:
return { l: { return { l: {

View File

@ -2,6 +2,7 @@
import errno import errno
import os import os
import re
import distutils.cmd import distutils.cmd
from distutils.command.build import build from distutils.command.build import build
from setuptools import setup, find_packages from setuptools import setup, find_packages
@ -15,8 +16,8 @@ class WebBuildCommand(distutils.cmd.Command):
description = 'Build components and styles for the web pages' description = 'Build components and styles for the web pages'
user_options = [] user_options = []
@staticmethod @classmethod
def generate_css_files(): def generate_css_files(cls):
from scss import Compiler from scss import Compiler
print('Building CSS files') print('Building CSS files')
@ -36,8 +37,13 @@ class WebBuildCommand(distutils.cmd.Command):
with open(css_file, 'w') as f: with open(css_file, 'w') as f:
css_content = Compiler(output_style='compressed', search_path=[root, input_path]).compile(scss_file) css_content = Compiler(output_style='compressed', search_path=[root, input_path]).compile(scss_file)
css_content = cls._fix_css4_vars(css_content)
f.write(css_content) f.write(css_content)
@staticmethod
def _fix_css4_vars(css):
return re.sub(r'var\("--([^"]+)"\)', r'var(--\1)', css)
def initialize_options(self): def initialize_options(self):
pass pass