Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: multicast/broadcast apis #25

Merged
merged 32 commits into from
Mar 15, 2021
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
8dd1d36
feat: multicast api
robertsLando Nov 24, 2020
fa2679f
docs: fix lint
robertsLando Nov 24, 2020
0b63206
Merge branch 'master' into feat#multicast
robertsLando Nov 24, 2020
af42de6
fix: await writes to zwave broadcast and multicast requests
robertsLando Nov 25, 2020
b9fbecc
fix: lint issues
robertsLando Nov 25, 2020
badad4e
fix: use parsed payload in write value
robertsLando Nov 25, 2020
9570bec
Merge branch 'master' into feat#multicast
robertsLando Nov 25, 2020
9ce6fff
docs: json payload
robertsLando Nov 25, 2020
7f20390
Merge branch 'master' into feat#multicast
robertsLando Dec 7, 2020
1648adf
Merge branch 'master' into feat#multicast
robertsLando Dec 10, 2020
fbeccbd
feat: use native multicast and broadcast from zwave-js
robertsLando Dec 11, 2020
64dbcd5
docs: multicast and broadcast
robertsLando Dec 11, 2020
0d51cdd
Merge branch 'master' into feat#multicast
robertsLando Dec 14, 2020
6e1e51c
Merge branch 'master' into feat#multicast
robertsLando Jan 23, 2021
854b4fa
fix: replace debug with logger
robertsLando Jan 23, 2021
42cccfb
fix: replace debug with logger
robertsLando Jan 23, 2021
ddd63c6
fix: replace debug with logger
robertsLando Jan 23, 2021
0d1cb48
fix: replace debug with logger
robertsLando Jan 23, 2021
0e880b8
fix: replace debug with logger
robertsLando Jan 23, 2021
cafd301
fix: replace debug with logger
robertsLando Jan 23, 2021
4b63eed
fix: replace debug with logger
robertsLando Jan 23, 2021
c686a0f
fix: types
robertsLando Jan 23, 2021
22aed8c
fix: types
robertsLando Jan 23, 2021
f611bcc
fix: lint issues
robertsLando Jan 23, 2021
b1987a5
Merge branch 'master' into feat#multicast
robertsLando Mar 9, 2021
de8847b
fix: lint issues and missing fallbacks
robertsLando Mar 10, 2021
103575d
fix: broadcast/multicast docs
robertsLando Mar 10, 2021
4100f38
fix: lint issues
robertsLando Mar 10, 2021
c35ecb4
fix: docs nits
robertsLando Mar 10, 2021
36e4eb3
Merge branch 'master' into feat#multicast
robertsLando Mar 10, 2021
566dbef
Merge branch 'master' into feat#multicast
robertsLando Mar 15, 2021
19a8339
docs: fix docs/guide/mqtt.md
robertsLando Mar 15, 2021
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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 values to _all values with a specific suffix_ in the network.
robertsLando marked this conversation as resolved.
Show resolved Hide resolved
robertsLando marked this conversation as resolved.
Show resolved Hide resolved

`<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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like I wrote in my comment, I would check for ZWaveErrorCodes.CC_NotSupported and fall back to iterating singlecast requests.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@AlCalzone Could this cause multiple writes?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No, the checks happen before anything is sent.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

} 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