Adding Z-Wave web panel (#123) [WIP]
This commit is contained in:
parent
c5adc141ea
commit
fcef7af6a4
15 changed files with 1209 additions and 10 deletions
|
@ -11,6 +11,10 @@
|
|||
text-align: right !important;
|
||||
}
|
||||
|
||||
.clickable {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
a:focus {
|
||||
outline: none;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,195 @@
|
|||
@import 'common/vars';
|
||||
@import 'webpanel/plugins/zwave/vars';
|
||||
|
||||
.fa.fa-zwave:before {
|
||||
content: ' ';
|
||||
background: url('/static/img/icons/z-wave-logo.png');
|
||||
background-size: 1em 1em;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.zwave-container {
|
||||
height: 100%;
|
||||
padding: 0 .5em;
|
||||
background: $container-bg;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
||||
.modal {
|
||||
.section {
|
||||
.header {
|
||||
background: none;
|
||||
padding: .5em 0;
|
||||
}
|
||||
|
||||
.body {
|
||||
padding: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.network-info {
|
||||
min-width: 600pt;
|
||||
}
|
||||
}
|
||||
|
||||
.error {
|
||||
color: $error-color;
|
||||
}
|
||||
|
||||
.node {
|
||||
.actions {
|
||||
.row {
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
form {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
|
BIN
platypush/backend/http/static/img/icons/z-wave-logo.png
Normal file
BIN
platypush/backend/http/static/img/icons/z-wave-logo.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
|
@ -132,7 +132,7 @@ Vue.component('light-hue', {
|
|||
);
|
||||
|
||||
this.selectedScene = event.id;
|
||||
groups = {}
|
||||
const groups = {};
|
||||
|
||||
for (const lightId of Object.values(this.scenes[this.selectedScene].lights)) {
|
||||
this.lights[lightId].state.on = true;
|
||||
|
@ -148,7 +148,7 @@ Vue.component('light-hue', {
|
|||
|
||||
groups[groupId].lights.push(lightId);
|
||||
|
||||
if (groups[groupId].lights.length == Object.values(group.lights).length) {
|
||||
if (groups[groupId].lights.length === Object.values(group.lights).length) {
|
||||
groups[groupId].all_on = true;
|
||||
}
|
||||
}
|
||||
|
@ -164,8 +164,8 @@ Vue.component('light-hue', {
|
|||
},
|
||||
|
||||
collapsedToggled: function(event) {
|
||||
if (event.type == this.selectedProperties.type
|
||||
&& event.id == this.selectedProperties.id) {
|
||||
if (event.type === this.selectedProperties.type
|
||||
&& event.id === this.selectedProperties.id) {
|
||||
this.selectedProperties = {
|
||||
type: undefined,
|
||||
id: undefined,
|
||||
|
|
24
platypush/backend/http/static/js/plugins/zwave/group.js
Normal file
24
platypush/backend/http/static/js/plugins/zwave/group.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
Vue.component('zwave-group', {
|
||||
template: '#tmpl-zwave-group',
|
||||
props: ['group','nodes','bus','selected'],
|
||||
|
||||
methods: {
|
||||
onGroupClicked: function() {
|
||||
this.bus.$emit('groupClicked', {
|
||||
groupId: this.group.index,
|
||||
});
|
||||
},
|
||||
|
||||
removeFromGroup: async function(nodeId) {
|
||||
if (!confirm('Are you sure that you want to remove this node from ' + this.group.label + '?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await request('zwave.remove_node_from_group', {
|
||||
node_id: nodeId,
|
||||
group_index: this.group.index,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
457
platypush/backend/http/static/js/plugins/zwave/index.js
Normal file
457
platypush/backend/http/static/js/plugins/zwave/index.js
Normal file
|
@ -0,0 +1,457 @@
|
|||
Vue.component('zwave', {
|
||||
template: '#tmpl-zwave',
|
||||
props: ['config'],
|
||||
|
||||
data: function() {
|
||||
return {
|
||||
bus: new Vue({}),
|
||||
status: {},
|
||||
views: {},
|
||||
nodes: {},
|
||||
groups: {},
|
||||
scenes: {},
|
||||
values: {},
|
||||
switches: new Set(),
|
||||
dimmers: new Set(),
|
||||
sensors: new Set(),
|
||||
batteryLevels: new Set(),
|
||||
powerLevels: new Set(),
|
||||
bulbs: new Set(),
|
||||
doorlocks: new Set(),
|
||||
usercodes: new Set(),
|
||||
thermostats: new Set(),
|
||||
protections: new Set(),
|
||||
commandRunning: false,
|
||||
selected: {
|
||||
view: 'nodes',
|
||||
nodeId: undefined,
|
||||
groupId: undefined,
|
||||
sceneId: undefined,
|
||||
valueId: undefined,
|
||||
},
|
||||
loading: {
|
||||
status: false,
|
||||
nodes: false,
|
||||
groups: false,
|
||||
scenes: false,
|
||||
values: false,
|
||||
},
|
||||
modal: {
|
||||
networkInfo: {
|
||||
visible: false,
|
||||
},
|
||||
group: {
|
||||
visible: false,
|
||||
},
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
computed: {
|
||||
networkDropdownItems: function() {
|
||||
const self = this;
|
||||
return [
|
||||
{
|
||||
text: 'Start Network',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
self.commandRunning = true;
|
||||
await request('zwave.start_network');
|
||||
self.commandRunning = false;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Stop Network',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
self.commandRunning = true;
|
||||
await request('zwave.start_network');
|
||||
self.commandRunning = false;
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Switch All On',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
self.commandRunning = true;
|
||||
await request('zwave.switch_all', {state: true});
|
||||
self.commandRunning = false;
|
||||
self.refresh();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Switch All Off',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
self.commandRunning = true;
|
||||
await request('zwave.switch_all', {state: false});
|
||||
self.commandRunning = false;
|
||||
self.refresh();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Cancel Command',
|
||||
click: async function() {
|
||||
await request('zwave.cancel_command');
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Kill Command',
|
||||
click: async function() {
|
||||
await request('zwave.kill_command');
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Set Controller Name',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
const name = prompt('Controller name');
|
||||
if (!name) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.commandRunning = true;
|
||||
await request('zwave.set_controller_name', {name: name});
|
||||
self.commandRunning = false;
|
||||
self.refresh();
|
||||
},
|
||||
},
|
||||
|
||||
|
||||
{
|
||||
text: 'Receive Configuration From Primary',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
self.commandRunning = true;
|
||||
await request('zwave.receive_configuration');
|
||||
self.commandRunning = false;
|
||||
self.refresh();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Create New Primary',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
self.commandRunning = true;
|
||||
await request('zwave.create_new_primary');
|
||||
self.commandRunning = false;
|
||||
self.refresh();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Transfer Primary Role',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
self.commandRunning = true;
|
||||
await request('zwave.transfer_primary_role');
|
||||
self.commandRunning = false;
|
||||
self.refresh();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Heal Network',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
self.commandRunning = true;
|
||||
await request('zwave.heal');
|
||||
self.commandRunning = false;
|
||||
self.refresh();
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Soft Reset',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
if (!confirm('Are you sure that you want to do a device soft reset? Network information will not be lost')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await request('zwave.soft_reset');
|
||||
},
|
||||
},
|
||||
|
||||
{
|
||||
text: 'Hard Reset',
|
||||
disabled: this.commandRunning,
|
||||
click: async function() {
|
||||
if (!confirm('Are you sure that you want to do a device soft reset? ALL network information will be lost!')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await request('zwave.hard_reset');
|
||||
},
|
||||
},
|
||||
]
|
||||
},
|
||||
},
|
||||
|
||||
methods: {
|
||||
refreshNodes: async function () {
|
||||
this.loading.nodes = true;
|
||||
this.loading.values = true;
|
||||
|
||||
this.nodes = await request('zwave.get_nodes');
|
||||
this.loading.nodes = false;
|
||||
|
||||
this.values = Object.values(this.nodes).reduce((values, node) => {
|
||||
values = {
|
||||
...Object.values(node.values).reduce((values, value) => {
|
||||
values[value.value_id] = {
|
||||
node_id: node.node_id,
|
||||
...value,
|
||||
};
|
||||
|
||||
return values;
|
||||
}, {}),
|
||||
...values
|
||||
};
|
||||
|
||||
return values;
|
||||
}, {});
|
||||
|
||||
this.loading.values = false;
|
||||
},
|
||||
|
||||
refreshGroups: async function () {
|
||||
this.loading.groups = true;
|
||||
this.groups = Object.values(await request('zwave.get_groups'))
|
||||
.filter((group) => group.index)
|
||||
.reduce((groups, group) => {
|
||||
groups[group.index] = group;
|
||||
return groups;
|
||||
}, {});
|
||||
|
||||
if (Object.keys(this.groups).length) {
|
||||
Vue.set(this.views, 'groups', true);
|
||||
}
|
||||
|
||||
this.loading.groups = false;
|
||||
},
|
||||
|
||||
refreshScenes: async function () {
|
||||
this.loading.scenes = true;
|
||||
this.scenes = Object.values(await request('zwave.get_scenes'))
|
||||
.filter((scene) => scene.scene_id)
|
||||
.reduce((scenes, scene) => {
|
||||
scenes[scene.scene_id] = scene;
|
||||
return scenes;
|
||||
}, {});
|
||||
|
||||
this.loading.scenes = false;
|
||||
},
|
||||
|
||||
refreshSwitches: async function () {
|
||||
this.switches = new Set(Object.values(await request('zwave.get_switches'))
|
||||
.filter((sw) => sw.id_on_network).map((sw) => sw.value_id));
|
||||
|
||||
if (this.switches.size) {
|
||||
Vue.set(this.views, 'switches', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshDimmers: async function () {
|
||||
this.dimmers = new Set(Object.values(await request('zwave.get_dimmers'))
|
||||
.filter((dimmer) => dimmer.id_on_network).map((dimmer) => dimmer.value_id));
|
||||
|
||||
if (this.dimmers.size) {
|
||||
Vue.set(this.views, 'dimmers', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshSensors: async function () {
|
||||
this.sensors = new Set(Object.values(await request('zwave.get_sensors'))
|
||||
.filter((sensor) => sensor.id_on_network).map((sensor) => sensor.value_id));
|
||||
|
||||
if (this.sensors.size) {
|
||||
Vue.set(this.views, 'sensors', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshBatteryLevels: async function () {
|
||||
this.batteryLevels = new Set(Object.values(await request('zwave.get_battery_levels'))
|
||||
.filter((battery) => battery.id_on_network).map((battery) => battery.value_id));
|
||||
|
||||
if (this.batteryLevels.size) {
|
||||
Vue.set(this.views, 'batteryLevels', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshPowerLevels: async function () {
|
||||
this.powerLevels = new Set(Object.values(await request('zwave.get_power_levels'))
|
||||
.filter((power) => power.id_on_network).map((power) => power.value_id));
|
||||
|
||||
if (this.powerLevels.size) {
|
||||
Vue.set(this.views, 'powerLevels', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshBulbs: async function () {
|
||||
this.bulbs = new Set(Object.values(await request('zwave.get_bulbs'))
|
||||
.filter((bulb) => bulb.id_on_network).map((bulb) => bulb.value_id));
|
||||
|
||||
if (this.bulbs.size) {
|
||||
Vue.set(this.views, 'bulbs', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshDoorlocks: async function () {
|
||||
this.doorlocks = new Set(Object.values(await request('zwave.get_doorlocks'))
|
||||
.filter((lock) => lock.id_on_network).map((lock) => lock.value_id));
|
||||
|
||||
if (this.doorlocks.size) {
|
||||
Vue.set(this.views, 'doorlocks', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshUsercodes: async function () {
|
||||
this.doorlocks = new Set(Object.values(await request('zwave.get_usercodes'))
|
||||
.filter((code) => code.id_on_network).map((code) => code.value_id));
|
||||
|
||||
if (this.usercodes.size) {
|
||||
Vue.set(this.views, 'usercodes', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshThermostats: async function () {
|
||||
this.thermostats = new Set(Object.values(await request('zwave.get_thermostats'))
|
||||
.filter((th) => th.id_on_network).map((th) => th.value_id));
|
||||
|
||||
if (this.thermostats.size) {
|
||||
Vue.set(this.views, 'thermostats', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshProtections: async function () {
|
||||
this.protections = new Set(Object.values(await request('zwave.get_protections'))
|
||||
.filter((p) => p.id_on_network).map((p) => p.value_id));
|
||||
|
||||
if (this.protections.size) {
|
||||
Vue.set(this.views, 'protections', true);
|
||||
}
|
||||
},
|
||||
|
||||
refreshStatus: async function() {
|
||||
this.loading.status = true;
|
||||
this.status = await request('zwave.status');
|
||||
this.loading.status = false;
|
||||
},
|
||||
|
||||
refresh: function () {
|
||||
this.views = {
|
||||
nodes: true,
|
||||
scenes: true,
|
||||
};
|
||||
|
||||
this.refreshNodes();
|
||||
this.refreshGroups();
|
||||
this.refreshScenes();
|
||||
this.refreshSwitches();
|
||||
this.refreshDimmers();
|
||||
this.refreshSensors();
|
||||
this.refreshBulbs();
|
||||
this.refreshDoorlocks();
|
||||
this.refreshUsercodes();
|
||||
this.refreshThermostats();
|
||||
this.refreshProtections();
|
||||
this.refreshBatteryLevels();
|
||||
this.refreshPowerLevels();
|
||||
this.refreshStatus();
|
||||
},
|
||||
|
||||
onNodeUpdate: function(event) {
|
||||
Vue.set(this.nodes, event.node.node_id, event.node);
|
||||
},
|
||||
|
||||
onViewChange: function(event) {
|
||||
Vue.set(this.selected, 'view', event.target.value);
|
||||
},
|
||||
|
||||
onNodeClicked: function(event) {
|
||||
Vue.set(this.selected, 'nodeId', event.nodeId === this.selected.nodeId ? undefined : event.nodeId);
|
||||
},
|
||||
|
||||
onGroupClicked: function(event) {
|
||||
Vue.set(this.selected, 'groupId', event.groupId === this.selected.groupId ? undefined : event.groupId);
|
||||
},
|
||||
|
||||
onNetworkInfoModalOpen: function() {
|
||||
this.refreshStatus();
|
||||
this.modal.networkInfo.visible = true;
|
||||
},
|
||||
|
||||
onCommandEvent: function(event) {
|
||||
if (event.error && event.error.length) {
|
||||
createNotification({
|
||||
text: event.state_description + ': ' + event.error_description,
|
||||
error: true,
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
openNetworkCommandsDropdown: function() {
|
||||
openDropdown(this.$refs.networkCommandsDropdown);
|
||||
},
|
||||
|
||||
addNode: async function() {
|
||||
this.commandRunning = true;
|
||||
await request('zwave.add_node');
|
||||
this.commandRunning = false;
|
||||
},
|
||||
|
||||
addToGroup: async function(nodeId, groupId) {
|
||||
this.commandRunning = true;
|
||||
await request('zwave.add_node_to_group', {
|
||||
node_id: nodeId,
|
||||
group_index: groupId,
|
||||
});
|
||||
|
||||
this.commandRunning = false;
|
||||
this.refreshGroups();
|
||||
},
|
||||
|
||||
removeNode: async function() {
|
||||
this.commandRunning = true;
|
||||
await request('zwave.remove_node');
|
||||
this.commandRunning = false;
|
||||
},
|
||||
},
|
||||
|
||||
created: function() {
|
||||
const self = this;
|
||||
this.bus.$on('nodeClicked', this.onNodeClicked);
|
||||
this.bus.$on('groupClicked', this.onGroupClicked);
|
||||
this.bus.$on('openAddToGroupModal', () => {self.modal.group.visible = true});
|
||||
|
||||
registerEventHandler(this.refreshGroups, 'platypush.message.event.zwave.ZwaveNodeGroupEvent');
|
||||
registerEventHandler(this.refreshScenes, 'platypush.message.event.zwave.ZwaveNodeSceneEvent');
|
||||
registerEventHandler(this.refreshNodes, 'platypush.message.event.zwave.ZwaveNodeRemovedEvent');
|
||||
registerEventHandler(this.onCommandEvent, 'platypush.message.event.zwave.ZwaveCommandEvent');
|
||||
|
||||
registerEventHandler(this.refreshStatus,
|
||||
'platypush.message.event.zwave.ZwaveNetworkReadyEvent',
|
||||
'platypush.message.event.zwave.ZwaveNetworkStoppedEvent',
|
||||
'platypush.message.event.zwave.ZwaveNetworkErrorEvent',
|
||||
'platypush.message.event.zwave.ZwaveNetworkResetEvent');
|
||||
|
||||
registerEventHandler(this.onNodeUpdate,
|
||||
'platypush.message.event.zwave.ZwaveNodeEvent',
|
||||
'platypush.message.event.zwave.ZwaveNodeAddedEvent',
|
||||
'platypush.message.event.zwave.ZwaveNodeRenamedEvent',
|
||||
'platypush.message.event.zwave.ZwaveNodeReadyEvent');
|
||||
},
|
||||
|
||||
mounted: function() {
|
||||
this.refresh();
|
||||
},
|
||||
});
|
||||
|
96
platypush/backend/http/static/js/plugins/zwave/node.js
Normal file
96
platypush/backend/http/static/js/plugins/zwave/node.js
Normal file
|
@ -0,0 +1,96 @@
|
|||
Vue.component('zwave-node', {
|
||||
template: '#tmpl-zwave-node',
|
||||
props: ['node','bus','selected'],
|
||||
data: function() {
|
||||
return {
|
||||
editMode: {
|
||||
name: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
|
||||
methods: {
|
||||
onNodeClicked: function() {
|
||||
this.bus.$emit('nodeClicked', {
|
||||
nodeId: this.node.node_id,
|
||||
});
|
||||
},
|
||||
|
||||
removeFailedNode: async function() {
|
||||
if (!confirm('Are you sure that you want to remove this node?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await request('zwave.remove_node', {
|
||||
node_id: this.node.node_id,
|
||||
});
|
||||
},
|
||||
|
||||
replaceFailedNode: async function() {
|
||||
if (!confirm('Are you sure that you want to replace this node?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
await request('zwave.replace_node', {
|
||||
node_id: this.node.node_id,
|
||||
});
|
||||
},
|
||||
|
||||
replicationSend: async function() {
|
||||
await request('zwave.replication_send', {
|
||||
node_id: this.node.node_id,
|
||||
});
|
||||
},
|
||||
|
||||
requestNetworkUpdate: async function() {
|
||||
await request('zwave.request_network_update', {
|
||||
node_id: this.node.node_id,
|
||||
});
|
||||
},
|
||||
|
||||
requestNeighbourUpdate: async function() {
|
||||
await request('zwave.request_node_neighbour_update', {
|
||||
node_id: this.node.node_id,
|
||||
});
|
||||
},
|
||||
|
||||
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('zwave.set_node_name', {
|
||||
node_id: this.node.node_id,
|
||||
new_name: name,
|
||||
});
|
||||
|
||||
this.editMode.name = false;
|
||||
this.enableForm(event.target);
|
||||
},
|
||||
|
||||
heal: async function(event) {
|
||||
await request('zwave.node_heal', {
|
||||
node_id: this.node.node_id,
|
||||
});
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
@ -19,6 +19,7 @@
|
|||
'switches': 'fa fa-toggle-on',
|
||||
'tts': 'fa fa-comment',
|
||||
'tts.google': 'fa fa-comment',
|
||||
'zwave': 'fa fa-zwave',
|
||||
}
|
||||
%}
|
||||
|
||||
|
|
54
platypush/backend/http/templates/plugins/zwave/group.html
Normal file
54
platypush/backend/http/templates/plugins/zwave/group.html
Normal file
|
@ -0,0 +1,54 @@
|
|||
<script type="text/x-template" id="tmpl-zwave-group">
|
||||
<div class="item group" :class="{selected: selected}">
|
||||
<div class="row name vertical-center" :class="{selected: selected}"
|
||||
v-text="group.label" @click="onGroupClicked"></div>
|
||||
|
||||
<div class="params" v-if="selected">
|
||||
<div class="section nodes">
|
||||
<div class="header">
|
||||
<div class="title col-10">Nodes</div>
|
||||
<div class="buttons col-2">
|
||||
<button class="btn btn-default" title="Add to group" @click="bus.$emit('openAddToGroupModal')"
|
||||
v-if="!group.max_associations || Object.keys(nodes).length < group.max_associations">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="row"
|
||||
v-for="node in nodes">
|
||||
<div class="col-10"
|
||||
v-text="node.name && node.name.length ? node.name : '<Node ' + node.node_id + '>'"></div>
|
||||
<div class="buttons col-2">
|
||||
<button class="btn btn-default" title="Remove from group" @click="removeFromGroup(node.node_id)">
|
||||
<i class="fa fa-trash"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="section config">
|
||||
<div class="header">
|
||||
<div class="title">Parameters</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="row">
|
||||
<div class="param-name">Index</div>
|
||||
<div class="param-value" v-text="group.index"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Max associations</div>
|
||||
<div class="param-value" v-text="group.max_associations"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zwave/group.js') }}"></script>
|
||||
|
99
platypush/backend/http/templates/plugins/zwave/index.html
Normal file
99
platypush/backend/http/templates/plugins/zwave/index.html
Normal file
|
@ -0,0 +1,99 @@
|
|||
{% include 'plugins/zwave/node.html' %}
|
||||
{% include 'plugins/zwave/group.html' %}
|
||||
|
||||
<script type="text/x-template" id="tmpl-zwave">
|
||||
<div class="zwave-container">
|
||||
{% include 'plugins/zwave/modals/network.html' %}
|
||||
{% include 'plugins/zwave/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)"
|
||||
:key="view"
|
||||
:selected="view == selected.view"
|
||||
:value="view">
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="buttons">
|
||||
<button class="btn btn-default" title="Add node" v-if="selected.view === 'nodes'"
|
||||
@click="addNode" :disabled="commandRunning">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" title="Remove node" v-if="selected.view === 'nodes'"
|
||||
@click="removeNode" :disabled="commandRunning">
|
||||
<i class="fa fa-minus"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" title="Add scene" v-if="selected.view === 'scenes'"
|
||||
:disabled="commandRunning">
|
||||
<i class="fa fa-plus"></i>
|
||||
</button>
|
||||
|
||||
<button class="btn btn-default" title="Network info" @click="onNetworkInfoModalOpen">
|
||||
<i class="fa fa-info"></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 nodes" v-if="selected.view == 'nodes'">
|
||||
<div class="no-items" v-if="Object.keys(nodes).length == 0">
|
||||
<div class="loading" v-if="loading.nodes">Loading nodes...</div>
|
||||
<div class="empty" v-else>No nodes available on the network</div>
|
||||
</div>
|
||||
|
||||
<zwave-node
|
||||
v-for="node, nodeId in nodes"
|
||||
:key="nodeId"
|
||||
:node="node"
|
||||
:bus="bus"
|
||||
:selected="selected.nodeId == nodeId">
|
||||
</zwave-node>
|
||||
</div>
|
||||
|
||||
<div class="view groups" v-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>
|
||||
|
||||
<zwave-group
|
||||
v-for="group, groupId in groups"
|
||||
:key="groupId"
|
||||
:group="group"
|
||||
:nodes="groupId in groups ? groups[groupId].associations.map((node) => nodes[node]).reduce((nodes, node) => {nodes[node.node_id] = node; return nodes}, {}) : {}"
|
||||
:selected="selected.groupId == groupId"
|
||||
:bus="bus">
|
||||
</zwave-group>
|
||||
</div>
|
||||
|
||||
<div class="view scenes" v-if="selected.view == 'scenes'">
|
||||
<div class="no-items" v-if="Object.keys(scenes).length == 0">
|
||||
<div class="loading" v-if="loading.scenes">Loading scenes...</div>
|
||||
<div class="empty" v-else>No scenes configured on the network</div>
|
||||
</div>
|
||||
|
||||
<!-- <zwave-scenes-->
|
||||
<!-- v-for="scene, sceneId in scenes"-->
|
||||
<!-- :key="sceneId"-->
|
||||
<!-- :name="scene.label"-->
|
||||
<!-- :bus="bus">-->
|
||||
<!-- </zwave-scene>-->
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
<modal id="zwave-add-to-group" title="Add nodes 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 nodes to add</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="row clickable" @click="addToGroup(node.node_id, selected.groupId)" :key="node.node_id"
|
||||
v-for="node in Object.values(nodes).filter((node) => groups[selected.groupId].associations.indexOf(node.node_id) < 0)">
|
||||
<div class="param-name" v-text="node.name"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
|
@ -0,0 +1,35 @@
|
|||
<modal id="zwave-network-info" title="Network info" v-model="modal.networkInfo.visible" v-if="modal.networkInfo.visible">
|
||||
<div class="network-info">
|
||||
<div class="no-items" v-if="loading.status">
|
||||
Loading status...
|
||||
</div>
|
||||
|
||||
<div class="params" v-else>
|
||||
<div class="row">
|
||||
<div class="param-name">State</div>
|
||||
<div class="param-value" v-text="status.state"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Device</div>
|
||||
<div class="param-value" v-text="status.device"></div>
|
||||
</div>
|
||||
|
||||
<div class="section">
|
||||
<div class="header">
|
||||
<div class="title">Statistics</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="row"
|
||||
v-for="value, name in status.stats"
|
||||
:key="name">
|
||||
<div class="param-name" v-text="name"></div>
|
||||
<div class="param-value" v-text="value"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
186
platypush/backend/http/templates/plugins/zwave/node.html
Normal file
186
platypush/backend/http/templates/plugins/zwave/node.html
Normal file
|
@ -0,0 +1,186 @@
|
|||
<script type="text/x-template" id="tmpl-zwave-node">
|
||||
<div class="item node" :class="{selected: selected}">
|
||||
<div class="row name vertical-center" :class="{selected: selected}"
|
||||
v-text="node.name" @click="onNodeClicked"></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="node.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="node.name && node.name.length ? node.name : '<Node ' + node.node_id + '>'"></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" v-if="node.location && node.location.length">
|
||||
<div class="param-name">Location</div>
|
||||
<div class="param-value" v-text="node.location"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Type</div>
|
||||
<div class="param-value" v-text="node.type"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Role</div>
|
||||
<div class="param-value" v-text="node.role"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Node ID</div>
|
||||
<div class="param-value" v-text="node.node_id"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Is Ready</div>
|
||||
<div class="param-value" v-text="node.is_ready"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Is Failed</div>
|
||||
<div class="param-value" v-text="node.is_failed"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Product ID</div>
|
||||
<div class="param-value" v-text="node.manufacturer_id"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Product Type</div>
|
||||
<div class="param-value" v-text="node.product_type"></div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="node.product_name && node.product_name.length">
|
||||
<div class="param-name">Product Name</div>
|
||||
<div class="param-value" v-text="node.product_name"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Manufacturer ID</div>
|
||||
<div class="param-value" v-text="node.manufacturer_id"></div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="node.manufacturer_name && node.manufacturer_name.length">
|
||||
<div class="param-name">Manufacturer Name</div>
|
||||
<div class="param-value" v-text="node.manufacturer_name"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Capabilities</div>
|
||||
<div class="param-value" v-text="node.capabilities.join(', ')"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Command Classes</div>
|
||||
<div class="param-value" v-text="node.command_classes.join(', ')"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Groups</div>
|
||||
<div class="param-value" v-text="Object.values(node.groups).map((g) => g.label || '').join(', ')"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Home ID</div>
|
||||
<div class="param-value" v-text="node.home_id.toString(16)"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Is Awake</div>
|
||||
<div class="param-value" v-text="node.is_awake"></div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="param-name">Is Locked</div>
|
||||
<div class="param-value" v-text="node.is_locked"></div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="node.last_update">
|
||||
<div class="param-name">Last Update</div>
|
||||
<div class="param-value" v-text="node.last_update"></div>
|
||||
</div>
|
||||
|
||||
<div class="row" v-if="node.last_update">
|
||||
<div class="param-name">Max Baud Rate</div>
|
||||
<div class="param-value" v-text="node.max_baud_rate"></div>
|
||||
</div>
|
||||
|
||||
<div class="section actions">
|
||||
<div class="header">
|
||||
<div class="title">Actions</div>
|
||||
</div>
|
||||
|
||||
<div class="body">
|
||||
<div class="row error" v-if="node.is_failed" @click="removeFailedNode">
|
||||
<div class="param-name">Remove Failed Node</div>
|
||||
<div class="param-value">
|
||||
<i class="fa fa-trash"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row error" v-if="node.is_failed" @click="replaceFailedNode">
|
||||
<div class="param-name">Replace Failed Node</div>
|
||||
<div class="param-value">
|
||||
<i class="fa fa-sync-alt"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" @click="heal">
|
||||
<div class="param-name">Heal Node</div>
|
||||
<div class="param-value">
|
||||
<i class="fas fa-wrench"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" @click="replicationSend">
|
||||
<div class="param-name">Replicate info to secondary controller</div>
|
||||
<div class="param-value">
|
||||
<i class="fa fa-clone"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" @click="requestNetworkUpdate">
|
||||
<div class="param-name">Request network update</div>
|
||||
<div class="param-value">
|
||||
<i class="fas fa-wifi"></i>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row" @click="requestNeighbourUpdate">
|
||||
<div class="param-name">Request neighbours update</div>
|
||||
<div class="param-value">
|
||||
<i class="fas fa-network-wired"></i>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</script>
|
||||
|
||||
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zwave/node.js') }}"></script>
|
||||
|
|
@ -52,6 +52,22 @@ class ZwavePlugin(Plugin):
|
|||
backend = self._get_backend()
|
||||
backend.stop_network()
|
||||
|
||||
@action
|
||||
def status(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Get the status of the controller.
|
||||
:return: dict
|
||||
"""
|
||||
backend = self._get_backend()
|
||||
network = self._get_network()
|
||||
controller = self._get_controller()
|
||||
|
||||
return {
|
||||
'device': backend.device,
|
||||
'state': network.state_str,
|
||||
'stats': controller.stats,
|
||||
}
|
||||
|
||||
@action
|
||||
def add_node(self, do_security=False):
|
||||
"""
|
||||
|
@ -189,7 +205,7 @@ class ZwavePlugin(Plugin):
|
|||
'node_id': node.node_id,
|
||||
'home_id': node.home_id,
|
||||
'capabilities': list(node.capabilities),
|
||||
'command_classes': list(node.command_classes),
|
||||
'command_classes': [node.get_command_class_as_string(cc) for cc in node.command_classes],
|
||||
'device_type': node.device_type,
|
||||
'groups': {
|
||||
group_id: cls.group_to_dict(group)
|
||||
|
@ -367,13 +383,13 @@ class ZwavePlugin(Plugin):
|
|||
self._get_controller().kill_command()
|
||||
|
||||
@action
|
||||
def set_controller_name(self, node_name: str):
|
||||
def set_controller_name(self, name: str):
|
||||
"""
|
||||
Set the name of the controller on the network.
|
||||
|
||||
:param node_name: New controller name.
|
||||
:param name: New controller name.
|
||||
"""
|
||||
self._get_controller().name = node_name
|
||||
self._get_controller().name = name
|
||||
self.write_config()
|
||||
|
||||
@action
|
||||
|
@ -613,9 +629,9 @@ class ZwavePlugin(Plugin):
|
|||
return self._get_values('power_levels', node_id=node_id, node_name=node_name)
|
||||
|
||||
@action
|
||||
def get_rgb_bulbs(self, node_id: Optional[int] = None, node_name: Optional[str] = None) -> Dict[int, Any]:
|
||||
def get_bulbs(self, node_id: Optional[int] = None, node_name: Optional[str] = None) -> Dict[int, Any]:
|
||||
"""
|
||||
Get the RGB bulbs/LEDs on the network or associated to a node.
|
||||
Get the bulbs/LEDs on the network or associated to a node.
|
||||
|
||||
:param node_id: Select node by node_id.
|
||||
:param node_name: Select node by name.
|
||||
|
|
Loading…
Reference in a new issue