From fcef7af6a4df5c628323cc51c1091426bdbbaec2 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Mon, 10 Feb 2020 00:39:26 +0100 Subject: [PATCH] Adding Z-Wave web panel (#123) [WIP] --- .../static/css/source/common/elements.scss | 4 + .../source/webpanel/plugins/zwave/index.scss | 195 ++++++++ .../source/webpanel/plugins/zwave/vars.scss | 13 + .../http/static/img/icons/z-wave-logo.png | Bin 0 -> 4034 bytes .../http/static/js/plugins/light.hue/index.js | 8 +- .../http/static/js/plugins/zwave/group.js | 24 + .../http/static/js/plugins/zwave/index.js | 457 ++++++++++++++++++ .../http/static/js/plugins/zwave/node.js | 96 ++++ platypush/backend/http/templates/nav.html | 1 + .../http/templates/plugins/zwave/group.html | 54 +++ .../http/templates/plugins/zwave/index.html | 99 ++++ .../templates/plugins/zwave/modals/group.html | 19 + .../plugins/zwave/modals/network.html | 35 ++ .../http/templates/plugins/zwave/node.html | 186 +++++++ platypush/plugins/zwave/__init__.py | 28 +- 15 files changed, 1209 insertions(+), 10 deletions(-) create mode 100644 platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss create mode 100644 platypush/backend/http/static/css/source/webpanel/plugins/zwave/vars.scss create mode 100644 platypush/backend/http/static/img/icons/z-wave-logo.png create mode 100644 platypush/backend/http/static/js/plugins/zwave/group.js create mode 100644 platypush/backend/http/static/js/plugins/zwave/index.js create mode 100644 platypush/backend/http/static/js/plugins/zwave/node.js create mode 100644 platypush/backend/http/templates/plugins/zwave/group.html create mode 100644 platypush/backend/http/templates/plugins/zwave/index.html create mode 100644 platypush/backend/http/templates/plugins/zwave/modals/group.html create mode 100644 platypush/backend/http/templates/plugins/zwave/modals/network.html create mode 100644 platypush/backend/http/templates/plugins/zwave/node.html diff --git a/platypush/backend/http/static/css/source/common/elements.scss b/platypush/backend/http/static/css/source/common/elements.scss index f07a9d043..d78c2be46 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 000000000..872ab07cf --- /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 000000000..798334a20 --- /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 0000000000000000000000000000000000000000..df9c95505947f6c5ddf9963c8b0448402a65c8c6 GIT binary patch literal 4034 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 M$Yl$O`H6~u11&`Ok^lez literal 0 HcmV?d00001 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 52fc3e6c6..d903a07f3 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 000000000..0410c1afd --- /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 000000000..48aed6dc3 --- /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 000000000..9dc88a447 --- /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 9c1055275..72a80ff1a 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 000000000..f2ae849dc --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/group.html @@ -0,0 +1,54 @@ + + + + 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 000000000..5f0478c21 --- /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' %} + + + 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 000000000..668bb309d --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/modals/group.html @@ -0,0 +1,19 @@ + +
+
+
+
+
Select nodes to add
+
+ +
+
+
+
+
+
+
+
+
+ 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 000000000..1cc7fce98 --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/modals/network.html @@ -0,0 +1,35 @@ + +
+
+ Loading status... +
+ +
+
+
State
+
+
+ +
+
Device
+
+
+ +
+
+
Statistics
+
+ +
+
+
+
+
+
+
+
+
+
+ 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 000000000..ea59ba14b --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/node.html @@ -0,0 +1,186 @@ + + + + diff --git a/platypush/plugins/zwave/__init__.py b/platypush/plugins/zwave/__init__.py index 2b88c357d..a08606fe0 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.