From 4234c6f6c2f00dee9d9fcde0d988f2844fac3808 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 08:40:39 +0100 Subject: [PATCH 01/17] feat: support start stop commands for multilevel switch --- lib/Gateway.js | 7 +++++-- lib/ZwaveClient.js | 20 +++++++++++++++++--- 2 files changed, 22 insertions(+), 5 deletions(-) diff --git a/lib/Gateway.js b/lib/Gateway.js index 0ef5843a23..181d081bf8 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -627,8 +627,11 @@ Gateway.prototype.parsePayload = function (payload, valueId, valueConf) { if (isDimmer) { // TODO: should we use valueId.max instead of 99 ? - if (typeof payload === 'boolean') payload = payload ? 99 : 0 - else payload = Math.round((payload / 255) * 99) + if (typeof payload === 'boolean') { + payload = payload ? 99 : 0 + } else if (typeof payload === 'number') { + payload = Math.round((payload / 255) * 99) + } } // map modes coming from hass diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index 80673818ea..c3b66674ac 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -9,7 +9,7 @@ const { InterviewStage, extractFirmware } = require('zwave-js') -const { applicationCCs } = require('@zwave-js/core') +const { applicationCCs, CommandClasses } = require('@zwave-js/core') const utils = reqlib('/lib/utils.js') const EventEmitter = require('events') const jsonStore = reqlib('/lib/jsonStore.js') @@ -1899,12 +1899,26 @@ ZwaveClient.prototype.writeValue = async function (valueId, value) { if (this.driverReady) { let result = false - if (valueId.type === 'number' && typeof value === 'string') { + if (valueId.type === 'number' && typeof value === 'string' && !isNaN(value)) { value = Number(value) } try { - result = await this.getNode(valueId.nodeId).setValue(valueId, value) + const zwaveNode = await this.getNode(valueId.nodeId) + + // handle multilevel switch 'start' and 'stop' commands + if (valueId.commandClass === CommandClasses['Multilevel Switch'] && isNaN(value)) { + if (/stop/i.test(value)) { + await zwaveNode.commandClasses['Multilevel Switch'].stopLevelChange() + } else if (/start/i.test(value)) { + await zwaveNode.commandClasses['Multilevel Switch'].startLevelChange() + } else { + throw Error('Command not valid for Multilevel Switch') + } + result = true + } else { + result = await this.getNode(valueId.nodeId).setValue(valueId, value) + } } catch (error) { debug(`Error while writing ${value} on ${valueId.id}: ${error.message}`) } From abcede28ae0fe4a6b3db3e72950a12ecdc361bc5 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 08:43:25 +0100 Subject: [PATCH 02/17] fix: use isNaN --- lib/Gateway.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/Gateway.js b/lib/Gateway.js index 181d081bf8..5a1b66198b 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -629,7 +629,7 @@ Gateway.prototype.parsePayload = function (payload, valueId, valueConf) { // TODO: should we use valueId.max instead of 99 ? if (typeof payload === 'boolean') { payload = payload ? 99 : 0 - } else if (typeof payload === 'number') { + } else if (!isNaN(payload)) { payload = Math.round((payload / 255) * 99) } } From f8d7614d5f71a247e589b1bb73ed7ea3a6993310 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 09:06:58 +0100 Subject: [PATCH 03/17] fix: update valueIds on metadata updated --- lib/ZwaveClient.js | 52 ++++++++++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 22 deletions(-) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index c3b66674ac..d3c8ea2c48 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -431,8 +431,8 @@ function onNodeValueRemoved (zwaveNode, args) { } function onNodeMetadataUpdated (zwaveNode, args) { - updateValueMetadata.call(this, zwaveNode, args, args.metadata) - debug(`Node ${zwaveNode.id}: metadata updated: ${getValueID(args)}`) + const valueId = initValue.call(this, zwaveNode, args, args.metadata) + debug(`Node ${valueId.nodeId}: metadata updated: ${getValueID(args)}`) this.emit( 'event', eventEmitter.node, @@ -711,33 +711,41 @@ function addValue (zwaveNode, zwaveValue) { } else { const zwaveValueMeta = zwaveNode.getValueMetadata(zwaveValue) - const valueId = updateValueMetadata.call( - this, - zwaveNode, - zwaveValue, - zwaveValueMeta - ) - valueId.value = zwaveNode.getValue(zwaveValue) - - if (isCurrentValue(valueId)) { - valueId.isCurrentValue = true - const targetValue = findTargetValue( - valueId, - zwaveNode.getDefinedValueIDs() - ) - if (targetValue) { - valueId.targetValue = getValueID(targetValue, false) - } - } + const valueId = initValue.call(this, zwaveNode, zwaveValue, zwaveValueMeta) debug(`Node ${zwaveNode.id}: value added ${valueId.id} => ${valueId.value}`) - node.values[getValueID(valueId)] = valueId - this.emit('valueChanged', valueId, node) } } +function initValue (zwaveNode, zwaveValue, zwaveValueMeta) { + const node = this.nodes[zwaveNode.id] + + const valueId = updateValueMetadata.call( + this, + zwaveNode, + zwaveValue, + zwaveValueMeta + ) + valueId.value = zwaveNode.getValue(zwaveValue) + + if (isCurrentValue(valueId)) { + valueId.isCurrentValue = true + const targetValue = findTargetValue( + valueId, + zwaveNode.getDefinedValueIDs() + ) + if (targetValue) { + valueId.targetValue = getValueID(targetValue, false) + } + } + + node.values[getValueID(valueId)] = valueId + + return valueId +} + // Triggered when a node is ready and a value changes function updateValue (zwaveNode, args) { const node = this.nodes[zwaveNode.id] From 854baee980366c00873055bf4f0a472b1906d6fe Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 09:07:31 +0100 Subject: [PATCH 04/17] fix: lint issues --- lib/ZwaveClient.js | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index d3c8ea2c48..31cd72d27e 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -732,10 +732,7 @@ function initValue (zwaveNode, zwaveValue, zwaveValueMeta) { if (isCurrentValue(valueId)) { valueId.isCurrentValue = true - const targetValue = findTargetValue( - valueId, - zwaveNode.getDefinedValueIDs() - ) + const targetValue = findTargetValue(valueId, zwaveNode.getDefinedValueIDs()) if (targetValue) { valueId.targetValue = getValueID(targetValue, false) } @@ -1907,7 +1904,11 @@ ZwaveClient.prototype.writeValue = async function (valueId, value) { if (this.driverReady) { let result = false - if (valueId.type === 'number' && typeof value === 'string' && !isNaN(value)) { + if ( + valueId.type === 'number' && + typeof value === 'string' && + !isNaN(value) + ) { value = Number(value) } @@ -1915,7 +1916,10 @@ ZwaveClient.prototype.writeValue = async function (valueId, value) { const zwaveNode = await this.getNode(valueId.nodeId) // handle multilevel switch 'start' and 'stop' commands - if (valueId.commandClass === CommandClasses['Multilevel Switch'] && isNaN(value)) { + if ( + valueId.commandClass === CommandClasses['Multilevel Switch'] && + isNaN(value) + ) { if (/stop/i.test(value)) { await zwaveNode.commandClasses['Multilevel Switch'].stopLevelChange() } else if (/start/i.test(value)) { From f00a5b300ffa8cf2dadd9eae2951f20ad539bb09 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 11:23:25 +0100 Subject: [PATCH 05/17] fix: discover values after node ready --- lib/Gateway.js | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/lib/Gateway.js b/lib/Gateway.js index 5a1b66198b..5d4789946d 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -230,6 +230,13 @@ function onValueChanged (valueId, node, changed) { this.zwave.sendToSocket(this.zwave.socketEvents.valueUpdated, valueId) } + const isDiscovered = this.discovered[valueId.id] + + // check if this value isn't discovered yet (values added after node is ready) + if (this.config.hassDiscovery && !isDiscovered) { + this.discoverValue(node, valueId.id.replace(valueId.nodeId + '-', '')) + } + const result = this.valueTopic(node, valueId, true) if (!result) return From 25d025974a2497dd824ed8412909e1279fafdba2 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 13:42:13 +0100 Subject: [PATCH 06/17] feat: auto discover climates devices --- hass/configurations.js | 9 ++--- hass/devices.js | 22 ++++++------ lib/Gateway.js | 80 ++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 96 insertions(+), 15 deletions(-) diff --git a/hass/configurations.js b/hass/configurations.js index 580fde8e6f..283db71d65 100755 --- a/hass/configurations.js +++ b/hass/configurations.js @@ -286,11 +286,12 @@ module.exports = { type: 'climate', object_id: 'climate', discovery_payload: { - min_temp: 7, - max_temp: 30, - modes: ['off', 'auto', 'heat'], + min_temp: 5, + max_temp: 40, + temp_step: 0.5, + modes: [], mode_state_topic: true, - mode_state_template: '{{ value_json.mode }}', + mode_state_template: '{{ value_json.value }}', mode_command_topic: true, current_temperature_topic: true, current_temperature_template: '{{ value_json.value }}', diff --git a/hass/devices.js b/hass/devices.js index 9c57564e89..cfa7701af0 100644 --- a/hass/devices.js +++ b/hass/devices.js @@ -44,8 +44,8 @@ const THERMOSTAT_2GIG = { on: 1 }, setpoint_topic: { - Heat: '67-1-setpoint-1', - Cool: '67-1-setpoint-2' + 1: '67-1-setpoint-1', + 2: '67-1-setpoint-2' }, default_setpoint: '67-1-setpoint-1', discovery_payload: { @@ -79,8 +79,8 @@ const STELLA_ZWAVE = { ], mode_map: { off: 0, heat: 1, cool: 11 }, setpoint_topic: { - Comfort: '67-0-setpoint-1', - 'Energy Saving': '67-0-setpoint-11' + 1: '67-0-setpoint-1', + 11: '67-0-setpoint-11' }, default_setpoint: '67-0-setpoint-1', discovery_payload: { @@ -109,8 +109,8 @@ const SPIRIT_ZWAVE_PLUS = { ], mode_map: { off: 0, heat: 1, cool: 11 }, setpoint_topic: { - Heat: '67-0-setpoint-1', - 'Heat Eco': '67-0-setpoint-11' + 1: '67-0-setpoint-1', + 11: '67-0-setpoint-11' }, default_setpoint: '67-0-setpoint-1', discovery_payload: { @@ -131,7 +131,7 @@ const DANFOSS_TRV_ZWAVE = { type: 'climate', object_id: 'thermostat', values: ['49-0-Air temperature', '67-0-setpoint-1'], - setpoint_topic: { Heat: '67-0-setpoint-1' }, + setpoint_topic: { 1: '67-0-setpoint-1' }, default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 4, @@ -171,7 +171,7 @@ module.exports = { off: 0, heat: 1 }, - setpoint_topic: { Heat: '67-0-setpoint-1' }, + setpoint_topic: { 1: '67-0-setpoint-1' }, default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 5, @@ -197,8 +197,8 @@ module.exports = { ], mode_map: { off: 0, heat: 1, cool: 2 }, setpoint_topic: { - 'Heat (Default)': '67-0-setpoint-1', - Cool: '67-0-setpoint-2' + 1: '67-0-setpoint-1', + 2: '67-0-setpoint-2' }, default_setpoint: '67-0-setpoint-1', discovery_payload: { @@ -226,7 +226,7 @@ module.exports = { '67-0-setpoint-2' ], mode_map: { off: 0, heat: 1, cool: 2 }, - setpoint_topic: { Heat: '67-0-setpoint-1', Cool: '67-0-setpoint-2' }, + setpoint_topic: { 1: '67-0-setpoint-1', 2: '67-0-setpoint-2' }, default_setpoint: '67-0-setpoint-1', discovery_payload: { min_temp: 15, diff --git a/lib/Gateway.js b/lib/Gateway.js index 5d4789946d..bb3708ead7 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -350,6 +350,9 @@ function onNodeStatus (node) { } } + // check if there are climates to discover + this.discoverClimates(node) + const nodeDevices = allDevices[node.deviceId] || [] nodeDevices.forEach(device => this.discoverDevice(node, device)) @@ -1145,6 +1148,83 @@ Gateway.prototype.discoverDevice = function (node, hassDevice) { } } +/** + * Discover climate devices + * + * @param {NodeObj} node Internal node object + */ +Gateway.prototype.discoverClimates = function (node) { + // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json#L177 + if (!node.deviceClass.generic === 0x08) { // it's a thermostat + return + } + + const nodeDevices = allDevices[node.deviceId] || [] + + // skip if there is already a climate device + if (nodeDevices.length > 0 && nodeDevices.find(d => d.type === 'climate')) { + return + } + + const setpoints = [] + const temperatures = [] + const modes = [] + + for (const vId in node.values) { + const v = node.values[vId] + if (v.commandClass === CommandClasses['Thermostat Setpoint'] && v.property === 'setpoint') { + setpoints.push(v) + } else if (v.commandClass === CommandClasses['Multilevel Sensor'] && v.property === 'Air temperature') { + temperatures.push(v) + } else if (v.commandClass === CommandClasses['Thermostat Mode'] && v.property === 'mode') { + modes.push(v) + } + } + + const mode = modes[0] + const temperature = temperatures[0] + + // [0, 1, 2] ---> ['off', 'heat', 'cold'] + const availableModes = mode.states.map(s => s.value) + + const hassModes = [ // [“auto”, “off”, “cool”, “heat”, “dry”, “fan_only”] + 'off', + 'heat', + 'cool' + ] + + const config = copy(hassCfg.thermostat) + + config.mode_map = {} + config.setpoint_topic = {} + config.values = [mode.id, temperature.id] + + config.discovery_payload.mode_state_topic = mode.id + config.discovery_payload.current_temperature_topic = temperature.id + + for (const m of availableModes) { + config.mode_map[hassModes[m]] = availableModes[m] + config.discovery_payload.modes.push(hassModes[m]) + if (m > 0) { // find the mode setpoint, ignore off + const setpoint = setpoints.find(v => v.propertyKey === m) + if (setpoint) { + if (setpoint.propertyKey === 1) { + config.default_setpoint = setpoint.id + } + + config.values.push(setpoint.id) + config.setpoint_topic[m] = setpoint.id + } + } + } + + nodeDevices.push(config) + + debug('New climate device discovered: ' + JSON.stringify(config)) + + allDevices[node.deviceId] = nodeDevices +} + /** * Try to guess the best way to discover this valueId in Hass * From 843bcd649d135248f7d54448cb1f7f7587892c9d Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 14:09:57 +0100 Subject: [PATCH 07/17] fix: add more hass modes maps --- lib/Gateway.js | 43 ++++++++++++++++++++++++++++++++++++------- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/lib/Gateway.js b/lib/Gateway.js index bb3708ead7..e06b73183b 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -1155,7 +1155,8 @@ Gateway.prototype.discoverDevice = function (node, hassDevice) { */ Gateway.prototype.discoverClimates = function (node) { // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json#L177 - if (!node.deviceClass.generic === 0x08) { // it's a thermostat + if (!node.deviceClass.generic === 0x08) { + // it's a thermostat return } @@ -1172,11 +1173,20 @@ Gateway.prototype.discoverClimates = function (node) { for (const vId in node.values) { const v = node.values[vId] - if (v.commandClass === CommandClasses['Thermostat Setpoint'] && v.property === 'setpoint') { + if ( + v.commandClass === CommandClasses['Thermostat Setpoint'] && + v.property === 'setpoint' + ) { setpoints.push(v) - } else if (v.commandClass === CommandClasses['Multilevel Sensor'] && v.property === 'Air temperature') { + } else if ( + v.commandClass === CommandClasses['Multilevel Sensor'] && + v.property === 'Air temperature' + ) { temperatures.push(v) - } else if (v.commandClass === CommandClasses['Thermostat Mode'] && v.property === 'mode') { + } else if ( + v.commandClass === CommandClasses['Thermostat Mode'] && + v.property === 'mode' + ) { modes.push(v) } } @@ -1187,10 +1197,25 @@ Gateway.prototype.discoverClimates = function (node) { // [0, 1, 2] ---> ['off', 'heat', 'cold'] const availableModes = mode.states.map(s => s.value) - const hassModes = [ // [“auto”, “off”, “cool”, “heat”, “dry”, “fan_only”] + // zwave modes: https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts#L54 + const hassModes = [ + // Available hass modes: [“auto”, “off”, “cool”, “heat”, “dry”, “fan_only”] 'off', 'heat', - 'cool' + 'cool', + 'auto', + undefined, // auxiliary + undefined, // resume + 'fan_only', + undefined, // furnace + 'dry', + undefined, // moist + 'auto', // auto changeover + 'heat', // energy heat + 'cool', // energy cool + 'off', // away + 'heat', // full power + undefined // manufacturer specific ] const config = copy(hassCfg.thermostat) @@ -1202,10 +1227,12 @@ Gateway.prototype.discoverClimates = function (node) { config.discovery_payload.mode_state_topic = mode.id config.discovery_payload.current_temperature_topic = temperature.id + // for all available modes update the modes map and setpoint topics for (const m of availableModes) { config.mode_map[hassModes[m]] = availableModes[m] config.discovery_payload.modes.push(hassModes[m]) - if (m > 0) { // find the mode setpoint, ignore off + if (m > 0) { + // find the mode setpoint, ignore off const setpoint = setpoints.find(v => v.propertyKey === m) if (setpoint) { if (setpoint.propertyKey === 1) { @@ -1218,6 +1245,8 @@ Gateway.prototype.discoverClimates = function (node) { } } + // add the new climate config to the nodeDevices so it will be + // discovered later when we call `discoverDevice` nodeDevices.push(config) debug('New climate device discovered: ' + JSON.stringify(config)) From c5aec29633c2cb27088439f2126fc4c1de2d2043 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 14:34:27 +0100 Subject: [PATCH 08/17] fix: duplicate nodestatus call in initNode --- lib/Gateway.js | 2 +- lib/ZwaveClient.js | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/lib/Gateway.js b/lib/Gateway.js index e06b73183b..924f9f306c 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -1155,7 +1155,7 @@ Gateway.prototype.discoverDevice = function (node, hassDevice) { */ Gateway.prototype.discoverClimates = function (node) { // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json#L177 - if (!node.deviceClass.generic === 0x08) { + if (!node.deviceClass || node.deviceClass.generic !== 0x08) { // it's a thermostat return } diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index 31cd72d27e..05a839b9be 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -276,8 +276,6 @@ function onNodeReady (zwaveNode) { if (!node || node.ready) return node.ready = true - onNodeStatus.call(this, zwaveNode) - initNode.call(this, zwaveNode) const values = zwaveNode.getDefinedValueIDs() @@ -288,7 +286,7 @@ function onNodeReady (zwaveNode) { node.lastActive = Date.now() - this.emit('nodeStatus', node) + onNodeStatus.call(this, zwaveNode) this.emit('event', eventEmitter.node, 'node ready', this.nodes[zwaveNode.id]) From 1b96d81236857ee7aae504bd3d8b0e7f6f4e106b Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 14:39:51 +0100 Subject: [PATCH 09/17] fix: better handle node ready event --- lib/ZwaveClient.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index 05a839b9be..2cf05c951e 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -273,15 +273,21 @@ function onNodeReady (zwaveNode) { const node = this.nodes[zwaveNode.id] // node ready event has been already tiggered by this node - if (!node || node.ready) return + if (!node) return - node.ready = true - initNode.call(this, zwaveNode) + node.lastActive = Date.now() + + // ignore the init when node is readt + if (!node.ready) { + initNode.call(this, zwaveNode) - const values = zwaveNode.getDefinedValueIDs() + node.ready = true - for (const zwaveValue of values) { - addValue.call(this, zwaveNode, zwaveValue) + const values = zwaveNode.getDefinedValueIDs() + + for (const zwaveValue of values) { + addValue.call(this, zwaveNode, zwaveValue) + } } node.lastActive = Date.now() From 6c6e229d5bace5f11eeea4fdaa69c62c649ed1cd Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 15:01:18 +0100 Subject: [PATCH 10/17] fix: ensure temperature and mode are defined --- lib/Gateway.js | 173 ++++++++++++++++++++++++++----------------------- 1 file changed, 92 insertions(+), 81 deletions(-) diff --git a/lib/Gateway.js b/lib/Gateway.js index 924f9f306c..ccc98de7ff 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -1160,98 +1160,109 @@ Gateway.prototype.discoverClimates = function (node) { return } - const nodeDevices = allDevices[node.deviceId] || [] + try { + const nodeDevices = allDevices[node.deviceId] || [] - // skip if there is already a climate device - if (nodeDevices.length > 0 && nodeDevices.find(d => d.type === 'climate')) { - return - } + // skip if there is already a climate device + if (nodeDevices.length > 0 && nodeDevices.find(d => d.type === 'climate')) { + return + } - const setpoints = [] - const temperatures = [] - const modes = [] + const setpoints = [] + const temperatures = [] + const modes = [] - for (const vId in node.values) { - const v = node.values[vId] - if ( - v.commandClass === CommandClasses['Thermostat Setpoint'] && - v.property === 'setpoint' - ) { - setpoints.push(v) - } else if ( - v.commandClass === CommandClasses['Multilevel Sensor'] && - v.property === 'Air temperature' - ) { - temperatures.push(v) - } else if ( - v.commandClass === CommandClasses['Thermostat Mode'] && - v.property === 'mode' - ) { - modes.push(v) + for (const vId in node.values) { + const v = node.values[vId] + if ( + v.commandClass === CommandClasses['Thermostat Setpoint'] && + v.property === 'setpoint' + ) { + setpoints.push(v) + } else if ( + v.commandClass === CommandClasses['Multilevel Sensor'] && + v.property === 'Air temperature' + ) { + temperatures.push(v) + } else if ( + v.commandClass === CommandClasses['Thermostat Mode'] && + v.property === 'mode' + ) { + modes.push(v) + } } - } - const mode = modes[0] - const temperature = temperatures[0] - - // [0, 1, 2] ---> ['off', 'heat', 'cold'] - const availableModes = mode.states.map(s => s.value) - - // zwave modes: https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts#L54 - const hassModes = [ - // Available hass modes: [“auto”, “off”, “cool”, “heat”, “dry”, “fan_only”] - 'off', - 'heat', - 'cool', - 'auto', - undefined, // auxiliary - undefined, // resume - 'fan_only', - undefined, // furnace - 'dry', - undefined, // moist - 'auto', // auto changeover - 'heat', // energy heat - 'cool', // energy cool - 'off', // away - 'heat', // full power - undefined // manufacturer specific - ] - - const config = copy(hassCfg.thermostat) - - config.mode_map = {} - config.setpoint_topic = {} - config.values = [mode.id, temperature.id] - - config.discovery_payload.mode_state_topic = mode.id - config.discovery_payload.current_temperature_topic = temperature.id - - // for all available modes update the modes map and setpoint topics - for (const m of availableModes) { - config.mode_map[hassModes[m]] = availableModes[m] - config.discovery_payload.modes.push(hassModes[m]) - if (m > 0) { - // find the mode setpoint, ignore off - const setpoint = setpoints.find(v => v.propertyKey === m) - if (setpoint) { - if (setpoint.propertyKey === 1) { - config.default_setpoint = setpoint.id - } + const mode = modes[0] + const temperature = temperatures[0] + + if (!temperature || !mode) { + debug( + 'Unable to discover climate device, there is no valid temperature/thermostat mode valueIds' + ) + return + } + + // [0, 1, 2] ---> ['off', 'heat', 'cold'] + const availableModes = mode.states.map(s => s.value) + + // zwave modes: https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts#L54 + const hassModes = [ + // Available hass modes: [“auto”, “off”, “cool”, “heat”, “dry”, “fan_only”] + 'off', + 'heat', + 'cool', + 'auto', + undefined, // auxiliary + undefined, // resume + 'fan_only', + undefined, // furnace + 'dry', + undefined, // moist + 'auto', // auto changeover + 'heat', // energy heat + 'cool', // energy cool + 'off', // away + 'heat', // full power + undefined // manufacturer specific + ] + + const config = copy(hassCfg.thermostat) + + config.mode_map = {} + config.setpoint_topic = {} + config.values = [mode.id, temperature.id] + + config.discovery_payload.mode_state_topic = mode.id + config.discovery_payload.current_temperature_topic = temperature.id + + // for all available modes update the modes map and setpoint topics + for (const m of availableModes) { + config.mode_map[hassModes[m]] = availableModes[m] + config.discovery_payload.modes.push(hassModes[m]) + if (m > 0) { + // find the mode setpoint, ignore off + const setpoint = setpoints.find(v => v.propertyKey === m) + if (setpoint) { + if (setpoint.propertyKey === 1) { + config.default_setpoint = setpoint.id + } - config.values.push(setpoint.id) - config.setpoint_topic[m] = setpoint.id + config.values.push(setpoint.id) + config.setpoint_topic[m] = setpoint.id + } } } - } - // add the new climate config to the nodeDevices so it will be - // discovered later when we call `discoverDevice` - nodeDevices.push(config) + // add the new climate config to the nodeDevices so it will be + // discovered later when we call `discoverDevice` + nodeDevices.push(config) - debug('New climate device discovered: ' + JSON.stringify(config)) + debug('New climate device discovered: ' + JSON.stringify(config)) - allDevices[node.deviceId] = nodeDevices + allDevices[node.deviceId] = nodeDevices + } catch (error) { + debug('Unable to discover climate device: ' + error.message) + } } /** From b82a1861f167f58f7e5214a65608f9ab770fc8ef Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 15:04:30 +0100 Subject: [PATCH 11/17] fix: ignoreUpdates on getGroups when initing node --- lib/ZwaveClient.js | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index 2cf05c951e..c4e7b23790 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -635,7 +635,7 @@ function initNode (zwaveNode) { node.productId )}-${utils.num2hex(node.productType)}` - this.getGroups(zwaveNode.id) + this.getGroups(zwaveNode.id, true) } function updateValueMetadata (zwaveNode, zwaveValue, zwaveValueMeta) { @@ -1094,7 +1094,10 @@ ZwaveClient.prototype.addEmptyNodes = function () { * * @param {Integer} nodeId Zwave node id */ -ZwaveClient.prototype.getGroups = async function (nodeId) { +ZwaveClient.prototype.getGroups = async function ( + nodeId, + ignoreUpdate = false +) { const zwaveNode = this.getNode(nodeId) if (zwaveNode) { let groups = [] @@ -1116,7 +1119,9 @@ ZwaveClient.prototype.getGroups = async function (nodeId) { } } - onNodeStatus.call(this, zwaveNode) + if (!ignoreUpdate) { + onNodeStatus.call(this, zwaveNode) + } } /** From 94da2272835f8764e6053fcb6829a6749b42febe Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 15:14:48 +0100 Subject: [PATCH 12/17] fix: manually send MultilevelSwitchCCStopLevelChange cc --- lib/ZwaveClient.js | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index c4e7b23790..c9c8b4b9b7 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -10,6 +10,7 @@ const { extractFirmware } = require('zwave-js') const { applicationCCs, CommandClasses } = require('@zwave-js/core') +const { MultilevelSwitchCCStopLevelChange } = require('zwave-js/CommandClass') const utils = reqlib('/lib/utils.js') const EventEmitter = require('events') const jsonStore = reqlib('/lib/jsonStore.js') @@ -1930,7 +1931,9 @@ ZwaveClient.prototype.writeValue = async function (valueId, value) { isNaN(value) ) { if (/stop/i.test(value)) { - await zwaveNode.commandClasses['Multilevel Switch'].stopLevelChange() + const cc = new MultilevelSwitchCCStopLevelChange(this.driver, valueId) + await this.driver.sendCommand(cc) + // await zwaveNode.commandClasses['Multilevel Switch'].stopLevelChange() } else if (/start/i.test(value)) { await zwaveNode.commandClasses['Multilevel Switch'].startLevelChange() } else { From 969e483c216f790670080505bf8538e4ccd48edd Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 15:43:49 +0100 Subject: [PATCH 13/17] fix: use ids without node prefix --- lib/Gateway.js | 31 +++++++++++++++++-------------- 1 file changed, 17 insertions(+), 14 deletions(-) diff --git a/lib/Gateway.js b/lib/Gateway.js index ccc98de7ff..9f982b3101 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -1178,30 +1178,32 @@ Gateway.prototype.discoverClimates = function (node) { v.commandClass === CommandClasses['Thermostat Setpoint'] && v.property === 'setpoint' ) { - setpoints.push(v) + setpoints.push(vId) } else if ( v.commandClass === CommandClasses['Multilevel Sensor'] && v.property === 'Air temperature' ) { - temperatures.push(v) + temperatures.push(vId) } else if ( v.commandClass === CommandClasses['Thermostat Mode'] && v.property === 'mode' ) { - modes.push(v) + modes.push(vId) } } - const mode = modes[0] - const temperature = temperatures[0] + const modeId = modes[0] + const temperatureId = temperatures[0] - if (!temperature || !mode) { + if (!temperatureId || !modeId) { debug( 'Unable to discover climate device, there is no valid temperature/thermostat mode valueIds' ) return } + const mode = node.values[modeId] + // [0, 1, 2] ---> ['off', 'heat', 'cold'] const availableModes = mode.states.map(s => s.value) @@ -1230,10 +1232,10 @@ Gateway.prototype.discoverClimates = function (node) { config.mode_map = {} config.setpoint_topic = {} - config.values = [mode.id, temperature.id] + config.values = [modeId, temperatureId] - config.discovery_payload.mode_state_topic = mode.id - config.discovery_payload.current_temperature_topic = temperature.id + config.discovery_payload.mode_state_topic = modeId + config.discovery_payload.current_temperature_topic = temperatureId // for all available modes update the modes map and setpoint topics for (const m of availableModes) { @@ -1241,14 +1243,15 @@ Gateway.prototype.discoverClimates = function (node) { config.discovery_payload.modes.push(hassModes[m]) if (m > 0) { // find the mode setpoint, ignore off - const setpoint = setpoints.find(v => v.propertyKey === m) - if (setpoint) { + const setId = setpoints.find(v => v.endsWith('-' + m)) + if (setId) { + const setpoint = node.values[setId] if (setpoint.propertyKey === 1) { - config.default_setpoint = setpoint.id + config.default_setpoint = setId } - config.values.push(setpoint.id) - config.setpoint_topic[m] = setpoint.id + config.values.push(setId) + config.setpoint_topic[m] = setId } } } From 2f8ba7b2615f44eb2ad8ca5a755a004fb54e95e2 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 15:51:07 +0100 Subject: [PATCH 14/17] fix: some nits --- lib/Gateway.js | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/lib/Gateway.js b/lib/Gateway.js index 9f982b3101..a959bef773 100755 --- a/lib/Gateway.js +++ b/lib/Gateway.js @@ -1155,8 +1155,8 @@ Gateway.prototype.discoverDevice = function (node, hassDevice) { */ Gateway.prototype.discoverClimates = function (node) { // https://github.com/zwave-js/node-zwave-js/blob/master/packages/config/config/deviceClasses.json#L177 + // check if device it's a thermostat if (!node.deviceClass || node.deviceClass.generic !== 0x08) { - // it's a thermostat return } @@ -1168,6 +1168,7 @@ Gateway.prototype.discoverClimates = function (node) { return } + // arrays of strings valueIds (without the node prefix) const setpoints = [] const temperatures = [] const modes = [] @@ -1192,7 +1193,10 @@ Gateway.prototype.discoverClimates = function (node) { } } + // take the first as valid (there shouldn't be more then one) const modeId = modes[0] + + // TODO: if the device supports multiple endpoints how could we identify the correct one to use? const temperatureId = temperatures[0] if (!temperatureId || !modeId) { @@ -1204,12 +1208,12 @@ Gateway.prototype.discoverClimates = function (node) { const mode = node.values[modeId] - // [0, 1, 2] ---> ['off', 'heat', 'cold'] + // [0, 1, 2 ... ] (['off', 'heat', 'cold', ...]) const availableModes = mode.states.map(s => s.value) - // zwave modes: https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts#L54 + // Zwave modes: https://github.com/zwave-js/node-zwave-js/blob/master/packages/zwave-js/src/lib/commandclass/ThermostatModeCC.ts#L54 + // Available hass modes: [“auto”, “off”, “cool”, “heat”, “dry”, “fan_only”] const hassModes = [ - // Available hass modes: [“auto”, “off”, “cool”, “heat”, “dry”, “fan_only”] 'off', 'heat', 'cool', @@ -1228,6 +1232,7 @@ Gateway.prototype.discoverClimates = function (node) { undefined // manufacturer specific ] + // generic configuration const config = copy(hassCfg.thermostat) config.mode_map = {} @@ -1239,13 +1244,15 @@ Gateway.prototype.discoverClimates = function (node) { // for all available modes update the modes map and setpoint topics for (const m of availableModes) { + if (hassModes[m] === undefined) continue + config.mode_map[hassModes[m]] = availableModes[m] config.discovery_payload.modes.push(hassModes[m]) if (m > 0) { // find the mode setpoint, ignore off const setId = setpoints.find(v => v.endsWith('-' + m)) - if (setId) { - const setpoint = node.values[setId] + const setpoint = node.values[setId] + if (setpoint) { if (setpoint.propertyKey === 1) { config.default_setpoint = setId } From f6000cb1a732a6b399986a5cdb7845db709e9365 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 16:36:36 +0100 Subject: [PATCH 15/17] fix: broken page refresh 404 #42 --- app.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index f4fca76cb0..2cf87651c1 100644 --- a/app.js +++ b/app.js @@ -39,14 +39,16 @@ app.use( ) app.use(cookieParser()) +app.use(history({ + index: '/' +})) + app.get('/', renderIndex) app.use('/', express.static(utils.joinPath(false, 'dist'))) app.use(cors()) -app.use(history()) - function hasProperty (obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop) } From b2e6d7fa4e989ba109a5b89173df83dc49b0c2fa Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 16:39:04 +0100 Subject: [PATCH 16/17] fix: MultilevelSwitchCCStopLevelChange import --- lib/ZwaveClient.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index c9c8b4b9b7..cff68e92e9 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -10,7 +10,7 @@ const { extractFirmware } = require('zwave-js') const { applicationCCs, CommandClasses } = require('@zwave-js/core') -const { MultilevelSwitchCCStopLevelChange } = require('zwave-js/CommandClass') +const { MultilevelSwitchCCStopLevelChange } = require('zwave-js/build/lib/commandclass/MultilevelSwitchCC') const utils = reqlib('/lib/utils.js') const EventEmitter = require('events') const jsonStore = reqlib('/lib/jsonStore.js') From 934eb92b0891e3060e5420c8d2b0dabda196f869 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 1 Dec 2020 17:05:42 +0100 Subject: [PATCH 17/17] fix: revert to use start/stopLevelChange --- app.js | 8 +++++--- lib/ZwaveClient.js | 5 +---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/app.js b/app.js index 2cf87651c1..f759c75005 100644 --- a/app.js +++ b/app.js @@ -39,9 +39,11 @@ app.use( ) app.use(cookieParser()) -app.use(history({ - index: '/' -})) +app.use( + history({ + index: '/' + }) +) app.get('/', renderIndex) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index cff68e92e9..c4e7b23790 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -10,7 +10,6 @@ const { extractFirmware } = require('zwave-js') const { applicationCCs, CommandClasses } = require('@zwave-js/core') -const { MultilevelSwitchCCStopLevelChange } = require('zwave-js/build/lib/commandclass/MultilevelSwitchCC') const utils = reqlib('/lib/utils.js') const EventEmitter = require('events') const jsonStore = reqlib('/lib/jsonStore.js') @@ -1931,9 +1930,7 @@ ZwaveClient.prototype.writeValue = async function (valueId, value) { isNaN(value) ) { if (/stop/i.test(value)) { - const cc = new MultilevelSwitchCCStopLevelChange(this.driver, valueId) - await this.driver.sendCommand(cc) - // await zwaveNode.commandClasses['Multilevel Switch'].stopLevelChange() + await zwaveNode.commandClasses['Multilevel Switch'].stopLevelChange() } else if (/start/i.test(value)) { await zwaveNode.commandClasses['Multilevel Switch'].startLevelChange() } else {