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)