Added Zigbee web panel (closes #123)
This commit is contained in:
parent
0643b7fade
commit
2d3c61173d
18 changed files with 1306 additions and 15 deletions
|
@ -0,0 +1,264 @@
|
||||||
|
@import 'common/vars';
|
||||||
|
@import 'webpanel/plugins/zigbee.mqtt/vars';
|
||||||
|
|
||||||
|
.fa.fa-zigbee:before {
|
||||||
|
content: ' ';
|
||||||
|
background: url('/static/img/icons/zigbee-logo.svg');
|
||||||
|
background-size: 1em 1em;
|
||||||
|
width: 1em;
|
||||||
|
height: 1em;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zigbee-container {
|
||||||
|
height: 100%;
|
||||||
|
padding: 0 .5em;
|
||||||
|
background: $container-bg;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
overflow: auto;
|
||||||
|
|
||||||
|
.no-items {
|
||||||
|
padding: 2em;
|
||||||
|
font-size: 1.5em;
|
||||||
|
color: $no-items-color;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view-options {
|
||||||
|
display: flex;
|
||||||
|
width: 100%;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 1em 0;
|
||||||
|
|
||||||
|
.view-selector {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-default {
|
||||||
|
border: 0;
|
||||||
|
padding: 0 1em;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
border: $default-border-2;
|
||||||
|
border-radius: 1em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.buttons {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.view {
|
||||||
|
min-width: 400pt;
|
||||||
|
max-width: 750pt;
|
||||||
|
background: $view-bg;
|
||||||
|
border: $view-border;
|
||||||
|
border-radius: 1.5em;
|
||||||
|
box-shadow: $view-box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.item {
|
||||||
|
&.selected {
|
||||||
|
box-shadow: $selected-item-box-shadow;
|
||||||
|
}
|
||||||
|
|
||||||
|
.name {
|
||||||
|
padding: 1em;
|
||||||
|
cursor: pointer;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: .06em;
|
||||||
|
|
||||||
|
&.selected {
|
||||||
|
border-radius: 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $hover-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:not(:last-child) {
|
||||||
|
border-bottom: $item-border;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:first-child {
|
||||||
|
border-radius: 1.5em 1.5em 0 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:last-child {
|
||||||
|
border-radius: 0 0 1.5em 1.5em;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.params {
|
||||||
|
background: $params-bg;
|
||||||
|
padding-bottom: 1em;
|
||||||
|
|
||||||
|
.section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 0 1em;
|
||||||
|
|
||||||
|
&:not(:first-child) {
|
||||||
|
padding-top: 1em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-weight: bold;
|
||||||
|
border-bottom: $param-section-header-border;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
border-radius: 1em;
|
||||||
|
padding: .3em;
|
||||||
|
|
||||||
|
&:nth-child(even) {
|
||||||
|
background: $param-even-row-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:nth-child(odd) {
|
||||||
|
background: $param-odd-row-bg;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: $hover-bg;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-name {
|
||||||
|
display: inline-flex;
|
||||||
|
width: 40%;
|
||||||
|
margin-left: 1%;
|
||||||
|
vertical-align: top;
|
||||||
|
letter-spacing: .04em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value {
|
||||||
|
display: inline-block;
|
||||||
|
width: 58%;
|
||||||
|
text-align: right;
|
||||||
|
|
||||||
|
.value-edit {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-data {
|
||||||
|
display: inline-block;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
|
||||||
|
.slider-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.unit {
|
||||||
|
font-size: .8em;
|
||||||
|
margin-left: 1em;
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
width: 100%;
|
||||||
|
border-radius: 2em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.numeric {
|
||||||
|
input.slider {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
input[type=text] {
|
||||||
|
text-align: right;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
background: none;
|
||||||
|
&:hover {
|
||||||
|
background: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-min, .value-max {
|
||||||
|
width: 50%;
|
||||||
|
font-size: .85em;
|
||||||
|
opacity: .75;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-min {
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.value-max {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn-value-name-edit {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.modal {
|
||||||
|
.section {
|
||||||
|
.header {
|
||||||
|
background: none;
|
||||||
|
padding: .5em 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.body {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.network-info {
|
||||||
|
min-width: 600pt;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.error {
|
||||||
|
color: $error-color;
|
||||||
|
}
|
||||||
|
|
||||||
|
.device, .group {
|
||||||
|
.actions {
|
||||||
|
.row {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
form {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.param-value {
|
||||||
|
input[type=text] {
|
||||||
|
text-align: right;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -0,0 +1,13 @@
|
||||||
|
$container-bg: #f1f1f1;
|
||||||
|
$view-bg: white;
|
||||||
|
$view-border: 1px solid #d8d8d8;
|
||||||
|
$view-box-shadow: 1px 2px 2px #ccc;
|
||||||
|
$item-border: 1px solid #dddddd;
|
||||||
|
$no-items-color: #555555;
|
||||||
|
$params-bg: white;
|
||||||
|
$param-even-row-bg: #ededed;
|
||||||
|
$param-odd-row-bg: white;
|
||||||
|
$param-section-header-border: 1px solid #e8e8e8;
|
||||||
|
$selected-item-box-shadow: 0 2px 4px 0 #bbb;
|
||||||
|
$error-color: #aa0000;
|
||||||
|
|
5
platypush/backend/http/static/img/icons/zigbee-logo.svg
Normal file
5
platypush/backend/http/static/img/icons/zigbee-logo.svg
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
<svg role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24">
|
||||||
|
<style>.color {fill: #5f7869}</style>
|
||||||
|
<title>Zigbee icon</title>
|
||||||
|
<path class="color" d="M11.988 0a11.85 11.85 0 00-8.617 3.696c7.02-.875 11.401-.583 13.289-.34 3.752.583 3.558 3.404 3.558 3.404L8.237 19.112c2.299.22 6.897.366 13.796-.631a11.86 11.86 0 001.912-6.469C23.945 5.374 18.595 0 11.988 0zm.232 4.31c-2.451-.014-5.772.146-9.963.723C.854 7.003.055 9.41.055 12.012.055 18.626 5.38 24 11.988 24c3.63 0 6.85-1.63 9.053-4.182-7.286.948-11.813.631-13.75.388-3.775-.56-3.557-3.404-3.557-3.404L15.691 4.474a38.635 38.635 0 00-3.471-.163Z"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 632 B |
|
@ -14,6 +14,11 @@ Vue.component('dropdown', {
|
||||||
type: Array,
|
type: Array,
|
||||||
default: [],
|
default: [],
|
||||||
},
|
},
|
||||||
|
|
||||||
|
classes: {
|
||||||
|
type: Array,
|
||||||
|
default: () => [],
|
||||||
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -38,7 +43,7 @@ let clickHndl = function(event) {
|
||||||
|
|
||||||
var element = event.target;
|
var element = event.target;
|
||||||
while (element) {
|
while (element) {
|
||||||
if (element == openedDropdown) {
|
if (element === openedDropdown) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
127
platypush/backend/http/static/js/plugins/zigbee.mqtt/device.js
Normal file
127
platypush/backend/http/static/js/plugins/zigbee.mqtt/device.js
Normal file
|
@ -0,0 +1,127 @@
|
||||||
|
Vue.component('zigbee-device', {
|
||||||
|
template: '#tmpl-zigbee-device',
|
||||||
|
props: ['device','bus','selected'],
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
newPropertyName: '',
|
||||||
|
editMode: {
|
||||||
|
name: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
onDeviceClicked: function() {
|
||||||
|
this.bus.$emit('deviceClicked', {
|
||||||
|
deviceId: this.device.friendly_name,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setValue: async function(event) {
|
||||||
|
let name = undefined;
|
||||||
|
if (this.newPropertyName && this.newPropertyName.length) {
|
||||||
|
name = this.newPropertyName;
|
||||||
|
} else {
|
||||||
|
name = event.event
|
||||||
|
? event.event.target.parentElement.dataset.name
|
||||||
|
: event.target.dataset.name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!name || !name.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const target = event.event
|
||||||
|
? event.event.target.parentElement.querySelector('input')
|
||||||
|
: event.target;
|
||||||
|
|
||||||
|
const value = target.getAttribute('type') === 'checkbox'
|
||||||
|
? (target.checked ? 'OFF' : 'ON')
|
||||||
|
: target.value;
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.device_set', {
|
||||||
|
device: this.device.friendly_name,
|
||||||
|
property: name,
|
||||||
|
value: value,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.newPropertyName && this.newPropertyName.length) {
|
||||||
|
this.newPropertyName = '';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.bus.$emit('refreshDevices');
|
||||||
|
},
|
||||||
|
|
||||||
|
removeDevice: async function(force=false) {
|
||||||
|
if (!confirm('Are you sure that you want to remove this device?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.device_remove', {
|
||||||
|
device: this.device.friendly_name,
|
||||||
|
force: force,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bus.$emit('refreshDevices');
|
||||||
|
},
|
||||||
|
|
||||||
|
banDevice: async function() {
|
||||||
|
if (!confirm('Are you sure that you want to ban this device?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.device_ban', {
|
||||||
|
device: this.device.friendly_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bus.$emit('refreshDevices');
|
||||||
|
},
|
||||||
|
|
||||||
|
whitelistDevice: async function() {
|
||||||
|
if (!confirm('Are you sure that you want to whitelist this device? Note: ALL the other non-whitelisted ' +
|
||||||
|
'devices will be removed from the network')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.device_whitelist', {
|
||||||
|
device: this.device.friendly_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bus.$emit('refreshDevices');
|
||||||
|
},
|
||||||
|
|
||||||
|
disableForm: function(form) {
|
||||||
|
form.querySelector('input,button').readOnly = true;
|
||||||
|
},
|
||||||
|
|
||||||
|
enableForm: function(form) {
|
||||||
|
form.querySelector('input,button').readOnly = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
onEditMode: function(mode) {
|
||||||
|
Vue.set(this.editMode, mode, true);
|
||||||
|
const form = this.$refs[mode + 'Form'];
|
||||||
|
const input = form.querySelector('input[type=text]');
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
input.focus();
|
||||||
|
input.select();
|
||||||
|
}, 10);
|
||||||
|
},
|
||||||
|
|
||||||
|
editName: async function(event) {
|
||||||
|
this.disableForm(event.target);
|
||||||
|
const name = event.target.querySelector('input[name=name]').value;
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.device_rename', {
|
||||||
|
device: this.device.friendly_name,
|
||||||
|
name: name,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.editMode.name = false;
|
||||||
|
this.enableForm(event.target);
|
||||||
|
this.bus.$emit('refreshDevices');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
149
platypush/backend/http/static/js/plugins/zigbee.mqtt/group.js
Normal file
149
platypush/backend/http/static/js/plugins/zigbee.mqtt/group.js
Normal file
|
@ -0,0 +1,149 @@
|
||||||
|
Vue.component('zigbee-group', {
|
||||||
|
template: '#tmpl-zigbee-group',
|
||||||
|
props: ['group','bus','selected'],
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
properties: {},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
anyOn: async function() {
|
||||||
|
for (const dev of this.group.devices) {
|
||||||
|
const params = await request('zigbee.mqtt.device_get', {device: dev.friendly_name});
|
||||||
|
if (params.state === 'ON') {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
},
|
||||||
|
|
||||||
|
allOn: async function() {
|
||||||
|
for (const dev of this.group.devices) {
|
||||||
|
const params = await request('zigbee.mqtt.device_get', {device: dev.friendly_name});
|
||||||
|
if (params.state === 'OFF') {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshProperties: async function() {
|
||||||
|
const props = {};
|
||||||
|
|
||||||
|
for (const dev of this.group.devices) {
|
||||||
|
const params = await request('zigbee.mqtt.device_get', {device: dev.friendly_name});
|
||||||
|
for (const [name, value] of Object.entries(params)) {
|
||||||
|
if (name === 'linkquality') {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (name in props) {
|
||||||
|
props[name].push(value);
|
||||||
|
} else {
|
||||||
|
props[name] = [value];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const [name, values] of Object.entries(props)) {
|
||||||
|
if (name === 'state') {
|
||||||
|
props[name] = values.filter((value) => value === 'ON').length > 0;
|
||||||
|
} else if (!isNaN(values[0])) {
|
||||||
|
props[name] = values.reduce((sum, value) => sum + value, 0) / values.length;
|
||||||
|
} else {
|
||||||
|
props[name] = values[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.properties = props;
|
||||||
|
},
|
||||||
|
|
||||||
|
onGroupClicked: function() {
|
||||||
|
this.bus.$emit('groupClicked', {
|
||||||
|
groupId: this.group.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
setValue: async function(event) {
|
||||||
|
const name = event.target.dataset.name;
|
||||||
|
if (!name || !name.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.group_set', {
|
||||||
|
group: this.group.friendly_name,
|
||||||
|
property: name,
|
||||||
|
value: event.target.value,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bus.$emit('refreshDevices');
|
||||||
|
},
|
||||||
|
|
||||||
|
toggleState: async function() {
|
||||||
|
const state = (await this.anyOn()) ? 'OFF' : 'ON';
|
||||||
|
await request('zigbee.mqtt.group_set', {
|
||||||
|
group: this.group.friendly_name,
|
||||||
|
property: 'state',
|
||||||
|
value: state,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bus.$emit('refreshDevices');
|
||||||
|
},
|
||||||
|
|
||||||
|
renameGroup: async function() {
|
||||||
|
const name = prompt('New name', this.group.friendly_name);
|
||||||
|
if (!name || !name.length || name === this.group.friendly_name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commandRunning = true;
|
||||||
|
await request('zigbee.mqtt.group_rename', {
|
||||||
|
name: name,
|
||||||
|
group: this.group.friendly_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.commandRunning = false;
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
self.bus.$emit('refreshGroups');
|
||||||
|
}, 100);
|
||||||
|
},
|
||||||
|
|
||||||
|
removeGroup: async function() {
|
||||||
|
if (!confirm('Are you sure that you want to delete this group?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commandRunning = true;
|
||||||
|
await request('zigbee.mqtt.group_remove', {name: this.group.friendly_name});
|
||||||
|
this.commandRunning = false;
|
||||||
|
this.bus.$emit('refreshGroups');
|
||||||
|
},
|
||||||
|
|
||||||
|
removeFromGroup: async function(device) {
|
||||||
|
if (!confirm('Are you sure that you want to remove this node from ' + this.group.label + '?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.group_remove_device', {
|
||||||
|
device: device,
|
||||||
|
group: this.group.friendly_name,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.bus.$emit('refreshGroups');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created: function() {
|
||||||
|
this.refreshProperties();
|
||||||
|
this.bus.$on('refresh', this.refreshProperties);
|
||||||
|
this.bus.$on('refreshDevices', this.refreshProperties);
|
||||||
|
this.bus.$on('refreshGroups', this.refreshProperties);
|
||||||
|
this.bus.$on('refreshProperties', this.refreshProperties);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
311
platypush/backend/http/static/js/plugins/zigbee.mqtt/index.js
Normal file
311
platypush/backend/http/static/js/plugins/zigbee.mqtt/index.js
Normal file
|
@ -0,0 +1,311 @@
|
||||||
|
Vue.component('zigbee-mqtt', {
|
||||||
|
template: '#tmpl-zigbee-mqtt',
|
||||||
|
props: ['config'],
|
||||||
|
|
||||||
|
data: function() {
|
||||||
|
return {
|
||||||
|
bus: new Vue({}),
|
||||||
|
status: {},
|
||||||
|
devices: {},
|
||||||
|
groups: {},
|
||||||
|
commandRunning: false,
|
||||||
|
selected: {
|
||||||
|
view: 'devices',
|
||||||
|
deviceId: undefined,
|
||||||
|
groupId: undefined,
|
||||||
|
},
|
||||||
|
loading: {
|
||||||
|
status: false,
|
||||||
|
devices: false,
|
||||||
|
groups: false,
|
||||||
|
},
|
||||||
|
views: {
|
||||||
|
devices: true,
|
||||||
|
groups: true,
|
||||||
|
},
|
||||||
|
modal: {
|
||||||
|
group: {
|
||||||
|
visible: false,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
computed: {
|
||||||
|
networkDropdownItems: function() {
|
||||||
|
const self = this;
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
text: 'Start Network',
|
||||||
|
disabled: this.commandRunning,
|
||||||
|
click: async function() {
|
||||||
|
self.commandRunning = true;
|
||||||
|
await request('zigbee.mqtt.start_network');
|
||||||
|
self.commandRunning = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'Stop Network',
|
||||||
|
disabled: this.commandRunning,
|
||||||
|
click: async function() {
|
||||||
|
self.commandRunning = true;
|
||||||
|
await request('zigbee.mqtt.stop_network');
|
||||||
|
self.commandRunning = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'Permit Join',
|
||||||
|
disabled: this.commandRunning,
|
||||||
|
click: async function() {
|
||||||
|
let seconds = prompt('Join allow period in seconds (type 0 for no time limits)', '60');
|
||||||
|
if (!seconds) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
seconds = parseInt(seconds);
|
||||||
|
self.commandRunning = true;
|
||||||
|
await request('zigbee.mqtt.permit_join', {permit: true, timeout: seconds || null});
|
||||||
|
self.commandRunning = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'Reset',
|
||||||
|
disabled: this.commandRunning,
|
||||||
|
click: async function() {
|
||||||
|
if (!confirm('Are you sure that you want to reset the device?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.reset');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
text: 'Factory Reset',
|
||||||
|
disabled: this.commandRunning,
|
||||||
|
classes: ['error'],
|
||||||
|
click: async function() {
|
||||||
|
if (!confirm('Are you sure that you want to do a device soft reset? ALL network information and custom firmware will be lost!!')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
await request('zigbee.mqtt.factory_reset');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
addToGroupDropdownItems: function() {
|
||||||
|
const self = this;
|
||||||
|
return Object.values(this.groups).filter((group) => {
|
||||||
|
return !group.values || !group.values.length || !(this.selected.valueId in this.scene.values);
|
||||||
|
}).map((group) => {
|
||||||
|
return {
|
||||||
|
text: group.name,
|
||||||
|
disabled: this.commandRunning,
|
||||||
|
click: async function () {
|
||||||
|
if (!self.selected.valueId) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
self.commandRunning = true;
|
||||||
|
await request('zwave.scene_add_value', {
|
||||||
|
id_on_network: self.selected.valueId,
|
||||||
|
scene_id: group.scene_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
self.commandRunning = false;
|
||||||
|
self.refresh();
|
||||||
|
},
|
||||||
|
};
|
||||||
|
});
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
methods: {
|
||||||
|
refreshDevices: async function () {
|
||||||
|
const self = this;
|
||||||
|
this.loading.devices = true;
|
||||||
|
this.devices = (await request('zigbee.mqtt.devices')).reduce((devices, device) => {
|
||||||
|
if (device.friendly_name in self.devices) {
|
||||||
|
device = {
|
||||||
|
values: self.devices[device.friendly_name].values || {},
|
||||||
|
...self.devices[device.friendly_name],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
devices[device.friendly_name] = device;
|
||||||
|
return devices;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
Object.values(this.devices).forEach((device) => {
|
||||||
|
if (device.type === 'Coordinator') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
request('zigbee.mqtt.device_get', {device: device.friendly_name}).then((response) => {
|
||||||
|
Vue.set(self.devices[device.friendly_name], 'values', response || {});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
this.loading.devices = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
refreshGroups: async function () {
|
||||||
|
this.loading.groups = true;
|
||||||
|
this.groups = (await request('zigbee.mqtt.groups')).reduce((groups, group) => {
|
||||||
|
groups[group.id] = group;
|
||||||
|
return groups;
|
||||||
|
}, {});
|
||||||
|
|
||||||
|
this.loading.groups = false;
|
||||||
|
},
|
||||||
|
|
||||||
|
refresh: function () {
|
||||||
|
this.refreshDevices();
|
||||||
|
this.refreshGroups();
|
||||||
|
this.bus.$emit('refreshProperties');
|
||||||
|
},
|
||||||
|
|
||||||
|
addGroup: async function() {
|
||||||
|
const name = prompt('Group name');
|
||||||
|
if (!name) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commandRunning = true;
|
||||||
|
await request('zigbee.mqtt.group_add', {name: name});
|
||||||
|
this.commandRunning = false;
|
||||||
|
this.refreshGroups();
|
||||||
|
},
|
||||||
|
|
||||||
|
onViewChange: function(event) {
|
||||||
|
Vue.set(this.selected, 'view', event.target.value);
|
||||||
|
},
|
||||||
|
|
||||||
|
onDeviceClicked: function(event) {
|
||||||
|
Vue.set(this.selected, 'deviceId', event.deviceId === this.selected.deviceId ? undefined : event.deviceId);
|
||||||
|
},
|
||||||
|
|
||||||
|
onGroupClicked: function(event) {
|
||||||
|
Vue.set(this.selected, 'groupId', event.groupId === this.selected.groupId ? undefined : event.groupId);
|
||||||
|
},
|
||||||
|
|
||||||
|
openNetworkCommandsDropdown: function() {
|
||||||
|
openDropdown(this.$refs.networkCommandsDropdown);
|
||||||
|
},
|
||||||
|
|
||||||
|
openAddToGroupDropdown: function(event) {
|
||||||
|
this.selected.valueId = event.valueId;
|
||||||
|
openDropdown(this.$refs.addToGroupDropdown);
|
||||||
|
},
|
||||||
|
|
||||||
|
addToGroup: async function(device, group) {
|
||||||
|
this.commandRunning = true;
|
||||||
|
await request('zigbee.mqtt.group_add_device', {
|
||||||
|
device: device,
|
||||||
|
group: group,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.commandRunning = false;
|
||||||
|
const self = this;
|
||||||
|
|
||||||
|
setTimeout(() => {
|
||||||
|
self.refresh();
|
||||||
|
self.bus.$emit('refreshProperties');
|
||||||
|
}, 100)
|
||||||
|
},
|
||||||
|
|
||||||
|
removeNodeFromGroup: async function(event) {
|
||||||
|
if (!confirm('Are you sure that you want to remove this value from the group?')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.commandRunning = true;
|
||||||
|
await request('zigbee.mqtt.group_remove_device', {
|
||||||
|
group: event.group,
|
||||||
|
device: event.device,
|
||||||
|
});
|
||||||
|
|
||||||
|
this.commandRunning = false;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
|
||||||
|
created: function() {
|
||||||
|
const self = this;
|
||||||
|
this.bus.$on('refresh', this.refresh);
|
||||||
|
this.bus.$on('refreshDevices', this.refreshDevices);
|
||||||
|
this.bus.$on('refreshGroups', this.refreshGroups);
|
||||||
|
this.bus.$on('deviceClicked', this.onDeviceClicked);
|
||||||
|
this.bus.$on('groupClicked', this.onGroupClicked);
|
||||||
|
this.bus.$on('openAddToGroupModal', () => {self.modal.group.visible = true});
|
||||||
|
this.bus.$on('openAddToGroupDropdown', this.openAddToGroupDropdown);
|
||||||
|
this.bus.$on('removeFromGroup', this.removeNodeFromGroup);
|
||||||
|
|
||||||
|
registerEventHandler(() => {
|
||||||
|
createNotification({
|
||||||
|
text: 'WARNING: The controller is now offline',
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
}, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttOnlineEvent');
|
||||||
|
|
||||||
|
registerEventHandler(() => {
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to remove the device',
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
}, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRemovedFailedEvent');
|
||||||
|
|
||||||
|
registerEventHandler(() => {
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to add the group',
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
}, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupAddedFailedEvent');
|
||||||
|
|
||||||
|
registerEventHandler(() => {
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to remove the group',
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
}, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemovedFailedEvent');
|
||||||
|
|
||||||
|
registerEventHandler(() => {
|
||||||
|
createNotification({
|
||||||
|
text: 'Failed to remove the devices from the group',
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
}, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemoveAllFailedEvent');
|
||||||
|
|
||||||
|
registerEventHandler((event) => {
|
||||||
|
createNotification({
|
||||||
|
text: 'Unhandled Zigbee error: ' + (event.error || '[Unknown error]'),
|
||||||
|
error: true,
|
||||||
|
});
|
||||||
|
}, 'platypush.message.event.zigbee.mqtt.ZigbeeMqttErrorEvent');
|
||||||
|
|
||||||
|
registerEventHandler(this.refresh,
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttOnlineEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePropertySetEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDevicePairingEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceConnectedEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceBannedEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRemovedEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceWhitelistedEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceRenamedEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceBindEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttDeviceUnbindEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupAddedEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemovedEvent',
|
||||||
|
'platypush.message.event.zigbee.mqtt.ZigbeeMqttGroupRemoveAllEvent',
|
||||||
|
);
|
||||||
|
},
|
||||||
|
|
||||||
|
mounted: function() {
|
||||||
|
this.refresh();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
|
@ -77,7 +77,7 @@ Vue.component('zwave', {
|
||||||
disabled: this.commandRunning,
|
disabled: this.commandRunning,
|
||||||
click: async function() {
|
click: async function() {
|
||||||
self.commandRunning = true;
|
self.commandRunning = true;
|
||||||
await request('zwave.start_network');
|
await request('zwave.stop_network');
|
||||||
self.commandRunning = false;
|
self.commandRunning = false;
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -1,6 +1,8 @@
|
||||||
<script type="text/x-template" id="tmpl-dropdown">
|
<script type="text/x-template" id="tmpl-dropdown">
|
||||||
<div class="dropdown" :id="id" :class="{hidden: !visible}">
|
<div class="dropdown" :id="id" :class="{hidden: !visible}">
|
||||||
<div class="row item" :class="{disabled: item.disabled}" v-for="item in items" @click="clicked(item)">
|
<div class="row item"
|
||||||
|
:class="{disabled: item.disabled, ...classes.reduce((classes, c) => {classes[c] = true; return classes}, {})}"
|
||||||
|
v-for="item in items" @click="clicked(item)">
|
||||||
<div class="col-1 icon">
|
<div class="col-1 icon">
|
||||||
<i class="fa" :class="['fa-' + (item.icon || '')]" v-if="item.icon"></i>
|
<i class="fa" :class="['fa-' + (item.icon || '')]" v-if="item.icon"></i>
|
||||||
<i :class="item.iconClass" v-else-if="item.iconClass"></i>
|
<i :class="item.iconClass" v-else-if="item.iconClass"></i>
|
||||||
|
|
|
@ -19,6 +19,7 @@
|
||||||
'switches': 'fa fa-toggle-on',
|
'switches': 'fa fa-toggle-on',
|
||||||
'tts': 'fa fa-comment',
|
'tts': 'fa fa-comment',
|
||||||
'tts.google': 'fa fa-comment',
|
'tts.google': 'fa fa-comment',
|
||||||
|
'zigbee.mqtt': 'fa fa-zigbee',
|
||||||
'zwave': 'fa fa-zwave',
|
'zwave': 'fa fa-zwave',
|
||||||
}
|
}
|
||||||
%}
|
%}
|
||||||
|
|
163
platypush/backend/http/templates/plugins/zigbee.mqtt/device.html
Normal file
163
platypush/backend/http/templates/plugins/zigbee.mqtt/device.html
Normal file
|
@ -0,0 +1,163 @@
|
||||||
|
<script type="text/x-template" id="tmpl-zigbee-device">
|
||||||
|
<div class="item device" :class="{selected: selected}">
|
||||||
|
<div class="row name vertical-center" :class="{selected: selected}"
|
||||||
|
v-text="device.friendly_name" @click="onDeviceClicked"></div>
|
||||||
|
|
||||||
|
<div class="params" v-if="selected">
|
||||||
|
<div class="row">
|
||||||
|
<div class="param-name">Name</div>
|
||||||
|
<div class="param-value">
|
||||||
|
<div :class="{hidden: !editMode.name}">
|
||||||
|
<form ref="nameForm" @submit.prevent="editName">
|
||||||
|
<input type="text" name="name" :value="device.friendly_name">
|
||||||
|
|
||||||
|
<span class="buttons">
|
||||||
|
<button type="button" class="btn btn-default" @click="editMode.name = false">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button type="submit" class="btn btn-default">
|
||||||
|
<i class="fa fa-check"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div :class="{hidden: editMode.name}">
|
||||||
|
<span v-text="device.friendly_name"></span>
|
||||||
|
<span class="buttons">
|
||||||
|
<button type="button" class="btn btn-default" @click="onEditMode('name')">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</button>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="param-name">IEEE Address</div>
|
||||||
|
<div class="param-value" v-text="device.ieeeAddr"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="param-name">Network Address</div>
|
||||||
|
<div class="param-value" v-text="'0x' + parseInt(device.networkAddress).toString(16)"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="param-name">Type</div>
|
||||||
|
<div class="param-value" v-text="device.type"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="device.manufacturerID">
|
||||||
|
<div class="param-name">Manufacturer</div>
|
||||||
|
<div class="param-value">
|
||||||
|
{% raw %}{{ device.manufacturerName }} ({{ device.manufacturerID }}){% endraw %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="device.modelID">
|
||||||
|
<div class="param-name">Model</div>
|
||||||
|
<div class="param-value">
|
||||||
|
{% raw %}{{ device.model }} ({{ device.modelID }}){% endraw %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="device.softwareBuildID && device.softwareBuildID">
|
||||||
|
<div class="param-name">Software Build ID</div>
|
||||||
|
<div class="param-value" v-text="device.softwareBuildID"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="device.dateCode && device.dateCode.length">
|
||||||
|
<div class="param-name">Date Code</div>
|
||||||
|
<div class="param-value" v-text="device.dateCode"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="device.powerSource">
|
||||||
|
<div class="param-name">Power Source</div>
|
||||||
|
<div class="param-value" v-text="device.powerSource"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" v-if="device.lastSeen">
|
||||||
|
<div class="param-name">Last Seen</div>
|
||||||
|
<div class="param-value" v-text="(new Date(device.lastSeen)).toLocaleString()"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section values" v-if="device.values && Object.keys(device.values).length">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Values</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="row"
|
||||||
|
v-for="value, name in device.values"
|
||||||
|
:key="name">
|
||||||
|
<div class="param-name" v-text="name"></div>
|
||||||
|
<div class="param-value">
|
||||||
|
<toggle-switch :value="value === 'ON' ? true : false"
|
||||||
|
:data-name="name"
|
||||||
|
@toggled="setValue"
|
||||||
|
v-if="name === 'state'">
|
||||||
|
</toggle-switch>
|
||||||
|
|
||||||
|
<span v-text="value.toString() + '%'" v-else-if="name === 'linkquality'"></span>
|
||||||
|
|
||||||
|
<div v-else>
|
||||||
|
<input type="text" :value="value" :data-name="name" @change="setValue">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row">
|
||||||
|
<div class="param-name">
|
||||||
|
<input type="text" placeholder="New property name" v-model="newPropertyName">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="param-value">
|
||||||
|
<input type="text" placeholder="New property value" @change="setValue">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section actions">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Actions</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="row" @click="removeDevice(false)">
|
||||||
|
<div class="param-name">Remove Device</div>
|
||||||
|
<div class="param-value">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row error" @click="removeDevice(true)">
|
||||||
|
<div class="param-name">Force Remove Device</div>
|
||||||
|
<div class="param-value">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" @click="banDevice">
|
||||||
|
<div class="param-name">Ban Device</div>
|
||||||
|
<div class="param-value">
|
||||||
|
<i class="fa fa-ban"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" @click="whitelistDevice">
|
||||||
|
<div class="param-name">Whitelist Device</div>
|
||||||
|
<div class="param-value">
|
||||||
|
<i class="fa fa-list"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zigbee.mqtt/device.js') }}"></script>
|
||||||
|
|
|
@ -0,0 +1,75 @@
|
||||||
|
<script type="text/x-template" id="tmpl-zigbee-group">
|
||||||
|
<div class="item group" :class="{selected: selected}">
|
||||||
|
<div class="row name vertical-center" :class="{selected: selected}"
|
||||||
|
v-text="group.friendly_name" @click="onGroupClicked"></div>
|
||||||
|
|
||||||
|
<div class="params" v-if="selected">
|
||||||
|
<div class="section values">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Values</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="row" v-for="value, name in properties" :key="name">
|
||||||
|
<div class="param-name" v-text="name"></div>
|
||||||
|
<div class="param-value">
|
||||||
|
<div v-if="name === 'state'">
|
||||||
|
<toggle-switch :value="value" @toggled="toggleState"></toggle-switch>
|
||||||
|
</div>
|
||||||
|
<div v-else>
|
||||||
|
<input type="text" :value="value" :data-name="name" @change="setValue">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section devices">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title col-10">Devices</div>
|
||||||
|
<div class="buttons col-2">
|
||||||
|
<button class="btn btn-default" title="Add Devices" @click="bus.$emit('openAddToGroupModal')">
|
||||||
|
<i class="fa fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="row" v-for="device in group.devices">
|
||||||
|
<div class="col-10" v-text="device.friendly_name"></div>
|
||||||
|
<div class="buttons col-2">
|
||||||
|
<button class="btn btn-default" title="Remove from group" @click="removeFromGroup(device.friendly_name)">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="section actions">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Actions</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="row" @click="renameGroup">
|
||||||
|
<div class="col-10">Rename Group</div>
|
||||||
|
<div class="buttons col-2">
|
||||||
|
<i class="fa fa-edit"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="row" @click="removeGroup">
|
||||||
|
<div class="col-10">Remove Group</div>
|
||||||
|
<div class="buttons col-2">
|
||||||
|
<i class="fa fa-trash"></i>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zigbee.mqtt/group.js') }}"></script>
|
||||||
|
|
|
@ -0,0 +1,69 @@
|
||||||
|
{% include 'plugins/zigbee.mqtt/device.html' %}
|
||||||
|
{% include 'plugins/zigbee.mqtt/group.html' %}
|
||||||
|
|
||||||
|
<script type="text/x-template" id="tmpl-zigbee-mqtt">
|
||||||
|
<div class="zigbee-container">
|
||||||
|
{% include 'plugins/zigbee.mqtt/modals/group.html' %}
|
||||||
|
|
||||||
|
<div class="view-options">
|
||||||
|
<div class="view-selector col-s-9 col-m-10 col-l-11">
|
||||||
|
<select @change="onViewChange">
|
||||||
|
<option v-for="_, view in views"
|
||||||
|
v-text="(view[0].toUpperCase() + view.slice(1)).replace('_', ' ')"
|
||||||
|
:key="view" :selected="view == selected.view" :value="view">
|
||||||
|
</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="buttons">
|
||||||
|
<button class="btn btn-default" title="Add Group" v-if="selected.view === 'groups'"
|
||||||
|
:disabled="commandRunning" @click="addGroup">
|
||||||
|
<i class="fa fa-plus"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-default" title="Network commands" @click="openNetworkCommandsDropdown">
|
||||||
|
<i class="fa fa-cog"></i>
|
||||||
|
</button>
|
||||||
|
|
||||||
|
<button class="btn btn-default" title="Refresh network" @click="refresh">
|
||||||
|
<i class="fa fa-sync-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<dropdown ref="networkCommandsDropdown" :items="networkDropdownItems"></dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view devices" v-if="selected.view == 'devices'">
|
||||||
|
<div class="no-items" v-if="Object.keys(devices).length == 0">
|
||||||
|
<div class="loading" v-if="loading.nodes">Loading devices...</div>
|
||||||
|
<div class="empty" v-else>No devices found on the network</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<zigbee-device
|
||||||
|
v-for="device, deviceId in devices"
|
||||||
|
:key="deviceId"
|
||||||
|
:device="device"
|
||||||
|
:bus="bus"
|
||||||
|
:selected="selected.deviceId == deviceId">
|
||||||
|
</zigbee-device>
|
||||||
|
|
||||||
|
<dropdown ref="addToGroupDropdown" :items="addToGroupDropdownItems"></dropdown>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="view groups" v-else-if="selected.view == 'groups'">
|
||||||
|
<div class="no-items" v-if="Object.keys(groups).length == 0">
|
||||||
|
<div class="loading" v-if="loading.groups">Loading groups...</div>
|
||||||
|
<div class="empty" v-else>No groups available on the network</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<zigbee-group
|
||||||
|
v-for="group, groupId in groups"
|
||||||
|
:key="groupId"
|
||||||
|
:group="group"
|
||||||
|
:selected="selected.groupId == groupId"
|
||||||
|
:bus="bus">
|
||||||
|
</zigbee-group>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</script>
|
||||||
|
|
|
@ -0,0 +1,20 @@
|
||||||
|
<modal id="zigbee-add-to-group" title="Add devices to group" v-model="modal.group.visible" v-if="modal.group.visible">
|
||||||
|
<div class="group-add">
|
||||||
|
<div class="params">
|
||||||
|
<div class="section">
|
||||||
|
<div class="header">
|
||||||
|
<div class="title">Select devices to add</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="body">
|
||||||
|
<div class="row clickable" @click="addToGroup(device.friendly_name, groups[selected.groupId].friendly_name)"
|
||||||
|
:key="device.friendly_name"
|
||||||
|
v-for="device in Object.values(devices).filter((dev) => Object.values(groups[selected.groupId].devices).map((d) => d.friendly_name).indexOf(dev.friendly_name) < 0)">
|
||||||
|
<div class="param-name" v-text="device.friendly_name"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</modal>
|
||||||
|
|
|
@ -57,7 +57,7 @@
|
||||||
<div class="row" v-if="node.neighbours.length">
|
<div class="row" v-if="node.neighbours.length">
|
||||||
<div class="param-name">Neighbours</div>
|
<div class="param-name">Neighbours</div>
|
||||||
<div class="param-value">
|
<div class="param-value">
|
||||||
<div class="row" v-for="neighbour in node.neighbours" v-text="neighbour"></div>
|
<div class="row pull-right" v-for="neighbour in node.neighbours" v-text="neighbour"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import Optional, Dict, Any
|
from typing import Dict, Any
|
||||||
|
|
||||||
from platypush.message.event import Event
|
from platypush.message.event import Event
|
||||||
|
|
||||||
|
@ -108,7 +108,7 @@ class ZigbeeMqttGroupAddedEvent(ZigbeeMqttEvent):
|
||||||
Triggered when a group is added.
|
Triggered when a group is added.
|
||||||
"""
|
"""
|
||||||
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
||||||
super().__init__(*args, host=host, port=port, device=device, **kwargs)
|
super().__init__(*args, host=host, port=port, group=group, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ZigbeeMqttGroupAddedFailedEvent(ZigbeeMqttEvent):
|
class ZigbeeMqttGroupAddedFailedEvent(ZigbeeMqttEvent):
|
||||||
|
@ -116,7 +116,7 @@ class ZigbeeMqttGroupAddedFailedEvent(ZigbeeMqttEvent):
|
||||||
Triggered when a request to add a group fails.
|
Triggered when a request to add a group fails.
|
||||||
"""
|
"""
|
||||||
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
||||||
super().__init__(*args, host=host, port=port, device=device, **kwargs)
|
super().__init__(*args, host=host, port=port, group=group, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ZigbeeMqttGroupRemovedEvent(ZigbeeMqttEvent):
|
class ZigbeeMqttGroupRemovedEvent(ZigbeeMqttEvent):
|
||||||
|
@ -124,7 +124,7 @@ class ZigbeeMqttGroupRemovedEvent(ZigbeeMqttEvent):
|
||||||
Triggered when a group is removed.
|
Triggered when a group is removed.
|
||||||
"""
|
"""
|
||||||
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
||||||
super().__init__(*args, host=host, port=port, device=device, **kwargs)
|
super().__init__(*args, host=host, port=port, group=group, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ZigbeeMqttGroupRemovedFailedEvent(ZigbeeMqttEvent):
|
class ZigbeeMqttGroupRemovedFailedEvent(ZigbeeMqttEvent):
|
||||||
|
@ -132,7 +132,7 @@ class ZigbeeMqttGroupRemovedFailedEvent(ZigbeeMqttEvent):
|
||||||
Triggered when a request to remove a group fails.
|
Triggered when a request to remove a group fails.
|
||||||
"""
|
"""
|
||||||
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
||||||
super().__init__(*args, host=host, port=port, device=device, **kwargs)
|
super().__init__(*args, host=host, port=port, group=group, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ZigbeeMqttGroupRemoveAllEvent(ZigbeeMqttEvent):
|
class ZigbeeMqttGroupRemoveAllEvent(ZigbeeMqttEvent):
|
||||||
|
@ -140,7 +140,7 @@ class ZigbeeMqttGroupRemoveAllEvent(ZigbeeMqttEvent):
|
||||||
Triggered when all the devices are removed from a group.
|
Triggered when all the devices are removed from a group.
|
||||||
"""
|
"""
|
||||||
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
||||||
super().__init__(*args, host=host, port=port, device=device, **kwargs)
|
super().__init__(*args, host=host, port=port, group=group, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ZigbeeMqttGroupRemoveAllFailedEvent(ZigbeeMqttEvent):
|
class ZigbeeMqttGroupRemoveAllFailedEvent(ZigbeeMqttEvent):
|
||||||
|
@ -148,7 +148,7 @@ class ZigbeeMqttGroupRemoveAllFailedEvent(ZigbeeMqttEvent):
|
||||||
Triggered when a request to remove all the devices from a group fails.
|
Triggered when a request to remove all the devices from a group fails.
|
||||||
"""
|
"""
|
||||||
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
def __init__(self, host: str, port: int, group=None, *args, **kwargs):
|
||||||
super().__init__(*args, host=host, port=port, device=device, **kwargs)
|
super().__init__(*args, host=host, port=port, group=group, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
class ZigbeeMqttErrorEvent(ZigbeeMqttEvent):
|
class ZigbeeMqttErrorEvent(ZigbeeMqttEvent):
|
||||||
|
|
|
@ -66,7 +66,7 @@ class Response(Message):
|
||||||
Overrides the str() operator and converts
|
Overrides the str() operator and converts
|
||||||
the message into a UTF-8 JSON string
|
the message into a UTF-8 JSON string
|
||||||
"""
|
"""
|
||||||
output = self.output if self.output is not None and self.output != {} else {
|
output = self.output if self.output is not None else {
|
||||||
'success': True if not self.errors else False
|
'success': True if not self.errors else False
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -92,12 +92,12 @@ class ZigbeeMqttPlugin(MqttPlugin):
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __init__(self, host: str, port: int = 1883, base_topic: str = 'zigbee2mqtt', timeout: int = 60,
|
def __init__(self, host: str = 'localhost', port: int = 1883, base_topic: str = 'zigbee2mqtt', timeout: int = 60,
|
||||||
tls_certfile: Optional[str] = None, tls_keyfile: Optional[str] = None,
|
tls_certfile: Optional[str] = None, tls_keyfile: Optional[str] = None,
|
||||||
tls_version: Optional[str] = None, tls_ciphers: Optional[str] = None,
|
tls_version: Optional[str] = None, tls_ciphers: Optional[str] = None,
|
||||||
username: Optional[str] = None, password: Optional[str] = None, **kwargs):
|
username: Optional[str] = None, password: Optional[str] = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
:param host: Default MQTT broker where ``zigbee2mqtt`` publishes its messages.
|
:param host: Default MQTT broker where ``zigbee2mqtt`` publishes its messages (default: ``localhost``).
|
||||||
:param port: Broker listen port (default: 1883).
|
:param port: Broker listen port (default: 1883).
|
||||||
:param base_topic: Topic prefix, as specified in ``/opt/zigbee2mqtt/data/configuration.yaml``
|
:param base_topic: Topic prefix, as specified in ``/opt/zigbee2mqtt/data/configuration.yaml``
|
||||||
(default: '``base_topic``').
|
(default: '``base_topic``').
|
||||||
|
@ -305,6 +305,15 @@ class ZigbeeMqttPlugin(MqttPlugin):
|
||||||
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||||
(default: query the default configured device).
|
(default: query the default configured device).
|
||||||
"""
|
"""
|
||||||
|
if name == device:
|
||||||
|
self.logger.info('Old and new name are the same: nothing to do')
|
||||||
|
return
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
devices = self.devices().output
|
||||||
|
assert not [dev for dev in devices if dev.get('friendly_name') == name], \
|
||||||
|
'A device named {} already exists on the network'.format(name)
|
||||||
|
|
||||||
self.publish(
|
self.publish(
|
||||||
topic=self._topic('bridge/config/rename{}'.format('_last' if not device else '')),
|
topic=self._topic('bridge/config/rename{}'.format('_last' if not device else '')),
|
||||||
msg={'old': device, 'new': name} if device else name,
|
msg={'old': device, 'new': name} if device else name,
|
||||||
|
@ -333,7 +342,7 @@ class ZigbeeMqttPlugin(MqttPlugin):
|
||||||
|
|
||||||
return properties
|
return properties
|
||||||
|
|
||||||
# noinspection PyShadowingBuiltins
|
# noinspection PyShadowingBuiltins,DuplicatedCode
|
||||||
@action
|
@action
|
||||||
def device_set(self, device: str, property: str, value: Any, **kwargs):
|
def device_set(self, device: str, property: str, value: Any, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -372,6 +381,37 @@ class ZigbeeMqttPlugin(MqttPlugin):
|
||||||
reply_topic=self._topic(device), msg=device, **self._mqtt_args(**kwargs)).\
|
reply_topic=self._topic(device), msg=device, **self._mqtt_args(**kwargs)).\
|
||||||
output.get('group_list', [])
|
output.get('group_list', [])
|
||||||
|
|
||||||
|
@action
|
||||||
|
def groups(self, **kwargs):
|
||||||
|
"""
|
||||||
|
Get the groups registered on the device.
|
||||||
|
|
||||||
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||||
|
(default: query the default configured device).
|
||||||
|
"""
|
||||||
|
groups = self.publish(topic=self._topic('bridge/config/groups'), msg={},
|
||||||
|
reply_topic=self._topic('bridge/log'),
|
||||||
|
**self._mqtt_args(**kwargs)).output.get('message', [])
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
devices = {
|
||||||
|
device['ieeeAddr']: device
|
||||||
|
for device in self.devices(**kwargs).output
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'id': group['ID'],
|
||||||
|
'friendly_name': group['friendly_name'],
|
||||||
|
'optimistic': group.get('optimistic', False),
|
||||||
|
'devices': [
|
||||||
|
devices[device.split('/')[0]]
|
||||||
|
for device in group.get('devices', [])
|
||||||
|
]
|
||||||
|
}
|
||||||
|
for group in groups
|
||||||
|
]
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def group_add(self, name: str, id: Optional[int] = None, **kwargs):
|
def group_add(self, name: str, id: Optional[int] = None, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
@ -388,6 +428,53 @@ class ZigbeeMqttPlugin(MqttPlugin):
|
||||||
|
|
||||||
self.publish(topic=self._topic('bridge/config/add_group'), msg=args, **self._mqtt_args(**kwargs))
|
self.publish(topic=self._topic('bridge/config/add_group'), msg=args, **self._mqtt_args(**kwargs))
|
||||||
|
|
||||||
|
# noinspection PyShadowingBuiltins,DuplicatedCode
|
||||||
|
@action
|
||||||
|
def group_set(self, group: str, property: str, value: Any, **kwargs):
|
||||||
|
"""
|
||||||
|
Set a properties on a group. The compatible properties vary depending on the devices on the group.
|
||||||
|
For example, a light bulb may have the "``state``" (with values ``"ON"`` and ``"OFF"``) and "``brightness``"
|
||||||
|
properties, while an environment sensor may have the "``temperature``" and "``humidity``" properties, and so on.
|
||||||
|
|
||||||
|
:param group: Display name of the group.
|
||||||
|
:param property: Name of the property that should be set.
|
||||||
|
:param value: New value of the property.
|
||||||
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||||
|
(default: query the default configured device).
|
||||||
|
"""
|
||||||
|
properties = self.publish(topic=self._topic(group + '/set'),
|
||||||
|
reply_topic=self._topic(group),
|
||||||
|
msg={property: value}, **self._mqtt_args(**kwargs)).output
|
||||||
|
|
||||||
|
if property:
|
||||||
|
assert property in properties, 'No such property: ' + property
|
||||||
|
return {property: properties[property]}
|
||||||
|
|
||||||
|
return properties
|
||||||
|
|
||||||
|
@action
|
||||||
|
def group_rename(self, name: str, group: str, **kwargs):
|
||||||
|
"""
|
||||||
|
Rename a group.
|
||||||
|
|
||||||
|
:param name: New name.
|
||||||
|
:param group: Current name of the group to rename.
|
||||||
|
:param kwargs: Extra arguments to be passed to :meth:`platypush.plugins.mqtt.MqttPlugin.publish``
|
||||||
|
(default: query the default configured device).
|
||||||
|
"""
|
||||||
|
if name == group:
|
||||||
|
self.logger.info('Old and new name are the same: nothing to do')
|
||||||
|
return
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
groups = {group.get('friendly_name'): group for group in self.groups().output}
|
||||||
|
assert name not in groups, 'A group named {} already exists on the network'.format(name)
|
||||||
|
|
||||||
|
self.publish(
|
||||||
|
topic=self._topic('bridge/config/rename'),
|
||||||
|
msg={'old': group, 'new': name} if group else name,
|
||||||
|
**self._mqtt_args(**kwargs))
|
||||||
|
|
||||||
@action
|
@action
|
||||||
def group_remove(self, name: str, **kwargs):
|
def group_remove(self, name: str, **kwargs):
|
||||||
"""
|
"""
|
||||||
|
|
Loading…
Reference in a new issue