From 46bee76b89c23de134fb04073a534e51ee10a355 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Tue, 26 Jan 2021 18:11:08 +0100 Subject: [PATCH 1/5] feat: send custom command api Fixes #336 --- lib/ZwaveClient.js | 53 +++++++++++++++++++++++++++++++++++++++++++++- lib/utils.js | 30 ++++++++++++++++++++++++++ 2 files changed, 82 insertions(+), 1 deletion(-) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index 135f588835..ec45d9a41e 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -2103,6 +2103,56 @@ ZwaveClient.prototype.hardReset = async function () { throw Error('Driver is closed') } + +ZwaveClient.prototype.sendCommand = async function (valueId, command, args) { + if (this.driver && !this.closed) { + if (typeof valueId.nodeId !== 'number') { + throw Error('nodeId must be a number') + } + + const error = utils.isValueId(valueId) + + if (typeof error === 'string') { + throw Error(error) + } + + if (args !== undefined && !Array.isArray(args)) { + throw Error( + 'if args is given, it must be an array' + ) + } + + const node = this.getNode(valueId.nodeId) + if (!node) { + throw Error(`Node ${valueId.nodeId} was not found!`) + } + const endpoint = node.getEndpoint(valueId.endpoint ?? 0) + if (!endpoint) { + throw Error( + `Endpoint ${valueId.endpoint} does not exist on Node ${valueId.nodeId}!` + ) + } + const api = endpoint.commandClasses[valueId.commandClass] + + if (!api || !api.isSupported()) { + throw Error( + `Node ${valueId.nodeId} (Endpoint ${valueId.endpoint}) does not support CC ${valueId.commandClass}` + ) + } else if (!(command in api)) { + throw Error( + `The command ${command} does not exist for CC ${valueId.commandClass}` + ) + } + + const method = api[command].bind(api) + const result = args + ? await method(...args) + : await method() + return result + } + + throw Error('Driver is closed') +} /** * Calls a specific `client` or `ZwaveClient` method based on `apiName` * ZwaveClients methods used are the ones that overrides default Zwave methods @@ -2152,7 +2202,8 @@ ZwaveClient.prototype.callApi = async function (apiName, ...args) { 'refreshInfo', 'beginFirmwareUpdate', 'abortFirmwareUpdate', - 'writeValue' + 'writeValue', + 'sendCommand' ] const allowed = diff --git a/lib/utils.js b/lib/utils.js index ec4dd3bd06..a40dd5360b 100644 --- a/lib/utils.js +++ b/lib/utils.js @@ -50,6 +50,35 @@ function joinProps (...props) { return ret } +/** + * Checks if an object is a valueId, returns error otherwise + * + * @param {import('zwave-js').ValueID} v the object + * @returns {boolean|string} Returns true if it's a valid valueId, an error string otherwise + */ +function isValueId (v) { + if (typeof v.commandClass !== 'number' || v.commandClass < 0) { + return 'invalid `commandClass`' + } + if (v.endpoint !== undefined && v.endpoint < 0) { + return 'invalid `endpoint`' + } + if ( + v.property === undefined || + (typeof v.property !== 'string' && typeof v.property !== 'number') + ) { + return 'invalid `property`' + } + if ( + v.propertyKey !== undefined && + typeof v.propertyKey !== 'string' && + typeof v.propertyKey !== 'number' + ) { + return 'invalid `propertyKey`' + } + return true +} + /** * Converts a decimal to an hex number of 4 digits and `0x` as prefix * @@ -150,6 +179,7 @@ module.exports = { getPath, joinPath, joinProps, + isValueId, num2hex, getVersion, sanitizeTopic, From 021d3ac60e74b8cb97ef7d1c43bcd9396c8ff465 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Wed, 27 Jan 2021 17:51:39 +0100 Subject: [PATCH 2/5] fix: lint issues --- lib/ZwaveClient.js | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/lib/ZwaveClient.js b/lib/ZwaveClient.js index ec45d9a41e..f2c6dd8fcb 100644 --- a/lib/ZwaveClient.js +++ b/lib/ZwaveClient.js @@ -2117,37 +2117,33 @@ ZwaveClient.prototype.sendCommand = async function (valueId, command, args) { } if (args !== undefined && !Array.isArray(args)) { - throw Error( - 'if args is given, it must be an array' - ) + throw Error('if args is given, it must be an array') } const node = this.getNode(valueId.nodeId) if (!node) { throw Error(`Node ${valueId.nodeId} was not found!`) } - const endpoint = node.getEndpoint(valueId.endpoint ?? 0) + const endpoint = node.getEndpoint(valueId.endpoint || 0) if (!endpoint) { throw Error( - `Endpoint ${valueId.endpoint} does not exist on Node ${valueId.nodeId}!` + `Endpoint ${valueId.endpoint} does not exist on Node ${valueId.nodeId}!` ) } const api = endpoint.commandClasses[valueId.commandClass] if (!api || !api.isSupported()) { throw Error( - `Node ${valueId.nodeId} (Endpoint ${valueId.endpoint}) does not support CC ${valueId.commandClass}` + `Node ${valueId.nodeId} (Endpoint ${valueId.endpoint}) does not support CC ${valueId.commandClass}` ) } else if (!(command in api)) { throw Error( - `The command ${command} does not exist for CC ${valueId.commandClass}` + `The command ${command} does not exist for CC ${valueId.commandClass}` ) } const method = api[command].bind(api) - const result = args - ? await method(...args) - : await method() + const result = args ? await method(...args) : await method() return result } From e1f6125ceec9212bbd7dd99821ea804fc217e825 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 28 Jan 2021 11:21:11 +0100 Subject: [PATCH 3/5] dos: sendCommand --- docs/guide/mqtt.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/guide/mqtt.md b/docs/guide/mqtt.md index 83deadda35..efec91de76 100644 --- a/docs/guide/mqtt.md +++ b/docs/guide/mqtt.md @@ -96,6 +96,7 @@ This are the available apis: - `beginFirmwareUpdate(nodeId, fileName, data)`: Starts a firmware update of a node. The `fileName` is used to check the extension (used to detect the firmware file type) and data is a `Buffer` - `abortFirmwareUpdate(nodeId)`: Aborts a firmware update - `writeValue(valueId, value)`: Write a specific value to a [valueId](https://zwave-js.github.io/node-zwave-js/#/api/valueid?id=valueid) +- `sendCommand(valueId, command, args)`: Send a custom command ## Set values From 61be803fa7c50523e0d02cd2bac991029e0477bb Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 28 Jan 2021 11:23:37 +0100 Subject: [PATCH 4/5] refactor: add types --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index a50cdf845f..85d1b24065 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -382,6 +382,7 @@ export interface ZwaveClient extends EventEmitter { ...args: any ): Promise<{ success: boolean; message: string; result: any; args: any[] }> writeValue(valueId: Z2MValueId, value: number | string): Promise + sendCommand(valueId: Z2MValueId, command: string, args: any[]): Promise } export interface Z2MGateway { From f7578480d935c93c52408bec5cd0925d479f5068 Mon Sep 17 00:00:00 2001 From: Daniel Lando Date: Thu, 28 Jan 2021 17:46:12 +0100 Subject: [PATCH 5/5] docs: example api call --- docs/guide/mqtt.md | 73 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 56 insertions(+), 17 deletions(-) diff --git a/docs/guide/mqtt.md b/docs/guide/mqtt.md index efec91de76..d460c2b62a 100644 --- a/docs/guide/mqtt.md +++ b/docs/guide/mqtt.md @@ -1,6 +1,6 @@ # MQTT -You have full access to all [zwavejs APIs](https://zwave-js.github.io/node-zwave-js/#/README) (and more) by simply using MQTT. +You have access to almost all [zwavejs APIs](https://zwave-js.github.io/node-zwave-js/#/README) (and more) via MQTT. ## Zwave Events @@ -34,22 +34,6 @@ Where `args` is an array with the args used to call the api, the topic is: The result will be published on the same topic without `/set` -Example: If I publish the previous json object to the topic - -`zwave/_CLIENTS/ZWAVE_GATEWAY-office/api/getAssociations/set` - -I will get this response (in the same topic without the suffix `/set`): - -```json -{ - "success": true, - "message": "Success zwave api call", - "result": [1] -} -``` - -`result` will contain the value returned from the API. In this example I will get an array with all node IDs that are associated to the group 1 (lifeline) of node 2. - ### APIs This are the available apis: @@ -98,6 +82,61 @@ This are the available apis: - `writeValue(valueId, value)`: Write a specific value to a [valueId](https://zwave-js.github.io/node-zwave-js/#/api/valueid?id=valueid) - `sendCommand(valueId, command, args)`: Send a custom command +### Api call examples + +#### Get Associations + +Get all the associations of node `23` group `Lifeline` (groupId `1`) + +Topic: `zwave/_CLIENTS/ZWAVE_GATEWAY-office/api/getAssociations/set` + +Payload: + +```js +{ + "args": [ + 23, // nodeid + 1 // lifeline group id + ] +} + +``` + +I will get this response (in the same topic without the suffix `/set`): + +```js +{ + "success": true, + "message": "Success zwave api call", + "result": [1] // the controller id +} +``` + +`result` will contain the value returned from the API. In this example I will get an array with all node IDs that are associated to the group 1 (lifeline) of node 23. + +#### Send Command + +Example calling [startLevelChange](https://github.com/zwave-js/node-zwave-js/blob/c695ee81cb2b1d3cf15e3db1cc14b1e41a911cc0/packages/zwave-js/src/lib/commandclass/MultilevelSwitchCC.ts) command: + +Topic: `zwavejs/_CLIENTS/ZWAVE_GATEWAY-/api/sendCommand/set` + +Payload: + +```js +{ "args": [ + { + "nodeId": 23, + "commandClass": 38, + "commandClassName": "Multilevel Switch", + "endpoint": 0, + "property": "targetValue" + }, + "startLevelChange", + [{}] // this are the args, in this case it could be omitted + ] +} +``` + ## Set values To write a value using MQTT you just need to send the value to set in the same topic where the value updates are published by adding the suffix `/set` to the topic (**READONLY VALUES CANNOT BE WRITE**).