From 98727c4f31f52c19f9b11382a1505c597d690a90 Mon Sep 17 00:00:00 2001 From: Fabio Manganiello Date: Thu, 20 Feb 2020 02:34:28 +0100 Subject: [PATCH] Added support for values in Z-Wave web panel (see #123) --- .../source/webpanel/plugins/zwave/index.scss | 63 +++++++ .../backend/http/static/js/elements/switch.js | 3 +- .../http/static/js/plugins/zwave/index.js | 172 +++++------------- .../http/static/js/plugins/zwave/value.js | 73 ++++++++ .../http/templates/plugins/zwave/index.html | 24 ++- .../http/templates/plugins/zwave/value.html | 110 +++++++++++ platypush/backend/zwave/__init__.py | 2 + platypush/message/__init__.py | 3 + platypush/plugins/zwave/__init__.py | 50 ++++- 9 files changed, 356 insertions(+), 144 deletions(-) create mode 100644 platypush/backend/http/static/js/plugins/zwave/value.js create mode 100644 platypush/backend/http/templates/plugins/zwave/value.html 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 index 872ab07c..42092210 100644 --- a/platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss +++ b/platypush/backend/http/static/css/source/webpanel/plugins/zwave/index.scss @@ -17,6 +17,7 @@ display: flex; flex-direction: column; align-items: center; + overflow: auto; .no-items { padding: 2em; @@ -156,9 +157,71 @@ display: inline-block; width: 58%; text-align: right; + + .value-edit { + display: flex; + align-items: center; + } + + .value-data { + display: inline-block; + font-weight: bold; + } + + .slider-container { + display: flex; + align-items: center; + } + + .unit { + font-size: .8em; + margin-left: 1em; + display: inline; + } + + select { + width: 100%; + border-radius: 2em; + } + + .numeric { + input.slider { + text-align: left; + } + + input[type=text] { + text-align: right; + width: 100%; + } + + .row { + background: none; + &:hover { + background: none; + } + } + + .value-min, .value-max { + width: 50%; + font-size: .85em; + opacity: .75; + } + + .value-min { + text-align: left; + } + + .value-max { + text-align: right; + } + } } } + .btn-value-name-edit { + padding: 0; + } + .modal { .section { .header { diff --git a/platypush/backend/http/static/js/elements/switch.js b/platypush/backend/http/static/js/elements/switch.js index f89263fb..ced82ced 100644 --- a/platypush/backend/http/static/js/elements/switch.js +++ b/platypush/backend/http/static/js/elements/switch.js @@ -6,7 +6,8 @@ Vue.component('toggle-switch', { toggled: function(event) { this.$emit('toggled', { id: this.id, - value: !this.value + value: !this.value, + event: event, }); }, }, diff --git a/platypush/backend/http/static/js/plugins/zwave/index.js b/platypush/backend/http/static/js/plugins/zwave/index.js index 48aed6dc..8c1e7550 100644 --- a/platypush/backend/http/static/js/plugins/zwave/index.js +++ b/platypush/backend/http/static/js/plugins/zwave/index.js @@ -10,31 +10,30 @@ Vue.component('zwave', { 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, + values: { + switches: {}, + dimmers: {}, + sensors: {}, + battery_levels: {}, + power_levels: {}, + bulbs: {}, + doorlocks: {}, + usercodes: {}, + thermostats: {}, + protections: {}, + }, selected: { view: 'nodes', nodeId: undefined, groupId: undefined, sceneId: undefined, - valueId: undefined, }, loading: { status: false, nodes: false, groups: false, scenes: false, - values: false, }, modal: { networkInfo: { @@ -198,28 +197,11 @@ Vue.component('zwave', { 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; + if (Object.keys(this.nodes).length) { + Vue.set(this.views, 'values', true); + } }, refreshGroups: async function () { @@ -250,93 +232,16 @@ Vue.component('zwave', { 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)); + refreshValues: async function(type) { + Vue.set(this.values, type, Object.values(await request('zwave.get_' + type)) + .filter((item) => item.id_on_network) + .reduce((values, value) => { + values[value.id_on_network] = true; + return values; + }, {})); - 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); + if (Object.keys(this.values[type]).length) { + Vue.set(this.views, type, true); } }, @@ -355,16 +260,17 @@ Vue.component('zwave', { 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.refreshValues('switches'); + this.refreshValues('dimmers'); + this.refreshValues('sensors'); + this.refreshValues('bulbs'); + this.refreshValues('doorlocks'); + this.refreshValues('usercodes'); + this.refreshValues('thermostats'); + this.refreshValues('protections'); + this.refreshValues('battery_levels'); + this.refreshValues('power_levels'); + this.refreshValues('node_config'); this.refreshStatus(); }, @@ -428,6 +334,8 @@ Vue.component('zwave', { created: function() { const self = this; + this.bus.$on('refresh', this.refresh); + this.bus.$on('refreshNodes', this.refreshNodes); this.bus.$on('nodeClicked', this.onNodeClicked); this.bus.$on('groupClicked', this.onGroupClicked); this.bus.$on('openAddToGroupModal', () => {self.modal.group.visible = true}); @@ -447,7 +355,11 @@ Vue.component('zwave', { 'platypush.message.event.zwave.ZwaveNodeEvent', 'platypush.message.event.zwave.ZwaveNodeAddedEvent', 'platypush.message.event.zwave.ZwaveNodeRenamedEvent', - 'platypush.message.event.zwave.ZwaveNodeReadyEvent'); + 'platypush.message.event.zwave.ZwaveNodeReadyEvent', + 'platypush.message.event.zwave.ZwaveValueAddedEvent', + 'platypush.message.event.zwave.ZwaveValueChangedEvent', + 'platypush.message.event.zwave.ZwaveValueRemovedEvent', + 'platypush.message.event.zwave.ZwaveValueRefreshedEvent'); }, mounted: function() { diff --git a/platypush/backend/http/static/js/plugins/zwave/value.js b/platypush/backend/http/static/js/plugins/zwave/value.js new file mode 100644 index 00000000..90adf5b4 --- /dev/null +++ b/platypush/backend/http/static/js/plugins/zwave/value.js @@ -0,0 +1,73 @@ +Vue.component('zwave-value', { + template: '#tmpl-zwave-value', + props: ['node','bus','selected','values'], + data: function() { + return { + }; + }, + + methods: { + onNodeClicked: function() { + this.bus.$emit('nodeClicked', { + nodeId: 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: function(event) { + const value = this.node.values[event.target.parentElement.dataset.idOnNetwork]; + const name = prompt('New name', value.label); + + if (!name || !name.length || name === value.label) { + return; + } + + request('zwave.set_value_label', { + id_on_network: value.id_on_network, + new_label: name, + }).then(() => { + this.bus.$emit('refreshNodes'); + createNotification({ + text: 'Value successfully renamed', + image: { icon: 'check' } + }); + }); + }, + + onValueChanged: function(event) { + const target = event.target ? event.target : event.event.target.parentElement; + const value = this.node.values[target.dataset.idOnNetwork]; + const data = value.type === 'List' ? value.data_items[event.target.value] : (target.value || event.value); + + request('zwave.set_value', { + id_on_network: value.id_on_network, + data: data, + }).then(() => { + this.bus.$emit('refreshNodes'); + createNotification({ + text: 'Value successfully modified', + image: { icon: 'check' } + }); + }); + }, + }, +}); + diff --git a/platypush/backend/http/templates/plugins/zwave/index.html b/platypush/backend/http/templates/plugins/zwave/index.html index 5f0478c2..5a2b6ebb 100644 --- a/platypush/backend/http/templates/plugins/zwave/index.html +++ b/platypush/backend/http/templates/plugins/zwave/index.html @@ -1,5 +1,6 @@ {% include 'plugins/zwave/node.html' %} {% include 'plugins/zwave/group.html' %} +{% include 'plugins/zwave/value.html' %} diff --git a/platypush/backend/http/templates/plugins/zwave/value.html b/platypush/backend/http/templates/plugins/zwave/value.html new file mode 100644 index 00000000..8f1a57f7 --- /dev/null +++ b/platypush/backend/http/templates/plugins/zwave/value.html @@ -0,0 +1,110 @@ + + + + diff --git a/platypush/backend/zwave/__init__.py b/platypush/backend/zwave/__init__.py index 431b9f49..77cc3d38 100644 --- a/platypush/backend/zwave/__init__.py +++ b/platypush/backend/zwave/__init__.py @@ -241,6 +241,8 @@ class ZwaveBackend(Backend): event = ZwaveValueRemovedEvent(device=self.device, node=ZwavePlugin.node_to_dict(event.args['node']), value=ZwavePlugin.value_to_dict(event.args['value'])) + else: + self.logger.info('Received unhandled ZWave event: {}'.format(event)) if isinstance(event, ZwaveEvent): self.bus.post(event) diff --git a/platypush/message/__init__.py b/platypush/message/__init__.py index c63d1225..f341e372 100644 --- a/platypush/message/__init__.py +++ b/platypush/message/__init__.py @@ -17,6 +17,9 @@ class Message(object): isinstance(obj, datetime.time): return obj.isoformat() + if isinstance(obj, set): + return list(obj) + return super().default(obj) def __init__(self, timestamp=None, *args, **kwargs): diff --git a/platypush/plugins/zwave/__init__.py b/platypush/plugins/zwave/__init__.py index a08606fe..7a2cb61c 100644 --- a/platypush/plugins/zwave/__init__.py +++ b/platypush/plugins/zwave/__init__.py @@ -150,13 +150,14 @@ class ZwavePlugin(Plugin): controller.request_node_neighbor_update(node.node_id) @staticmethod - def value_to_dict(value) -> Dict[str, Any]: + def value_to_dict(value: Optional[ZWaveValue]) -> Dict[str, Any]: if not value: return {} return { - 'command_class': value.command_class, + 'command_class': value.node.get_command_class_as_string(value.command_class), 'data': value.data, + 'data_as_string': value.data_as_string, 'data_items': list(value.data_items) if isinstance(value.data_items, set) else value.data_items, 'genre': value.genre, 'help': value.help, @@ -185,7 +186,7 @@ class ZwavePlugin(Plugin): } @staticmethod - def group_to_dict(group) -> Dict[str, Any]: + def group_to_dict(group: Optional[ZWaveGroup]) -> Dict[str, Any]: if not group: return {} @@ -197,7 +198,7 @@ class ZwavePlugin(Plugin): } @classmethod - def node_to_dict(cls, node) -> Dict[str, Any]: + def node_to_dict(cls, node: Optional[ZWaveNode]) -> Dict[str, Any]: if not node: return {} @@ -240,7 +241,7 @@ class ZwavePlugin(Plugin): 'use_cache': node.use_cache, 'version': node.version, 'values': { - value_id: cls.value_to_dict(value) + value.id_on_network: cls.value_to_dict(value) for value_id, value in (node.values or {}).items() }, } @@ -445,16 +446,22 @@ class ZwavePlugin(Plugin): node_id: Optional[int] = None, node_name: Optional[str] = None, value_label: Optional[str] = None) \ -> ZWaveValue: assert (value_id is not None or id_on_network is not None) or \ - (node_id is not None and node_name is not None and value_label is not None),\ + ((node_id is not None or node_name is not None) and value_label is not None),\ 'Specify either value_id, id_on_network, or [node_id/node_name, value_label]' if value_id is not None: return self._get_network().get_value(value_id) if id_on_network is not None: - return self._get_network().get_value_from_id_on_network(id_on_network) + values = [value + for node in self._get_network().nodes.values() + for value in node.values.values() + if value.id_on_network == id_on_network] + + assert values, 'No such value ID: {}'.format(id_on_network) + return values[0] node = self._get_node(node_id=node_id, node_name=node_name) - values = [v for v in node.values if v.label == value_label] + values = [v for v in node.values.values() if v.label == value_label] assert values, 'No such value on node "{}": "{}"'.format(node.name, value_label) return values[0] @@ -489,7 +496,30 @@ class ZwavePlugin(Plugin): """ value = self._get_value(value_id=value_id, id_on_network=id_on_network, node_id=node_id, node_name=node_name, value_label=value_label) - value.data = data + new_val = value.check_data(data) + assert new_val is not None, 'Invalid value passed to the property' + node: ZWaveNode = self._get_network().nodes[value.node.node_id] + node.values[value.value_id].data = new_val + self.write_config() + + @action + def set_value_label(self, new_label: str, value_id: Optional[int] = None, id_on_network: Optional[str] = None, + value_label: Optional[str] = None, node_id: Optional[int] = None, + node_name: Optional[str] = None): + """ + Change the label/name of a value. + + :param new_label: New value label. + :param value_id: Select value by value_id. + :param id_on_network: Select value by id_on_network. + :param value_label: Select value by [node_id/node_name, value_label] + :param node_id: Select value by [node_id/node_name, value_label] + :param node_name: Select value by [node_id/node_name, value_label] + """ + value = self._get_value(value_id=value_id, id_on_network=id_on_network, + node_id=node_id, node_name=node_name, value_label=value_label) + value.label = new_label + self.write_config() @action def node_add_value(self, value_id: Optional[int] = None, id_on_network: Optional[str] = None, @@ -579,7 +609,7 @@ class ZwavePlugin(Plugin): else self._get_network().nodes.values() return { - value_id: { + value.id_on_network: { 'node_id': node.node_id, 'node_name': node.name, **self.value_to_dict(value)