Added support for values in Z-Wave web panel (see #123)

This commit is contained in:
Fabio Manganiello 2020-02-20 02:34:28 +01:00
parent a0ceb560b4
commit 98727c4f31
9 changed files with 356 additions and 144 deletions

View File

@ -17,6 +17,7 @@
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
overflow: auto;
.no-items { .no-items {
padding: 2em; padding: 2em;
@ -156,9 +157,71 @@
display: inline-block; display: inline-block;
width: 58%; width: 58%;
text-align: right; 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 { .modal {
.section { .section {
.header { .header {

View File

@ -6,7 +6,8 @@ Vue.component('toggle-switch', {
toggled: function(event) { toggled: function(event) {
this.$emit('toggled', { this.$emit('toggled', {
id: this.id, id: this.id,
value: !this.value value: !this.value,
event: event,
}); });
}, },
}, },

View File

@ -10,31 +10,30 @@ Vue.component('zwave', {
nodes: {}, nodes: {},
groups: {}, groups: {},
scenes: {}, 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, commandRunning: false,
values: {
switches: {},
dimmers: {},
sensors: {},
battery_levels: {},
power_levels: {},
bulbs: {},
doorlocks: {},
usercodes: {},
thermostats: {},
protections: {},
},
selected: { selected: {
view: 'nodes', view: 'nodes',
nodeId: undefined, nodeId: undefined,
groupId: undefined, groupId: undefined,
sceneId: undefined, sceneId: undefined,
valueId: undefined,
}, },
loading: { loading: {
status: false, status: false,
nodes: false, nodes: false,
groups: false, groups: false,
scenes: false, scenes: false,
values: false,
}, },
modal: { modal: {
networkInfo: { networkInfo: {
@ -198,28 +197,11 @@ Vue.component('zwave', {
methods: { methods: {
refreshNodes: async function () { refreshNodes: async function () {
this.loading.nodes = true; this.loading.nodes = true;
this.loading.values = true;
this.nodes = await request('zwave.get_nodes'); this.nodes = await request('zwave.get_nodes');
this.loading.nodes = false;
this.values = Object.values(this.nodes).reduce((values, node) => { if (Object.keys(this.nodes).length) {
values = { Vue.set(this.views, 'values', true);
...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 () { refreshGroups: async function () {
@ -250,93 +232,16 @@ Vue.component('zwave', {
this.loading.scenes = false; this.loading.scenes = false;
}, },
refreshSwitches: async function () { refreshValues: async function(type) {
this.switches = new Set(Object.values(await request('zwave.get_switches')) Vue.set(this.values, type, Object.values(await request('zwave.get_' + type))
.filter((sw) => sw.id_on_network).map((sw) => sw.value_id)); .filter((item) => item.id_on_network)
.reduce((values, value) => {
values[value.id_on_network] = true;
return values;
}, {}));
if (this.switches.size) { if (Object.keys(this.values[type]).length) {
Vue.set(this.views, 'switches', true); Vue.set(this.views, type, 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);
} }
}, },
@ -355,16 +260,17 @@ Vue.component('zwave', {
this.refreshNodes(); this.refreshNodes();
this.refreshGroups(); this.refreshGroups();
this.refreshScenes(); this.refreshScenes();
this.refreshSwitches(); this.refreshValues('switches');
this.refreshDimmers(); this.refreshValues('dimmers');
this.refreshSensors(); this.refreshValues('sensors');
this.refreshBulbs(); this.refreshValues('bulbs');
this.refreshDoorlocks(); this.refreshValues('doorlocks');
this.refreshUsercodes(); this.refreshValues('usercodes');
this.refreshThermostats(); this.refreshValues('thermostats');
this.refreshProtections(); this.refreshValues('protections');
this.refreshBatteryLevels(); this.refreshValues('battery_levels');
this.refreshPowerLevels(); this.refreshValues('power_levels');
this.refreshValues('node_config');
this.refreshStatus(); this.refreshStatus();
}, },
@ -428,6 +334,8 @@ Vue.component('zwave', {
created: function() { created: function() {
const self = this; const self = this;
this.bus.$on('refresh', this.refresh);
this.bus.$on('refreshNodes', this.refreshNodes);
this.bus.$on('nodeClicked', this.onNodeClicked); this.bus.$on('nodeClicked', this.onNodeClicked);
this.bus.$on('groupClicked', this.onGroupClicked); this.bus.$on('groupClicked', this.onGroupClicked);
this.bus.$on('openAddToGroupModal', () => {self.modal.group.visible = true}); 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.ZwaveNodeEvent',
'platypush.message.event.zwave.ZwaveNodeAddedEvent', 'platypush.message.event.zwave.ZwaveNodeAddedEvent',
'platypush.message.event.zwave.ZwaveNodeRenamedEvent', '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() { mounted: function() {

View File

@ -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' }
});
});
},
},
});

View File

@ -1,5 +1,6 @@
{% include 'plugins/zwave/node.html' %} {% include 'plugins/zwave/node.html' %}
{% include 'plugins/zwave/group.html' %} {% include 'plugins/zwave/group.html' %}
{% include 'plugins/zwave/value.html' %}
<script type="text/x-template" id="tmpl-zwave"> <script type="text/x-template" id="tmpl-zwave">
<div class="zwave-container"> <div class="zwave-container">
@ -10,7 +11,7 @@
<div class="view-selector col-s-9 col-m-10 col-l-11"> <div class="view-selector col-s-9 col-m-10 col-l-11">
<select @change="onViewChange"> <select @change="onViewChange">
<option v-for="_, view in views" <option v-for="_, view in views"
v-text="view[0].toUpperCase() + view.slice(1)" v-text="(view[0].toUpperCase() + view.slice(1)).replace('_', ' ')"
:key="view" :key="view"
:selected="view == selected.view" :selected="view == selected.view"
:value="view"> :value="view">
@ -65,7 +66,7 @@
</zwave-node> </zwave-node>
</div> </div>
<div class="view groups" v-if="selected.view == 'groups'"> <div class="view groups" v-else-if="selected.view == 'groups'">
<div class="no-items" v-if="Object.keys(groups).length == 0"> <div class="no-items" v-if="Object.keys(groups).length == 0">
<div class="loading" v-if="loading.groups">Loading groups...</div> <div class="loading" v-if="loading.groups">Loading groups...</div>
<div class="empty" v-else>No groups available on the network</div> <div class="empty" v-else>No groups available on the network</div>
@ -81,7 +82,7 @@
</zwave-group> </zwave-group>
</div> </div>
<div class="view scenes" v-if="selected.view == 'scenes'"> <div class="view scenes" v-else-if="selected.view == 'scenes'">
<div class="no-items" v-if="Object.keys(scenes).length == 0"> <div class="no-items" v-if="Object.keys(scenes).length == 0">
<div class="loading" v-if="loading.scenes">Loading scenes...</div> <div class="loading" v-if="loading.scenes">Loading scenes...</div>
<div class="empty" v-else>No scenes configured on the network</div> <div class="empty" v-else>No scenes configured on the network</div>
@ -94,6 +95,23 @@
<!-- :bus="bus">--> <!-- :bus="bus">-->
<!-- </zwave-scene>--> <!-- </zwave-scene>-->
</div> </div>
<div class="view values" v-else>
<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 found on the network</div>
</div>
<zwave-value
v-for="node, nodeId in nodes"
v-if="selected.view === 'values' || Object.values(node.values).filter((value) => value.id_on_network in values[selected.view]).length > 0"
:key="nodeId"
:values="Object.values(node.values).filter((value) => selected.view === 'values' || value.id_on_network in values[selected.view]).map((value) => value.id_on_network)"
:node="node"
:selected="selected.nodeId == nodeId"
:bus="bus">
</zwave-value>
</div>
</div> </div>
</script> </script>

View File

@ -0,0 +1,110 @@
<script type="text/x-template" id="tmpl-zwave-value">
<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="section value"
v-for="value in node.values"
v-if="values.indexOf(value.id_on_network) >= 0"
:key="value.id_on_network">
<div class="header">
<div class="title">
<button class="btn btn-default btn-value-name-edit" title="Edit value name"
:data-id-on-network="value.id_on_network" @click="editName">
<i class="fa fa-edit"></i>
</button>
{% raw %}{{ value.label }}{% endraw %}
</div>
</div>
<div class="body">
<div class="row">
<div class="param-name">Value</div>
<div class="param-value">
<div class="value-view" v-if="value.is_read_only">
<div class="value-data" v-text="value.data" ></div>
<div class="unit" v-text="value.units" v-if="value.units && value.units.length">
&nbsp; {% raw %}{{ value.units }}{% endraw %}}
</div>
</div>
<div class="value-edit" v-else>
<div :class="['col-' + (value.units && value.units.length ? '11' : '12')]">
<div class="list" v-if="value.type === 'List'">
<select @change="onValueChanged"
:data-id-on-network="value.id_on_network">
<option v-for="data, index in value.data_items"
v-text="data"
:key="index"
:selected="value.data == data"
:value="index">
</option>
</select>
</div>
<div class="numeric slider-container" v-else-if="['Byte', 'Decimal', 'Short'].indexOf(value.type) >= 0">
<div class="col-10">
<div class="row">
<span class="value-min" v-text="value.min"></span>
<span class="value-max" v-text="value.max"></span>
</div>
<div class="row">
<input class="slider" type="range" :min="value.min" :max="value.max"
:value="value.data" :data-id-on-network="value.id_on_network"
@change="onValueChanged">
</div>
</div>
<div class="col-2">
<input type="text" :data-id-on-network="value.id_on_network" :value="value.data"
@change="onValueChanged">
</div>
</div>
<div class="boolean" v-else-if="['Bool', 'Button'].indexOf(value.type) >= 0">
<toggle-switch :value="value.data" :data-id-on-network="value.id_on_network"
@toggled="onValueChanged"></toggle-switch>
</div>
<div class="value-data" v-text="value.data" v-else></div>
</div>
<div class="col-1 unit" v-text="value.units" v-if="value.units && value.units.length">
&nbsp; {% raw %}{{ value.units }}{% endraw %}}
</div>
</div>
</div>
</div>
<div class="row" v-if="value.help && value.help.length">
<div class="param-name">Help</div>
<div class="param-value" v-text="value.help"></div>
</div>
<div class="row">
<div class="param-name">Value ID</div>
<div class="param-value" v-text="value.value_id"></div>
</div>
<div class="row">
<div class="param-name">ID on Network</div>
<div class="param-value" v-text="value.id_on_network"></div>
</div>
<div class="row">
<div class="param-name">Command Class</div>
<div class="param-value" v-text="value.command_class"></div>
</div>
<div class="row" v-if="value.last_update">
<div class="param-name">Last Update</div>
<div class="param-value" v-text="value.last_update"></div>
</div>
</div>
</div>
</div>
</div>
</script>
<script type="application/javascript" src="{{ url_for('static', filename='js/plugins/zwave/value.js') }}"></script>

View File

@ -241,6 +241,8 @@ class ZwaveBackend(Backend):
event = ZwaveValueRemovedEvent(device=self.device, event = ZwaveValueRemovedEvent(device=self.device,
node=ZwavePlugin.node_to_dict(event.args['node']), node=ZwavePlugin.node_to_dict(event.args['node']),
value=ZwavePlugin.value_to_dict(event.args['value'])) value=ZwavePlugin.value_to_dict(event.args['value']))
else:
self.logger.info('Received unhandled ZWave event: {}'.format(event))
if isinstance(event, ZwaveEvent): if isinstance(event, ZwaveEvent):
self.bus.post(event) self.bus.post(event)

View File

@ -17,6 +17,9 @@ class Message(object):
isinstance(obj, datetime.time): isinstance(obj, datetime.time):
return obj.isoformat() return obj.isoformat()
if isinstance(obj, set):
return list(obj)
return super().default(obj) return super().default(obj)
def __init__(self, timestamp=None, *args, **kwargs): def __init__(self, timestamp=None, *args, **kwargs):

View File

@ -150,13 +150,14 @@ class ZwavePlugin(Plugin):
controller.request_node_neighbor_update(node.node_id) controller.request_node_neighbor_update(node.node_id)
@staticmethod @staticmethod
def value_to_dict(value) -> Dict[str, Any]: def value_to_dict(value: Optional[ZWaveValue]) -> Dict[str, Any]:
if not value: if not value:
return {} return {}
return { return {
'command_class': value.command_class, 'command_class': value.node.get_command_class_as_string(value.command_class),
'data': value.data, '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, 'data_items': list(value.data_items) if isinstance(value.data_items, set) else value.data_items,
'genre': value.genre, 'genre': value.genre,
'help': value.help, 'help': value.help,
@ -185,7 +186,7 @@ class ZwavePlugin(Plugin):
} }
@staticmethod @staticmethod
def group_to_dict(group) -> Dict[str, Any]: def group_to_dict(group: Optional[ZWaveGroup]) -> Dict[str, Any]:
if not group: if not group:
return {} return {}
@ -197,7 +198,7 @@ class ZwavePlugin(Plugin):
} }
@classmethod @classmethod
def node_to_dict(cls, node) -> Dict[str, Any]: def node_to_dict(cls, node: Optional[ZWaveNode]) -> Dict[str, Any]:
if not node: if not node:
return {} return {}
@ -240,7 +241,7 @@ class ZwavePlugin(Plugin):
'use_cache': node.use_cache, 'use_cache': node.use_cache,
'version': node.version, 'version': node.version,
'values': { '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() 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) \ node_id: Optional[int] = None, node_name: Optional[str] = None, value_label: Optional[str] = None) \
-> ZWaveValue: -> ZWaveValue:
assert (value_id is not None or id_on_network is not None) or \ 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]' 'Specify either value_id, id_on_network, or [node_id/node_name, value_label]'
if value_id is not None: if value_id is not None:
return self._get_network().get_value(value_id) return self._get_network().get_value(value_id)
if id_on_network is not None: 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) 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) assert values, 'No such value on node "{}": "{}"'.format(node.name, value_label)
return values[0] return values[0]
@ -489,7 +496,30 @@ class ZwavePlugin(Plugin):
""" """
value = self._get_value(value_id=value_id, id_on_network=id_on_network, 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) 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 @action
def node_add_value(self, value_id: Optional[int] = None, id_on_network: Optional[str] = None, 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() else self._get_network().nodes.values()
return { return {
value_id: { value.id_on_network: {
'node_id': node.node_id, 'node_id': node.node_id,
'node_name': node.name, 'node_name': node.name,
**self.value_to_dict(value) **self.value_to_dict(value)