diff --git a/platypush/backend/http/static/css/source/common/elements.scss b/platypush/backend/http/static/css/source/common/elements.scss index f07a9d04..d78c2be4 100644 --- a/platypush/backend/http/static/css/source/common/elements.scss +++ b/platypush/backend/http/static/css/source/common/elements.scss @@ -11,6 +11,10 @@ text-align: right !important; } +.clickable { + cursor: pointer; +} + a:focus { outline: none; } diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss new file mode 100644 index 00000000..872ab07c --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss @@ -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; + } + } +} + diff --git a/platypush/backend/http/static/css/source/webpanel/plugins/zwave/vars.scss b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/vars.scss new file mode 100644 index 00000000..798334a2 --- /dev/null +++ b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/vars.scss @@ -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; + diff --git a/platypush/backend/http/static/img/icons/z-wave-logo.png b/platypush/backend/http/static/img/icons/z-wave-logo.png new file mode 100644 index 00000000..df9c9550 Binary files /dev/null and b/platypush/backend/http/static/img/icons/z-wave-logo.png differ diff --git a/platypush/backend/http/static/js/plugins/light.hue/index.js b/platypush/backend/http/static/js/plugins/light.hue/index.js index 52fc3e6c..d903a07f 100644 --- a/platypush/backend/http/static/js/plugins/light.hue/index.js +++ b/platypush/backend/http/static/js/plugins/light.hue/index.js @@ -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, diff --git a/platypush/backend/http/static/js/plugins/zwave/group.js b/platypush/backend/http/static/js/plugins/zwave/group.js new file mode 100644 index 00000000..0410c1af --- /dev/null +++ b/platypush/backend/http/static/js/plugins/zwave/group.js @@ -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, + }); + }, + }, +}); + diff --git a/platypush/backend/http/static/js/plugins/zwave/index.js b/platypush/backend/http/static/js/plugins/zwave/index.js new file mode 100644 index 00000000..48aed6dc --- /dev/null +++ b/platypush/backend/http/static/js/plugins/zwave/index.js @@ -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(); + }, +}); + diff --git a/platypush/backend/http/static/js/plugins/zwave/node.js b/platypush/backend/http/static/js/plugins/zwave/node.js new file mode 100644 index 00000000..9dc88a44 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/zwave/node.js @@ -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, + }); + }, + }, +}); + diff --git a/platypush/backend/http/templates/nav.html b/platypush/backend/http/templates/nav.html index 9c105527..72a80ff1 100644 --- a/platypush/backend/http/templates/nav.html +++ b/platypush/backend/http/templates/nav.html @@ -19,6 +19,7 @@ 'switches': 'fa fa-toggle-on', 'tts': 'fa fa-comment', 'tts.google': 'fa fa-comment', + 'zwave': 'fa fa-zwave', } %} diff --git a/platypush/backend/http/templates/plugins/zwave/group.html b/platypush/backend/http/templates/plugins/zwave/group.html new file mode 100644 index 00000000..f2ae849d --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/group.html @@ -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> + diff --git a/platypush/backend/http/templates/plugins/zwave/index.html b/platypush/backend/http/templates/plugins/zwave/index.html new file mode 100644 index 00000000..5f0478c2 --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/index.html @@ -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> + diff --git a/platypush/backend/http/templates/plugins/zwave/modals/group.html b/platypush/backend/http/templates/plugins/zwave/modals/group.html new file mode 100644 index 00000000..668bb309 --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/modals/group.html @@ -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> + diff --git a/platypush/backend/http/templates/plugins/zwave/modals/network.html b/platypush/backend/http/templates/plugins/zwave/modals/network.html new file mode 100644 index 00000000..1cc7fce9 --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/modals/network.html @@ -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> + diff --git a/platypush/backend/http/templates/plugins/zwave/node.html b/platypush/backend/http/templates/plugins/zwave/node.html new file mode 100644 index 00000000..ea59ba14 --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/node.html @@ -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> + diff --git a/platypush/plugins/zwave/__init__.py b/platypush/plugins/zwave/__init__.py index 2b88c357..a08606fe 100644 --- a/platypush/plugins/zwave/__init__.py +++ b/platypush/plugins/zwave/__init__.py @@ -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.