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: ota device firmware updates #2504

Merged
merged 8 commits into from Jul 6, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
54 changes: 54 additions & 0 deletions lib/ZwaveClient.ts
Expand Up @@ -63,6 +63,8 @@ import {
ZWavePlusRoleType,
ZWaveNodeEvents,
SerialAPISetupCommand,
FirmwareUpdateFileInfo,
FirmwareUpdateInfo,
} from 'zwave-js'
import { getEnumMemberName, parseQRCodeString } from 'zwave-js/Utils'
import { nvmBackupsDir, storeDir } from '../config/app'
Expand Down Expand Up @@ -148,6 +150,8 @@ const allowedApis = validateMethods([
'refreshInfo',
'beginFirmwareUpdate',
'abortFirmwareUpdate',
'getAvailableFirmwareUpdates',
'beginOTAFirmwareUpdate',
'sendCommand',
'writeValue',
'writeBroadcast',
Expand Down Expand Up @@ -1877,6 +1881,56 @@ class ZwaveClient extends TypedEventEmitter<ZwaveClientEventCallbacks> {
}
}

async getAvailableFirmwareUpdates(nodeId: number) {
if (this.driverReady) {
const result =
await this._driver.controller.getAvailableFirmwareUpdates(
nodeId
)

// result = [
// {
// version: '1.13',
// changelog: '* Fixed some bugs\n* Added other bugs',
// files: [
// {
// target: 0,
// integrity:
// 'sha256:123456789012345678901234567890123456789012345678901234567890123',
// url: 'https://example.com/firmware.bin',
// },
// {
// target: 1,
// integrity:
// 'sha256:123456789012345678901234567890123456789012345678901234567890123',
// url: 'https://example.com/firmware.bin',
// },
// ],
// },
// ]

return result
}

throw new DriverNotReadyError()
}

async beginOTAFirmwareUpdate(
nodeId: number,
update: FirmwareUpdateFileInfo
) {
if (this.driverReady) {
const result = await this._driver.controller.beginOTAFirmwareUpdate(
nodeId,
update
)

return result
}

throw new DriverNotReadyError()
}

async setPowerlevel(
powerlevel: number,
measured0dBm: number
Expand Down
16 changes: 15 additions & 1 deletion src/components/nodes-table/ExpandedNode.vue
Expand Up @@ -97,6 +97,9 @@
<v-tab key="groups" class="justify-start">
<v-icon small left>device_hub</v-icon> Groups
</v-tab>
<v-tab key="ota" class="justify-start">
<v-icon small left>auto_mode</v-icon> OTA Updates
</v-tab>
<v-tab key="events" class="justify-start">
<v-icon small left>list_alt</v-icon> Events
</v-tab>
Expand Down Expand Up @@ -126,7 +129,7 @@
></node-details>
</v-tab-item>

<!-- TAB NODE -->
<!-- TAB METADATA -->
<v-tab-item
v-if="nodeMetadata"
key="manual"
Expand Down Expand Up @@ -171,6 +174,15 @@
<association-groups :node="node" :socket="socket" />
</v-tab-item>

<!-- TAB OTA UPDATES -->
<v-tab-item key="ota" transition="slide-y-transition">
<OTAUpdates
:node="node"
:socket="socket"
v-on="$listeners"
/>
</v-tab-item>

<!-- TAB EVENTS -->
<v-tab-item key="events" transition="slide-y-transition">
<v-container grid-list-md>
Expand Down Expand Up @@ -279,6 +291,7 @@ import StatisticsCard from '@/components/custom/StatisticsCard.vue'
import { jsonToList } from '@/lib/utils'

import { mapGetters } from 'vuex'
import OTAUpdates from './OTAUpdates.vue'

export default {
props: {
Expand All @@ -294,6 +307,7 @@ export default {
NodeDetails,
DialogAdvanced,
StatisticsCard,
OTAUpdates,
},
computed: {
...mapGetters(['gateway', 'mqtt']),
Expand Down
162 changes: 162 additions & 0 deletions src/components/nodes-table/OTAUpdates.vue
@@ -0,0 +1,162 @@
<template>
<v-container grid-list-md>
<v-row class="ml-5">
<v-col class="text-center" cols="12">
<v-btn
v-if="!loading"
text
color="green"
@click="checkUpdates"
class="mb-2"
>Check updates</v-btn
>
</v-col>

<template v-if="fwUpdates.length > 0">
<v-col
cols="12"
sm="2"
md="4"
v-for="u in fwUpdates"
:key="u.version"
>
<v-card dense elevation="5">
<v-card-title>
<v-icon>mdi-update</v-icon>
<span class="headline"
><strong>v{{ u.version }}</strong></span
>
</v-card-title>
<v-divider class="mx-4"></v-divider>
<v-card-text>
<v-subheader class="subtitle-1"
><strong>Changelog</strong></v-subheader
>
<p
class="text-caption ml-4"
v-text="u.changelog"
style="white-space: pre"
></p>

<v-list-item
@click.stop="updateFirmware(u, f)"
v-for="f in u.files"
:key="f.url"
two-line
dense
style="border-bottom: 1px solid #e0e0e0"
>
<v-list-item-icon class="my-auto mr-3">
<v-icon color="primary">widgets</v-icon>
</v-list-item-icon>
<v-list-item-content>
<v-list-item-title
>Target:
{{ f.target }}</v-list-item-title
>
<v-list-item-subtitle>{{
f.url
}}</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-icon class="my-auto">
<v-icon color="success">download</v-icon>
</v-list-item-icon>
</v-list-item>
</v-card-text>
</v-card>
</v-col>
</template>
<v-col class="text-center" v-else-if="loading">
<v-progress-circular indeterminate color="primary" />
<p class="text-caption">
Remember to weak up sleeping devices...
</p>
</v-col>
<v-col class="text-center" v-else>
<h1 class="title">No updates available</h1>
</v-col>
</v-row>
</v-container>
</template>

<script>
import {
socketEvents,
inboundEvents as socketActions,
} from '@/../server/lib/SocketEvents'
import { mapMutations } from 'vuex'

export default {
components: {},
props: {
node: Object,
socket: Object,
},
data() {
return {
fwUpdates: [],
loading: false,
}
},
computed: {},
mounted() {
this.socket.on(socketEvents.api, (data) => {
if (
data.api === 'getAvailableFirmwareUpdates' &&
data.originalArgs[0] === this.node.id
) {
this.loading = false
if (data.success) {
this.fwUpdates = data.result
}
}
})

this.checkUpdates()
},
methods: {
...mapMutations(['showSnackbar']),
apiRequest(apiName, args) {
if (this.socket.connected) {
const data = {
api: apiName,
args: args,
}
this.socket.emit(socketActions.zwave, data)
} else {
this.showSnackbar('Socket disconnected')
}
},
checkUpdates() {
this.loading = true
this.fwUpdates = []
this.apiRequest('getAvailableFirmwareUpdates', [this.node.id])
},
async updateFirmware(update, firmware) {
if (
await this.$listeners.showConfirm(
'OTA Update',
`<p>Are you sure you want to update target <b>${firmware.target}</b> to <b>v${update.version}</b>?</p>

<p><strong>We don't take any responsibility if devices upgraded using Z-Wave JS don't work after an update. Always double-check that the correct update is about to be installed</strong></p>

<p>This will download the desired firmware update from the <a href="https://github.com/zwave-js/firmware-updates/">Z-Wave JS firmware update service</a> and start an over-the-air (OTA) firmware update for the given node.</p>

`,
'warning',
{
confirmText: 'Update',
cancelText: 'Cancel',
width: '500px',
}
)
) {
this.apiRequest('beginOTAFirmwareUpdate', [
this.node.id,
firmware,
])
}
},
},
}
</script>