Add manual firmware update support for zwave-js devices (#12910)

* Subscribe to zwave_js node status updates in device panel

* Add typing for message

* Add manual firmware update support for zwave-js devices

* Tweaks based on upstream changes

* Tweaks

* remove unused CSS

* Update zwave_js.ts

* Tweaks after somet esting

* Bold device name instead of italic, catch abort errors and show the message

* Incorporate new commands tweak the UI and messaging

* Add a warning about firmware updates potentially bricking a device, and use Promise.all where possible

* Better typing so we can clean up code

* Additional tweaks

* Remove commented out code

* change style a bit

* prettier

* Be more precise with progress because it always helps the user if they can see progress

* nit

* Update src/translations/en.json

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>

* Bram's review

* Only ask for firmware target if there is more than one available

* Only offer another firmware update if the original firmware update failed

* Only offer firmware upgrade if node is ready and pass firmware capabilities into dialog so we don't have to make call again

* Use ha-form

* Add comment

* Switch schema name

* Import icon

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
This commit is contained in:
Raman Gupta 2022-06-23 04:51:52 -04:00 committed by GitHub
parent 7d118a5715
commit 2812b467ec
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 671 additions and 7 deletions

View File

@ -85,6 +85,22 @@ enum Protocols {
ZWaveLongRange = 1,
}
export enum FirmwareUpdateStatus {
Error_Timeout = -1,
Error_Checksum = 0,
Error_TransmissionFailed = 1,
Error_InvalidManufacturerID = 2,
Error_InvalidFirmwareID = 3,
Error_InvalidFirmwareTarget = 4,
Error_InvalidHeaderInformation = 5,
Error_InvalidHeaderFormat = 6,
Error_InsufficientMemory = 7,
Error_InvalidHardwareVersion = 8,
OK_WaitingForActivation = 0xfd,
OK_NoRestart = 0xfe,
OK_RestartPending = 0xff,
}
export interface QRProvisioningInformation {
version: QRCodeVersion;
securityClasses: SecurityClass[];
@ -147,7 +163,7 @@ export interface ZWaveJSController {
export interface ZWaveJSNodeStatus {
node_id: number;
ready: boolean;
status: number;
status: NodeStatus;
is_secure: boolean | string;
is_routing: boolean | null;
zwave_plus_version: number | null;
@ -281,6 +297,27 @@ export interface ZWaveJSNodeStatusUpdatedMessage {
status: NodeStatus;
}
export interface ZWaveJSNodeFirmwareUpdateProgressMessage {
event: "firmware update progress";
sent_fragments: number;
total_fragments: number;
}
export interface ZWaveJSNodeFirmwareUpdateFinishedMessage {
event: "firmware update finished";
status: FirmwareUpdateStatus;
wait_time: number;
}
export type ZWaveJSNodeFirmwareUpdateCapabilities =
| { firmware_upgradable: false }
| {
firmware_upgradable: true;
firmware_targets: number[];
continues_to_function: boolean | null;
supports_activation: boolean | null;
};
export interface ZWaveJSRemovedNode {
node_id: number;
manufacturer: string;
@ -628,6 +665,74 @@ export const subscribeZwaveNodeStatistics = (
}
);
export const fetchZwaveNodeIsFirmwareUpdateInProgress = (
hass: HomeAssistant,
device_id: string
): Promise<boolean> =>
hass.callWS({
type: "zwave_js/get_firmware_update_progress",
device_id,
});
export const fetchZwaveNodeFirmwareUpdateCapabilities = (
hass: HomeAssistant,
device_id: string
): Promise<ZWaveJSNodeFirmwareUpdateCapabilities> =>
hass.callWS({
type: "zwave_js/get_firmware_update_capabilities",
device_id,
});
export const uploadFirmwareAndBeginUpdate = async (
hass: HomeAssistant,
device_id: string,
file: File,
target?: number
) => {
const fd = new FormData();
fd.append("file", file);
if (target !== undefined) {
fd.append("target", target.toString());
}
const resp = await hass.fetchWithAuth(
`/api/zwave_js/firmware/upload/${device_id}`,
{
method: "POST",
body: fd,
}
);
if (resp.status !== 200) {
throw new Error(resp.statusText);
}
};
export const subscribeZwaveNodeFirmwareUpdate = (
hass: HomeAssistant,
device_id: string,
callbackFunction: (
message:
| ZWaveJSNodeFirmwareUpdateFinishedMessage
| ZWaveJSNodeFirmwareUpdateProgressMessage
) => void
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage(
(message: any) => callbackFunction(message),
{
type: "zwave_js/subscribe_firmware_update_status",
device_id,
}
);
export const abortZwaveNodeFirmwareUpdate = (
hass: HomeAssistant,
device_id: string
): Promise<UnsubscribeFunc> =>
hass.callWS({
type: "zwave_js/abort_firmware_update",
device_id,
});
export type ZWaveJSLogUpdate = ZWaveJSLogMessageUpdate | ZWaveJSLogConfigUpdate;
interface ZWaveJSLogMessageUpdate {

View File

@ -1,11 +1,16 @@
import { getConfigEntries } from "../../../../../../data/config_entries";
import { DeviceRegistryEntry } from "../../../../../../data/device_registry";
import { fetchZwaveNodeStatus } from "../../../../../../data/zwave_js";
import {
fetchZwaveNodeFirmwareUpdateCapabilities,
fetchZwaveNodeStatus,
} from "../../../../../../data/zwave_js";
import { showConfirmationDialog } from "../../../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../../../types";
import { showZWaveJSHealNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-heal-node";
import { showZWaveJSNodeStatisticsDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-node-statistics";
import { showZWaveJSReinterviewNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-reinterview-node";
import { showZWaveJSRemoveFailedNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-remove-failed-node";
import { showZWaveJUpdateFirmwareNodeDialog } from "../../../../integrations/integration-panels/zwave_js/show-dialog-zwave_js-update-firmware-node";
import type { DeviceAction } from "../../../ha-config-device-page";
export const getZwaveDeviceActions = async (
@ -27,13 +32,13 @@ export const getZwaveDeviceActions = async (
const entryId = configEntry.entry_id;
const node = await fetchZwaveNodeStatus(hass, device.id);
const nodeStatus = await fetchZwaveNodeStatus(hass, device.id);
if (!node || node.is_controller_node) {
if (!nodeStatus || nodeStatus.is_controller_node) {
return [];
}
return [
const actions = [
{
label: hass.localize(
"ui.panel.config.zwave_js.device_info.device_config"
@ -53,7 +58,7 @@ export const getZwaveDeviceActions = async (
label: hass.localize("ui.panel.config.zwave_js.device_info.heal_node"),
action: () =>
showZWaveJSHealNodeDialog(el, {
device: device,
device,
}),
},
{
@ -71,8 +76,41 @@ export const getZwaveDeviceActions = async (
),
action: () =>
showZWaveJSNodeStatisticsDialog(el, {
device: device,
device,
}),
},
];
if (!nodeStatus.ready) {
return actions;
}
const firmwareUpdateCapabilities =
await fetchZwaveNodeFirmwareUpdateCapabilities(hass, device.id);
if (firmwareUpdateCapabilities.firmware_upgradable) {
actions.push({
label: hass.localize(
"ui.panel.config.zwave_js.device_info.update_firmware"
),
action: async () => {
if (
await showConfirmationDialog(el, {
text: hass.localize(
"ui.panel.config.zwave_js.update_firmware.warning"
),
dismissText: hass.localize("ui.common.no"),
confirmText: hass.localize("ui.common.yes"),
})
) {
showZWaveJUpdateFirmwareNodeDialog(el, {
device,
firmwareUpdateCapabilities,
});
}
},
});
}
return actions;
};

View File

@ -0,0 +1,461 @@
import "../../../../../components/ha-file-upload";
import "../../../../../components/ha-form/ha-form";
import "../../../../../components/ha-svg-icon";
import "@material/mwc-button/mwc-button";
import "@material/mwc-linear-progress/mwc-linear-progress";
import { mdiCheckCircle, mdiCloseCircle, mdiFileUpload } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { fireEvent } from "../../../../../common/dom/fire_event";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import {
DeviceRegistryEntry,
computeDeviceName,
} from "../../../../../data/device_registry";
import {
abortZwaveNodeFirmwareUpdate,
fetchZwaveNodeIsFirmwareUpdateInProgress,
fetchZwaveNodeStatus,
FirmwareUpdateStatus,
NodeStatus,
subscribeZwaveNodeStatus,
subscribeZwaveNodeFirmwareUpdate,
uploadFirmwareAndBeginUpdate,
ZWaveJSNodeFirmwareUpdateFinishedMessage,
ZWaveJSNodeFirmwareUpdateProgressMessage,
ZWaveJSNodeStatusUpdatedMessage,
ZWaveJSNodeFirmwareUpdateCapabilities,
ZWaveJSNodeStatus,
} from "../../../../../data/zwave_js";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZWaveJSUpdateFirmwareNodeDialogParams } from "./show-dialog-zwave_js-update-firmware-node";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../../dialogs/generic/show-dialog-box";
import { HaFormIntegerSchema } from "../../../../../components/ha-form/types";
@customElement("dialog-zwave_js-update-firmware-node")
class DialogZWaveJSUpdateFirmwareNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private device?: DeviceRegistryEntry;
@state() private _uploading = false;
@state()
private _updateFinishedMessage?: ZWaveJSNodeFirmwareUpdateFinishedMessage;
@state()
private _updateProgressMessage?: ZWaveJSNodeFirmwareUpdateProgressMessage;
@state() private _updateInProgress = false;
@state() private _firmwareFile?: File;
@state() private _nodeStatus?: ZWaveJSNodeStatus;
@state() private _firmwareTarget? = 0;
private _subscribedNodeStatus?: Promise<UnsubscribeFunc>;
private _subscribedNodeFirmwareUpdate?: Promise<UnsubscribeFunc>;
private _deviceName?: string;
private _firmwareUpdateCapabilities?: ZWaveJSNodeFirmwareUpdateCapabilities;
public showDialog(params: ZWaveJSUpdateFirmwareNodeDialogParams): void {
this._deviceName = computeDeviceName(params.device, this.hass!);
this.device = params.device;
this._firmwareUpdateCapabilities = params.firmwareUpdateCapabilities;
this._fetchData();
this._subscribeNodeStatus();
}
public closeDialog(): void {
this._unsubscribeNodeFirmwareUpdate();
this._unsubscribeNodeStatus();
this.device =
this._updateProgressMessage =
this._updateFinishedMessage =
this._firmwareFile =
this._nodeStatus =
this._firmwareUpdateCapabilities =
undefined;
this._firmwareTarget = 0;
this._uploading = this._updateInProgress = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
private _schema = memoizeOne(
(
firmwareUpdateCapabilities: ZWaveJSNodeFirmwareUpdateCapabilities
): HaFormIntegerSchema => {
if (!firmwareUpdateCapabilities.firmware_upgradable) {
// We should never get here, this is to pass type checks
throw new Error();
}
return {
name: "firmware_target",
type: "integer",
valueMin: Math.min(...firmwareUpdateCapabilities.firmware_targets),
valueMax: Math.max(...firmwareUpdateCapabilities.firmware_targets),
};
}
);
protected render(): TemplateResult {
if (
!this.device ||
!this._nodeStatus ||
!this._firmwareUpdateCapabilities ||
!this._firmwareUpdateCapabilities.firmware_upgradable ||
this._updateInProgress === undefined
) {
return html``;
}
const beginFirmwareUpdateHTML = html`<ha-file-upload
.hass=${this.hass}
.uploading=${this._uploading}
.icon=${mdiFileUpload}
label=${this._firmwareFile?.name ??
this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.upload_firmware"
)}
@file-picked=${this._uploadFile}
></ha-file-upload>
${this._firmwareUpdateCapabilities.firmware_targets.length > 1
? html`<p>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.firmware_target_intro"
)}
</p>
<ha-form
.hass=${this.hass}
.data=${{ firmware_target: this._firmwareTarget }}
.schema=${[this._schema(this._firmwareUpdateCapabilities)]}
@value-changed=${this._firmwareTargetChanged}
></ha-form>`
: ""}
<mwc-button
slot="primaryAction"
@click=${this._beginFirmwareUpdate}
.disabled=${this._firmwareFile === undefined}
>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.begin_update"
)}
</mwc-button>`;
const abortFirmwareUpdateButton = html`
<mwc-button slot="primaryAction" @click=${this._abortFirmwareUpdate}>
${this.hass.localize("ui.panel.config.zwave_js.update_firmware.abort")}
</mwc-button>
`;
const status = this._updateFinishedMessage
? FirmwareUpdateStatus[this._updateFinishedMessage.status]
.split("_")[0]
.toLowerCase()
: undefined;
return html`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.panel.config.zwave_js.update_firmware.title")
)}
>
${!this._updateProgressMessage && !this._updateFinishedMessage
? !this._updateInProgress
? html`
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.introduction",
{
device: html`<strong>${this._deviceName}</strong>`,
}
)}
</p>
${beginFirmwareUpdateHTML}
`
: html`
<p>
${this._nodeStatus.status === NodeStatus.Asleep
? this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.queued",
{
device: html`<strong>${this._deviceName}</strong>`,
}
)
: this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.awake",
{
device: html`<strong>${this._deviceName}</strong>`,
}
)}
</p>
<p>
${this._nodeStatus.status === NodeStatus.Asleep
? this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.close_queued",
{
device: html`<strong>${this._deviceName}</strong>`,
}
)
: this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.close",
{
device: html`<strong>${this._deviceName}</strong>`,
}
)}
</p>
${abortFirmwareUpdateButton}
`
: this._updateProgressMessage && !this._updateFinishedMessage
? html`
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.in_progress",
{
device: html`<strong>${this._deviceName}</strong>`,
progress: (
(this._updateProgressMessage.sent_fragments * 100) /
this._updateProgressMessage.total_fragments
).toFixed(2),
}
)}
</p>
<mwc-linear-progress
determinate
.progress=${this._updateProgressMessage.sent_fragments /
this._updateProgressMessage.total_fragments}
></mwc-linear-progress>
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.close",
{
device: html`<strong>${this._deviceName}</strong>`,
}
)}
</p>
${abortFirmwareUpdateButton}
`
: html`
<div class="flex-container">
<ha-svg-icon
.path=${status === "ok" ? mdiCheckCircle : mdiCloseCircle}
.class=${status}
></ha-svg-icon>
<div class="status">
<p>
${this.hass.localize(
`ui.panel.config.zwave_js.update_firmware.finished_status.${status}`,
{
device: html`<strong>${this._deviceName}</strong>`,
message: this.hass.localize(
`ui.panel.config.zwave_js.update_firmware.finished_status.${
FirmwareUpdateStatus[
this._updateFinishedMessage!.status
]
}`
),
}
)}
</p>
</div>
</div>
${status === "ok"
? html`<p>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.finished_status.done"
)}
</p>`
: html`<p>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.finished_status.try_again"
)}
</p>
${beginFirmwareUpdateHTML}`}
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.finished_status.try_again"
)}
</p>
${beginFirmwareUpdateHTML}
`}
</ha-dialog>
`;
}
private async _fetchData(): Promise<void> {
[this._nodeStatus, this._updateInProgress] = await Promise.all([
fetchZwaveNodeStatus(this.hass, this.device!.id),
fetchZwaveNodeIsFirmwareUpdateInProgress(this.hass, this.device!.id),
]);
if (this._updateInProgress) {
this._subscribeNodeFirmwareUpdate();
}
}
private async _beginFirmwareUpdate(): Promise<void> {
this._uploading = true;
this._updateProgressMessage = this._updateFinishedMessage = undefined;
try {
this._subscribeNodeFirmwareUpdate();
await uploadFirmwareAndBeginUpdate(
this.hass,
this.device!.id,
this._firmwareFile!,
this._firmwareTarget
);
this._updateInProgress = true;
this._uploading = false;
} catch (err: any) {
this._unsubscribeNodeFirmwareUpdate();
this._uploading = false;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.upload_failed"
),
text: err.message,
confirmText: this.hass!.localize("ui.common.close"),
});
}
}
private async _abortFirmwareUpdate(): Promise<void> {
if (
await showConfirmationDialog(this, {
text: this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.confirm_abort",
{
device: html`<strong>${this._deviceName}</strong>`,
}
),
dismissText: this.hass!.localize("ui.common.no"),
confirmText: this.hass!.localize("ui.common.yes"),
})
) {
this._unsubscribeNodeFirmwareUpdate();
try {
await abortZwaveNodeFirmwareUpdate(this.hass, this.device!.id);
} catch (err: any) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.zwave_js.update_firmware.abort_failed"
),
text: err.message,
confirmText: this.hass!.localize("ui.common.close"),
});
}
this._firmwareFile = undefined;
this._updateFinishedMessage = undefined;
this._updateProgressMessage = undefined;
this._updateInProgress = false;
}
}
private _subscribeNodeStatus(): void {
if (!this.hass || !this.device || this._subscribedNodeStatus) {
return;
}
this._subscribedNodeStatus = subscribeZwaveNodeStatus(
this.hass,
this.device.id,
(message: ZWaveJSNodeStatusUpdatedMessage) => {
this._nodeStatus!.status = message.status;
}
);
}
private _unsubscribeNodeStatus(): void {
if (!this._subscribedNodeStatus) {
return;
}
this._subscribedNodeStatus.then((unsub) => unsub());
this._subscribedNodeStatus = undefined;
}
private _subscribeNodeFirmwareUpdate(): void {
if (!this.hass || !this.device || this._subscribedNodeFirmwareUpdate) {
return;
}
this._subscribedNodeFirmwareUpdate = subscribeZwaveNodeFirmwareUpdate(
this.hass,
this.device.id,
(
message:
| ZWaveJSNodeFirmwareUpdateFinishedMessage
| ZWaveJSNodeFirmwareUpdateProgressMessage
) => {
if (message.event === "firmware update progress") {
if (!this._updateFinishedMessage) {
this._updateProgressMessage = message;
}
} else {
this._unsubscribeNodeFirmwareUpdate();
this._updateProgressMessage = undefined;
this._updateInProgress = false;
this._updateFinishedMessage = message;
}
}
);
}
private _unsubscribeNodeFirmwareUpdate(): void {
if (!this._subscribedNodeFirmwareUpdate) {
return;
}
this._subscribedNodeFirmwareUpdate.then((unsub) => unsub());
this._subscribedNodeFirmwareUpdate = undefined;
}
private async _firmwareTargetChanged(ev) {
this._firmwareTarget = ev.detail.value.firmware_target;
}
private async _uploadFile(ev) {
this._firmwareFile = ev.detail.files[0];
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
css`
.ok {
color: var(--success-color);
}
.error {
color: var(--error-color);
}
.flex-container {
display: flex;
align-items: center;
margin-bottom: 5px;
}
ha-svg-icon {
width: 68px;
height: 48px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-zwave_js-update-firmware-node": DialogZWaveJSUpdateFirmwareNode;
}
}

View File

@ -0,0 +1,22 @@
import { fireEvent } from "../../../../../common/dom/fire_event";
import { DeviceRegistryEntry } from "../../../../../data/device_registry";
import { ZWaveJSNodeFirmwareUpdateCapabilities } from "../../../../../data/zwave_js";
export interface ZWaveJSUpdateFirmwareNodeDialogParams {
device: DeviceRegistryEntry;
firmwareUpdateCapabilities: ZWaveJSNodeFirmwareUpdateCapabilities;
}
export const loadUpdateFirmwareNodeDialog = () =>
import("./dialog-zwave_js-update-firmware-node");
export const showZWaveJUpdateFirmwareNodeDialog = (
element: HTMLElement,
updateFirmwareNodeDialogParams: ZWaveJSUpdateFirmwareNodeDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-zwave_js-update-firmware-node",
dialogImport: loadUpdateFirmwareNodeDialog,
dialogParams: updateFirmwareNodeDialogParams,
});
};

View File

@ -3107,6 +3107,7 @@
"reinterview_device": "Re-interview Device",
"heal_node": "Heal Device",
"remove_failed": "Remove Failed Device",
"update_firmware": "Update Device Firmware",
"highest_security": "Highest Security",
"unknown": "Unknown",
"zwave_plus": "Z-Wave Plus",
@ -3314,6 +3315,43 @@
"in_progress": "{device} healing is in progress.",
"network_heal_in_progress": "A Z-Wave network heal is already in progress. Please wait for it to finish before healing an individual device."
},
"update_firmware": {
"title": "Update Device Firmware",
"warning": "WARNING: Firmware updates can brick your device if you do not correctly follow the manufacturer's guidance. The Home Assistant and Z-Wave JS teams do not take any responsibility for any damages to your device as a result of the firmware update and will not be able to help you if you brick your device. Would you still like to continue?",
"introduction": "Select the firmware file you would like to use to update {device}.",
"upload_firmware": "Upload Firmware",
"firmware_target_intro": "Select the firmware target (0 for the Z-Wave chip, ≥1 for other chips if they exist) for this update, or uncheck the box to have the driver attempt to figure it out from the firmware file.",
"firmware_target": "Firmware Target (chip)",
"upload_failed": "Upload Failed",
"begin_update": "Begin Firmware Update",
"queued": "The firmware update is ready to be sent to {device} but the device is asleep, wake the device to start the update.",
"close_queued": "If you close this dialog, the update will continue to be queued in the background and start automatically once the device wakes up.",
"awake": "The firmware update should start being sent to {device} shortly.",
"close": "If you close this dialog, the update will continue in the background.",
"in_progress": "The firmware update on {device} is in progress ({progress}%).",
"abort": "Abort Firmware Update",
"abort_failed": "Abort Failed",
"confirm_abort": "Are you sure you want to abort the firmware update on {device}?",
"finished_status": {
"ok": "Successfully updated firmware on {device}: {message}.",
"error": "Unable to update firmware on {device}: {message}.",
"try_again": "To attempt the firmware update again, select the new firmware file you would like to use.",
"done": "The firmware update is complete! If you want to attempt another firmware update on this device, please wait until it gets re-interviewed.",
"Error_Timeout": "Timed Out",
"Error_Checksum": "Checksum Error",
"Error_TransmissionFailed": "Transmission Failed",
"Error_InvalidManufacturerID": "Invalid Manufacturer ID",
"Error_InvalidFirmwareID": "Invalid Firmware ID",
"Error_InvalidFirmwareTarget": "Invalid Firmware Target",
"Error_InvalidHeaderInformation": "Invalid Header Information",
"Error_InvalidHeaderFormat": "Invalid Header Format",
"Error_InsufficientMemory": "Insufficient Memory",
"Error_InvalidHardwareVersion": "Invalid Hardware Version",
"OK_WaitingForActivation": "Waiting for Activiation",
"OK_NoRestart": "No Restart",
"OK_RestartPending": "Restart Pending"
}
},
"logs": {
"title": "Z-Wave JS Logs",
"log_level": "Log Level",