Skip to content

Commit

Permalink
feat: multicast/broadcast apis (#25)
Browse files Browse the repository at this point in the history
* feat: multicast api

* docs: fix lint

* fix: await writes to zwave broadcast and multicast requests

* fix: lint issues

* fix: use parsed payload in write value

* docs: json payload

* feat: use native multicast and broadcast from zwave-js

* docs: multicast and broadcast

* fix: replace debug with logger

* fix: replace debug with logger

* fix: replace debug with logger

* fix: replace debug with logger

* fix: replace debug with logger

* fix: replace debug with logger

* fix: replace debug with logger

* fix: types

* fix: types

* fix: lint issues

* fix: lint issues and missing fallbacks

* fix: broadcast/multicast docs

* fix: lint issues

* fix: docs nits

* docs: fix docs/guide/mqtt.md
  • Loading branch information
robertsLando authored Mar 15, 2021
1 parent cd5691a commit 98e145d
Show file tree
Hide file tree
Showing 5 changed files with 179 additions and 31 deletions.
61 changes: 50 additions & 11 deletions docs/guide/mqtt.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,8 @@ This are the available apis:
- `beginFirmwareUpdate(nodeId, fileName, data, target)`: 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)
- `writeBroadcast(valueId, value)`: Send a broadcast request to all nodes that support [valueId](https://zwave-js.github.io/node-zwave-js/#/api/valueid?id=valueid)
- `writeMulticast(nodes, valueId, value)`: Send a multicast request to all `nodes` provided that support [valueId](https://zwave-js.github.io/node-zwave-js/#/api/valueid?id=valueid)
- `sendCommand(ctx, command, args)`: Send a custom command.
- `ctx`:context to get the instance to send the command (`{ nodeId: number, endpoint: number, commandClass: number }`)
- `command`: the command name. Check available commands by selecting a CC [here](https://zwave-js.github.io/node-zwave-js/#/api/CCs/index)
Expand Down Expand Up @@ -169,27 +171,64 @@ I will set the Heating setpoint of the node with id `4` located in the `office`

## Broadcast

You can send broadcast values to _all values with a specific suffix_ in the network.
You can send two kind of broadcast requests:

Broadcast API is accessible from:
1. Send it to _all values with a specific suffix_ in the network.

`<mqtt_prefix>/_CLIENTS/ZWAVE_GATEWAY-<mqtt_name>/broadcast/<value_topic_suffix>/set`
> [!NOTE]
> This creates a LOT of traffic and can have a significant performance impact.
Topic: `<mqtt_prefix>/_CLIENTS/ZWAVE_GATEWAY-<mqtt_name>/broadcast/<value_topic_suffix>/set`

- `value_topic_suffix`: the suffix of the topic of the value I want to control using broadcast.

It works like the set value API without the node name and location properties.
If the API is correctly called the same payload of the request will be published
to the topic without `/set` suffix.
It works like the set value API without the node name and location properties.
If the API is correctly called the same payload of the request will be published
to the topic without `/set` suffix.

Example of broadcast command (gateway configured as `named topics`):

`zwave/_CLIENTS/ZWAVE_GATEWAY-test/broadcast/38/0/targetValue/set`

Payload: `25.5`

All nodes with a valueId **Command class** `38` (Multilevel Switch), **Endpoint** `0` will receive a write request of value `25.5` to **property** `targetValue` and will get the same value (as feedback) on the topic:

Example of broadcast command (gateway configured as `named topics`):
`zwave/_CLIENTS/ZWAVE_GATEWAY-test/broadcast/38/0/targetValue`

`zwave/_CLIENTS/ZWAVE_GATEWAY-test/broadcast/thermostat_setpoint/heating/set`
1. Send a real zwave [broadcast](https://zwave-js.github.io/node-zwave-js/#/api/controller?id=getbroadcastnode) request

Payload: `25.5`
Topic: `<mqtt_prefix>/_CLIENTS/ZWAVE_GATEWAY-<mqtt_name>/broadcast/set`
Payload:

All nodes with command class `thermostat_setpoint` and value `heating` will be set to `25.5` and I will get the same value on the topic:
```js
{
"commandClass": 38,
"endpoint": 0,
"property": "targetValue",
"value": 80
}
```

`zwave/_CLIENTS/ZWAVE_GATEWAY-test/broadcast/thermostat_setpoint/heating`
## Multicast

Send a [multicast](https://zwave-js.github.io/node-zwave-js/#/api/controller?id=getmulticastgroup) request to all nodes specified in the array in the payload. If this fails because it's not supported a fallback will try to send multiple single requests

> [!NOTE]
> Multicast requests have no delay between individual nodes reactions
Topic: `<mqtt_prefix>/_CLIENTS/ZWAVE_GATEWAY-<mqtt_name>/multicast/set`
Payload:

```js
{
"nodes": [2, 3, 4, 6]
"commandClass": 38,
"endpoint": 0,
"property": "targetValue",
"value": 80
}
```

## Special topics

Expand Down
72 changes: 57 additions & 15 deletions lib/Gateway.js
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ Gateway.prototype.start = async function () {
if (this.mqtt) {
this.mqtt.on('writeRequest', onWriteRequest.bind(this))
this.mqtt.on('broadcastRequest', onBroadRequest.bind(this))
this.mqtt.on('multicastRequest', onMulticastRequest.bind(this))
this.mqtt.on('apiCall', onApiRequest.bind(this))
this.mqtt.on('hassStatus', onHassStatus.bind(this))
this.mqtt.on('brokerStatus', onBrokerStatus.bind(this))
Expand Down Expand Up @@ -494,20 +495,36 @@ async function onApiRequest (topic, apiName, payload) {
* @param {string[]} parts
* @param {any} payload
*/
function onBroadRequest (parts, payload) {
const topic = parts.join('/')
const values = Object.keys(this.topicValues).filter(t => t.endsWith(topic))

if (values.length > 0) {
// all values are the same type just different node,parse the Payload by using the first one
payload = this.parsePayload(
payload,
this.topicValues[values[0]],
this.topicValues[values[0]].conf
)
for (let i = 0; i < values.length; i++) {
this.zwave.writeValue(this.topicValues[values[i]], payload)
async function onBroadRequest (parts, payload) {
if (parts.length > 0) {
// multiple writes (back compatibility mode)
const topic = parts.join('/')
const values = Object.keys(this.topicValues).filter(t => t.endsWith(topic))
if (values.length > 0) {
// all values are the same type just different node,parse the Payload by using the first one
payload = this.parsePayload(
payload,
this.topicValues[values[0]],
this.topicValues[values[0]].conf
)
for (let i = 0; i < values.length; i++) {
await this.zwave.writeValue(this.topicValues[values[i]], payload)
}
}
} else {
// try real zwave broadcast
if (payload.value === undefined) {
logger.error('No value found in broadcast request')
return
}

const error = utils.isValueId(payload)

if (typeof error === 'string') {
logger.error('Invalid valueId: ' + error)
return
}
await this.zwave.writeBroadcast(payload, payload.value)
}
}

Expand All @@ -517,13 +534,38 @@ function onBroadRequest (parts, payload) {
* @param {string[]} parts
* @param {any} payload
*/
function onWriteRequest (parts, payload) {
async function onWriteRequest (parts, payload) {
const valueId = this.topicValues[parts.join('/')]

if (valueId) {
payload = this.parsePayload(payload, valueId, valueId.conf)
this.zwave.writeValue(valueId, payload)
await this.zwave.writeValue(valueId, payload)
}
}

async function onMulticastRequest (payload) {
const nodes = payload.nodes
const valueId = payload
const value = payload.value

if (!nodes || nodes.length === 0) {
logger.error('No nodes found in multicast request')
return
}

const error = utils.isValueId(valueId)

if (typeof error === 'string') {
logger.error('Invalid valueId: ' + error)
return
}

if (payload.value === undefined) {
logger.error('No value found in multicast request')
return
}

await this.zwave.writeMulticast(nodes, valueId, value)
}

/**
Expand Down
10 changes: 8 additions & 2 deletions lib/MqttClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ const BROADCAST_PREFIX = '_BROADCAST'

const NAME_PREFIX = 'ZWAVE_GATEWAY-'

const ACTIONS = ['broadcast', 'api']
const ACTIONS = ['broadcast', 'api', 'multicast']

const HASS_WILL = 'homeassistant/status'

Expand Down Expand Up @@ -230,13 +230,19 @@ function onMessageReceived (topic, payload) {
switch (action) {
case 0: // broadcast
this.emit('broadcastRequest', parts.slice(3), payload)
// publish back to give a feedback the action is received
// publish back to give a feedback the action has been received
// same topic without /set suffix
this.publish(parts.join('/'), payload)
break
case 1: // api
this.emit('apiCall', parts.join('/'), parts[3], payload)
break
case 2: // multicast
this.emit('multicastRequest', payload)
// publish back to give a feedback the action has been received
// same topic without /set suffix
this.publish(parts.join('/'), payload)
break
default:
logger.warn(`Unknown action received ${action} ${topic}`)
}
Expand Down
58 changes: 56 additions & 2 deletions lib/ZwaveClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ const {
CommandClass,
libVersion
} = require('zwave-js')
const { CommandClasses, Duration } = require('@zwave-js/core')
const { CommandClasses, Duration, ZWaveErrorCodes } = require('@zwave-js/core')
const utils = reqlib('/lib/utils.js')
const EventEmitter = require('events')
const jsonStore = reqlib('/lib/jsonStore.js')
Expand Down Expand Up @@ -76,7 +76,9 @@ const allowedApis = [
'beginFirmwareUpdate',
'abortFirmwareUpdate',
'sendCommand',
'writeValue'
'writeValue',
'writeBroadcast',
'writeMulticast'
]

const ZWAVEJS_LOG_FILE = utils.joinPath(storeDir, `zwavejs_${process.pid}.log`)
Expand Down Expand Up @@ -2499,6 +2501,58 @@ ZwaveClient.prototype.callApi = async function (apiName, ...args) {
return result
}

/**
* Send broadcast write request
*
* @param {import('zwave-js').ValueID} valueId Zwave valueId object
* @param {unknown} value The value to send
*/
ZwaveClient.prototype.writeBroadcast = async function (valueId, value) {
if (this.driverReady) {
try {
const broadcastNode = this.driver.controller.getBroadcastNode()

await broadcastNode.setValue(valueId, value)
} catch (error) {
logger.error(
`Error while sending broadcast ${value} to CC ${valueId.commandClass} ${
valueId.property
} ${valueId.propertyKey || ''}: ${error.message}`
)
}
}
}

/**
* Send multicast write request to a group of nodes
*
* @param {number[]} nodes Array of nodes ids
* @param {import('zwave-js').ValueID} valueId Zwave valueId object
* @param {unknown} value The value to send
*/
ZwaveClient.prototype.writeMulticast = async function (nodes, valueId, value) {
if (this.driverReady) {
let fallback = false
try {
const multicastGroup = this.driver.controller.getMulticastGroup(nodes)
await multicastGroup.setValue(valueId, value)
} catch (error) {
fallback = error.code === ZWaveErrorCodes.CC_NotSupported
logger.error(
`Error while sending multicast ${value} to CC ${valueId.commandClass} ${
valueId.property
} ${valueId.propertyKey || ''}: ${error.message}`
)
}
// try single writes requests
if (fallback) {
for (const n of nodes) {
await this.writeValue({ ...valueId, nodeId: n }, value)
}
}
}
}

/**
* Set a value of a specific zwave valueId
*
Expand Down
9 changes: 8 additions & 1 deletion types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,7 @@ export interface MqttClient extends EventEmitter {
event: 'broadcastRequest',
listener: (parts: string[], payload: any) => void
): this
on(event: 'multicastRequest', listener: (payload: any) => void): this
on(
event: 'apiCall',
listener: (topic: string, apiNema: string, payload: any) => void
Expand Down Expand Up @@ -391,7 +392,13 @@ export interface ZwaveClient extends EventEmitter {
apiName: string,
...args: any
): Promise<{ success: boolean; message: string; result: any; args: any[] }>
writeValue(valueId: Z2MValueId, value: number | string): Promise<void>
writeBroadcast(valueId: Z2MValueId, value: unknown): Promise<void>
writeMulticast(
nodes: number[],
valueId: Z2MValueId,
value: unknown
): Promise<void>
writeValue(valueId: Z2MValueId, value: unknown): Promise<void>
sendCommand(
ctx: { nodeId: number; endpoint: number; commandClass: number },
command: string,
Expand Down

0 comments on commit 98e145d

Please sign in to comment.