Skip to content

Commit

Permalink
feat: zwave-js config updates (#1115)
Browse files Browse the repository at this point in the history
* feat: zwave-js config updates

* fix: docs for volumes

* fix: lint issues

* fix: add missing methods

* fix: notification text

* fix: use `installConfigUpdate`

* fix: add feedback
  • Loading branch information
robertsLando authored Apr 30, 2021
1 parent 3988049 commit 0a65549
Show file tree
Hide file tree
Showing 8 changed files with 163 additions and 30 deletions.
21 changes: 14 additions & 7 deletions docker/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,12 @@ Here there are 3 different way to start the container and provide data persisten
docker run --rm -it -p 8091:8091 -p 3000:3000 --device=/dev/ttyACM0 --mount source=zwavejs2mqtt,target=/usr/src/app/store zwavejs/zwavejs2mqtt:latest
```

If you want to support devices config updates you also need to setup a volume on `/usr/src/app/node_modules/@zwave-js/config`:

```bash
docker run --rm -it -p 8091:8091 -p 3000:3000 --device=/dev/ttyACM0 --mount source=zwave-config,target=/usr/src/app/node_modules/@zwave-js/config zwavejs/zwavejs2mqtt:latest
```

### Run using local folder

Here we will store our data in the current path (`$(pwd)`) named `store`. You can choose the path and the directory name you prefer, a valid alternative (with linux) could be `/var/lib/zwavejs2mqtt`
Expand All @@ -55,7 +61,7 @@ docker run --rm -it -p 8091:8091 -p 3000:3000 --device=/dev/ttyACM0 -v $(pwd)/st
To run the app as a service you can use the `docker-compose.yml` file you find [here](./docker-compose.yml). Here is the content:

```yml
version: '3.7'
version: "3.7"
services:
zwavejs2mqtt:
container_name: zwavejs2mqtt
Expand All @@ -69,17 +75,18 @@ services:
networks:
- zwave
devices:
- '/dev/ttyACM0:/dev/ttyACM0'
- "/dev/ttyACM0:/dev/ttyACM0"
volumes:
- ./store:/usr/src/app/store
- zwave-config:/usr/src/app/node_modules/@zwave-js/config
ports:
- '8091:8091' # port for web interface
- '3000:3000' # port for zwave-js websocket server
- "8091:8091" # port for web interface
- "3000:3000" # port for zwave-js websocket server
networks:
zwave:
# volumes:
# zwavejs2mqtt:
# name: zwavejs2mqtt
volumes:
zwave-config:
name: zwave-config
```
Like the other solutions, remember to replace device `/dev/ttyACM0` with the path of your USB stick and choose the solution you prefer for data persistence.
Expand Down
8 changes: 4 additions & 4 deletions docker/docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,12 @@ services:
- '/dev/ttyACM0:/dev/ttyACM0'
volumes:
- ./store:/usr/src/app/store
- zwave-config:/usr/src/app/node_modules/@zwave-js/config
ports:
- '8091:8091' # port for web interface
- '3000:3000' # port for zwave-js websocket server
networks:
zwave:
# volumes:
# zwavejs2mqtt:
# name: zwavejs2mqtt

volumes:
zwave-config:
name: zwave-config
1 change: 1 addition & 0 deletions lib/SocketManager.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const socketEvents = {
nodeUpdated: 'NODE_UPDATED',
valueUpdated: 'VALUE_UPDATED',
valueRemoved: 'VALUE_REMOVED',
info: 'INFO',
api: 'API_RETURN', // api results
debug: 'DEBUG'
}
Expand Down
68 changes: 67 additions & 1 deletion lib/ZwaveClient.js
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,9 @@ const allowedApis = [
'writeValue',
'writeBroadcast',
'writeMulticast',
'driverFunction'
'driverFunction',
'checkForConfigUpdates',
'installConfigUpdate'
]

const ZWAVEJS_LOG_FILE = utils.joinPath(storeDir, `zwavejs_${process.pid}.log`)
Expand Down Expand Up @@ -1523,6 +1525,11 @@ ZwaveClient.prototype.close = async function () {
this.healTimeout = null
}

if (this.updatesCheckTimeout) {
clearTimeout(this.updatesCheckTimeout)
this.updatesCheckTimeout = null
}

if (this.statelessTimeouts) {
for (const k in this.statelessTimeouts) {
clearTimeout(this.statelessTimeouts[k])
Expand Down Expand Up @@ -1895,6 +1902,8 @@ ZwaveClient.prototype.connect = async function () {
this.enableStatistics()
}

await scheduledConfigCheck.call(this)

this.status = ZWAVE_STATUS.connected
this.connected = true
} catch (error) {
Expand Down Expand Up @@ -2255,6 +2264,63 @@ ZwaveClient.prototype.setPollInterval = function (valueId, interval) {
throw Error('Driver is closed')
}
}
/**
* Internal function to check for config updates automatically once a day
*
*/
async function scheduledConfigCheck () {
try {
await this.checkForConfigUpdates()
} catch (error) {
logger.warn(`Scheduled update check has failed: ${error.message}`)
}

const nextUpdate = new Date()
nextUpdate.setHours(24, 0, 0, 0) // next midnight

const waitMillis = nextUpdate.getTime() - Date.now()

logger.info(`Next update scheduled for: ${nextUpdate}`)

this.updatesCheckTimeout = setTimeout(
scheduledConfigCheck.bind(this),
waitMillis > 0 ? waitMillis : 1000
)
}

/**
* Checks for configs updates
*
* @returns {Promise<string | undefined} The new version if present
*/
ZwaveClient.prototype.checkForConfigUpdates = async function () {
if (this.driver && !this.closed) {
this.driverInfo.newConfigVersion = await this.driver.checkForConfigUpdates()
this.sendToSocket(socketEvents.info, this.getInfo())
return this.driverInfo.newConfigVersion
} else {
throw Error('Driver is closed')
}
}

/**
* Checks for configs updates and installs them
*
* @returns {Promise<boolean>} True when update is installed, false otherwise
*/
ZwaveClient.prototype.installConfigUpdate = async function () {
if (this.driver && !this.closed) {
const updated = await this.driver.installConfigUpdate()
if (updated) {
this.driverInfo.newConfigVersion = undefined
this.sendToSocket(socketEvents.info, this.getInfo())
}
return updated
} else {
throw Error('Driver is closed')
}
}

/**
* Try to poll a value, don't throw. Used in the setTimeout
*
Expand Down
87 changes: 70 additions & 17 deletions src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,28 @@

<div class="controller-status">{{ appInfo.controllerStatus }}</div>

<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon
class="mr-3 ml-3"
dark
medium
style="cursor:default;"
:color="statusColor || 'orange'"
v-on="on"
>swap_horizontal_circle</v-icon
>
</template>
<span>{{ status }}</span>
</v-tooltip>

<v-tooltip bottom open-on-click>
<template v-slot:activator="{ on }">
<v-icon
dark
medium
style="cursor:default;margin:0 1rem"
class="mr-3"
style="cursor:default;"
color="primary"
v-on="on"
@click="copyVersion"
Expand Down Expand Up @@ -90,22 +106,25 @@

<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon
dark
medium
style="cursor:default;"
:color="statusColor || 'orange'"
<v-badge
v-on="on"
>swap_horizontal_circle</v-icon
class="mr-3"
:content="updateAvailable"
:value="updateAvailable"
color="red"
overlap
>
<v-btn small icon @click="showUpdateDialog">
<v-icon dark medium color="primary">history</v-icon>
</v-btn>
</v-badge>
</template>
<span>{{ status }}</span>
</v-tooltip>

<div v-if="auth">
<v-menu v-if="$vuetify.breakpoint.xsOnly" bottom left>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon>
<v-btn small v-on="on" icon>
<v-icon>more_vert</v-icon>
</v-btn>
</template>
Expand All @@ -127,7 +146,7 @@
<div v-else>
<v-menu v-for="item in menu" :key="item.text" bottom left>
<template v-slot:activator="{ on }">
<v-btn v-on="on" icon @click="item.func">
<v-btn small class="mr-2" v-on="on" icon @click="item.func">
<v-tooltip bottom>
<template v-slot:activator="{ on }">
<v-icon dark color="primary" v-on="on">{{
Expand Down Expand Up @@ -245,7 +264,10 @@ export default {
},
name: 'app',
computed: {
...mapGetters(['user', 'auth', 'appInfo'])
...mapGetters(['user', 'auth', 'appInfo']),
updateAvailable () {
return this.appInfo.newConfigVersion ? 1 : 0
}
},
watch: {
$route: function (value) {
Expand All @@ -266,15 +288,15 @@ export default {
dialog_password: false,
password: {},
menu: [
{
icon: 'logout',
func: this.logout,
tooltip: 'Logout'
},
{
icon: 'lock',
func: this.showPasswordDialog,
tooltip: 'Password'
},
{
icon: 'logout',
func: this.logout,
tooltip: 'Logout'
}
],
pages: [
Expand Down Expand Up @@ -376,6 +398,33 @@ export default {
this.status = status
this.statusColor = color
},
async showUpdateDialog () {
const newVersion = this.appInfo.newConfigVersion
const result = await this.confirm(
'Config updates',
newVersion
? `<div style="text-align:center"><p>New <b>zwave-js</b> config version available: <code>${newVersion}</code>.</p><p>Mind that some changes may require a <b>re-interview</b> of affected devices</p></div>`
: '<div style="text-align:center"><p>No updates available yet. Press on <b>CHECK</b> to trigger a new check.</p><p>By default checks are automatically done daily at midnight</p></div>',
'info',
{
width: 500,
cancelText: 'Close',
confirmText: newVersion ? 'Install' : 'Check'
}
)
if (result) {
this.apiRequest(
newVersion ? 'installConfigUpdate' : 'checkForConfigUpdates',
[]
)
this.showSnackbar(
newVersion ? 'Installation started' : 'Check requested'
)
}
},
changeThemeColor: function () {
const metaThemeColor = document.querySelector('meta[name=theme-color]')
const metaThemeColor2 = document.querySelector(
Expand Down Expand Up @@ -489,7 +538,7 @@ export default {
• Information about which version of <code>node-zwave-js</code> you are running;</br>
• The <b>manufacturer ID</b>, <b>product type</b>, <b>product ID</b>, and <b>firmware version</b> of each device that is part of your Z-Wave network.</br></br>
<p>Informations are sent <b>once a day</b> or, if you restart your network, when all nodes are ready. Collecting this information is critical to the user experience provided by Z-Wave JS.
<p>Informations are sent <b>once a day</b> or, if you restart your network, when all nodes are ready. Collecting this information is critical to the user experience provided by Z-Wave JS.
More information about the data that is collected and how it is used, including an example of the data collected, can be found <a target="_blank" href="https://zwave-js.github.io/node-zwave-js/#/data-collection/data-collection?id=usage-statistics">here</a>`,
'info</p>',
{
Expand Down Expand Up @@ -561,6 +610,10 @@ export default {
this.setAppInfo(data.info)
})
this.socket.on(socketEvents.info, data => {
this.setAppInfo(data)
})
this.socket.on(socketEvents.connected, this.setAppInfo.bind(this))
this.socket.on(
socketEvents.controller,
Expand Down
1 change: 1 addition & 0 deletions src/plugins/socket.js
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ export const socketEvents = {
nodeUpdated: 'NODE_UPDATED',
valueUpdated: 'VALUE_UPDATED',
valueRemoved: 'VALUE_REMOVED',
info: 'INFO',
api: 'API_RETURN', // api results
debug: 'DEBUG'
}
4 changes: 3 additions & 1 deletion src/store/mutations.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,8 @@ export const state = {
homeHex: '',
appVersion: '',
zwaveVersion: '',
controllerStatus: 'Unknown'
controllerStatus: 'Unknown',
newConfigVersion: undefined
}
}

Expand Down Expand Up @@ -136,6 +137,7 @@ export const mutations = {
state.appInfo.appVersion = data.appVersion
state.appInfo.zwaveVersion = data.zwaveVersion
state.appInfo.serverVersion = data.serverVersion
state.appInfo.newConfigVersion = data.newConfigVersion
},
setValue (state, valueId) {
const toReplace = getValue(valueId)
Expand Down
3 changes: 3 additions & 0 deletions types/index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -266,6 +266,7 @@ export type Z2MDriverInfo = {
homeId: string
name: string
controllerId: string
newConfigVersion: string | undefined
}

export enum ZwaveClientStatus {
Expand Down Expand Up @@ -377,6 +378,8 @@ export interface ZwaveClient extends EventEmitter {
getInfo(): Map<string, any>
refreshValues(nodeId: number): Promise<void>
setPollInterval(valueId: Z2MValueId, interval: number): void
checkForConfigUpdates(): Promise<string | undefined>
installConfigUpdate(): Promise<boolean>
pollValue(valueId: Z2MValueId): Promise<any>
replaceFailedNode(nodeId: number, secure: any): Promise<boolean>
startInclusion(secure: boolean): Promise<boolean>
Expand Down

0 comments on commit 0a65549

Please sign in to comment.