Add S2 support to Z wave JS (#10090)

Co-authored-by: Martin Hjelmare <marhje52@gmail.com>
Co-authored-by: kpine <keith.pine@gmail.com>
This commit is contained in:
Bram Kragten 2021-09-29 00:37:32 +02:00 committed by GitHub
parent 68095417b9
commit b26c44b2b9
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
3 changed files with 516 additions and 163 deletions

View File

@ -2,6 +2,61 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { DeviceRegistryEntry } from "./device_registry";
export enum InclusionStrategy {
/**
* Always uses Security S2 if supported, otherwise uses Security S0 for certain devices which don't work without encryption and uses no encryption otherwise.
*
* Issues a warning if Security S0 or S2 is supported, but the secure bootstrapping fails.
*
* **This is the recommended** strategy and should be used unless there is a good reason not to.
*/
Default = 0,
/**
* Include using SmartStart (requires Security S2).
* Issues a warning if Security S2 is not supported, or the secure bootstrapping fails.
*
* **Should be preferred** over **Default** if supported.
*/
SmartStart,
/**
* Don't use encryption, even if supported.
*
* **Not recommended**, because S2 should be used where possible.
*/
Insecure,
/**
* Use Security S0, even if a higher security mode is supported.
*
* Issues a warning if Security S0 is not supported or the secure bootstrapping fails.
*
* **Not recommended** because S0 should be used sparingly and S2 preferred whereever possible.
*/
Security_S0,
/**
* Use Security S2 and issue a warning if it is not supported or the secure bootstrapping fails.
*
* **Not recommended** because the *Default* strategy is more versatile and user-friendly.
*/
Security_S2,
}
export enum SecurityClass {
/**
* Used internally during inclusion of a node. Don't use this!
*/
Temporary = -2,
/**
* `None` is used to indicate that a node is included without security.
* It is not meant as input to methods that accept a security class.
*/
None = -1,
S2_Unauthenticated = 0,
S2_Authenticated = 1,
S2_AccessControl = 2,
S0_Legacy = 7,
}
export interface ZWaveJSNodeIdentifiers {
home_id: string;
node_id: number;
@ -107,6 +162,16 @@ export enum NodeStatus {
Alive,
}
export interface RequestedGrant {
/**
* An array of security classes that are requested or to be granted.
* The granted security classes MUST be a subset of the requested ones.
*/
securityClasses: SecurityClass[];
/** Whether client side authentication is requested or to be granted */
clientSideAuth: boolean;
}
export const nodeStatus = ["unknown", "asleep", "awake", "dead", "alive"];
export const fetchNetworkStatus = (
@ -138,6 +203,48 @@ export const setDataCollectionPreference = (
opted_in,
});
export const subscribeAddNode = (
hass: HomeAssistant,
entry_id: string,
callbackFunction: (message: any) => void,
inclusion_strategy: InclusionStrategy = InclusionStrategy.Default
): Promise<UnsubscribeFunc> =>
hass.connection.subscribeMessage((message) => callbackFunction(message), {
type: "zwave_js/add_node",
entry_id: entry_id,
inclusion_strategy,
});
export const stopInclusion = (hass: HomeAssistant, entry_id: string) =>
hass.callWS({
type: "zwave_js/stop_inclusion",
entry_id,
});
export const grantSecurityClasses = (
hass: HomeAssistant,
entry_id: string,
security_classes: SecurityClass[],
client_side_auth?: boolean
) =>
hass.callWS({
type: "zwave_js/grant_security_classes",
entry_id,
security_classes,
client_side_auth,
});
export const validateDskAndEnterPin = (
hass: HomeAssistant,
entry_id: string,
pin: string
) =>
hass.callWS({
type: "zwave_js/validate_dsk_and_enter_pin",
entry_id,
pin,
});
export const fetchNodeStatus = (
hass: HomeAssistant,
entry_id: string,

View File

@ -1,15 +1,30 @@
import "@material/mwc-button/mwc-button";
import { mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import "../../../../../components/ha-switch";
import "../../../../../components/ha-formfield";
import { CSSResultGroup, html, LitElement, TemplateResult, css } from "lit";
import { mdiAlertCircle, mdiCheckCircle, mdiCloseCircle } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress";
import { customElement, property, state } from "lit/decorators";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import { haStyleDialog } from "../../../../../resources/styles";
import "../../../../../components/ha-formfield";
import "../../../../../components/ha-switch";
import {
grantSecurityClasses,
InclusionStrategy,
RequestedGrant,
SecurityClass,
stopInclusion,
subscribeAddNode,
validateDskAndEnterPin,
} from "../../../../../data/zwave_js";
import { haStyle, haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
import { ZWaveJSAddNodeDialogParams } from "./show-dialog-zwave_js-add-node";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-radio";
import { HaCheckbox } from "../../../../../components/ha-checkbox";
import "../../../../../components/ha-alert";
export interface ZWaveJSAddNodeDevice {
id: string;
@ -20,23 +35,38 @@ export interface ZWaveJSAddNodeDevice {
class DialogZWaveJSAddNode extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private entry_id?: string;
@state() private _entryId?: string;
@state() private _use_secure_inclusion = false;
@state() private _status = "";
@state() private _nodeAdded = false;
@state() private _status?:
| "loading"
| "started"
| "choose_strategy"
| "interviewing"
| "failed"
| "timed_out"
| "finished"
| "validate_dsk_enter_pin"
| "grant_security_classes";
@state() private _device?: ZWaveJSAddNodeDevice;
@state() private _stages?: string[];
private _stoppedTimeout?: any;
@state() private _inclusionStrategy?: InclusionStrategy;
@state() private _dsk?: string;
@state() private _error?: string;
@state() private _requestedGrant?: RequestedGrant;
@state() private _securityClasses: SecurityClass[] = [];
@state() private _lowSecurity = false;
private _addNodeTimeoutHandle?: number;
private _subscribed?: Promise<() => Promise<void>>;
private _subscribed?: Promise<UnsubscribeFunc>;
public disconnectedCallback(): void {
super.disconnectedCallback();
@ -44,11 +74,15 @@ class DialogZWaveJSAddNode extends LitElement {
}
public async showDialog(params: ZWaveJSAddNodeDialogParams): Promise<void> {
this.entry_id = params.entry_id;
this._entryId = params.entry_id;
this._status = "loading";
this._startInclusion();
}
@query("#pin-input") private _pinInput?: PaperInputElement;
protected render(): TemplateResult {
if (!this.entry_id) {
if (!this._entryId) {
return html``;
}
@ -61,46 +95,153 @@ class DialogZWaveJSAddNode extends LitElement {
this.hass.localize("ui.panel.config.zwave_js.add_node.title")
)}
>
${this._status === ""
? html`
<p>
${this.hass.localize(
"ui.panel.config.zwave_js.add_node.introduction"
)}
</p>
<div class="secure_inclusion_field">
${this._status === "loading"
? html`<div style="display: flex; justify-content: center;">
<ha-circular-progress size="large" active></ha-circular-progress>
</div>`
: this._status === "choose_strategy"
? html`<h3>Choose strategy</h3>
<div class="flex-column">
<ha-formfield
.label=${this.hass.localize(
"ui.panel.config.zwave_js.add_node.use_secure_inclusion"
)}
.label=${html`<b>Secure if possible</b>
<div class="secondary">
Requires user interaction during inclusion. Fast and
secure with S2 when supported. Fallback to legacy S0 or no
encryption when necessary.
</div>`}
>
<ha-switch
@change=${this._secureInclusionToggleChanged}
.checked=${this._use_secure_inclusion}
></ha-switch>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Default}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Default ||
this._inclusionStrategy === undefined}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Legacy Secure</b>
<div class="secondary">
Uses the older S0 security that is secure, but slow due to
a lot of overhead. Allows securely including S2 capable
devices which fail to be included with S2.
</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Security_S0}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Security_S0}
>
</ha-radio>
</ha-formfield>
<ha-formfield
.label=${html`<b>Insecure</b>
<div class="secondary">Do not use encryption.</div>`}
>
<ha-radio
name="strategy"
@change=${this._handleStrategyChange}
.value=${InclusionStrategy.Insecure}
.checked=${this._inclusionStrategy ===
InclusionStrategy.Insecure}
>
</ha-radio>
</ha-formfield>
<p>
<em>
<small>
${this.hass!.localize(
"ui.panel.config.zwave_js.add_node.secure_inclusion_warning"
)}
</small>
</em>
</p>
</div>
<mwc-button slot="primaryAction" @click=${this._startInclusion}>
${this._use_secure_inclusion
? html`${this.hass.localize(
"ui.panel.config.zwave_js.add_node.start_secure_inclusion"
)}`
: html` ${this.hass.localize(
"ui.panel.config.zwave_js.add_node.start_inclusion"
)}`}
<mwc-button
slot="primaryAction"
@click=${this._startManualInclusion}
>
Search device
</mwc-button>`
: this._status === "validate_dsk_enter_pin"
? html`
<p>
Please enter the 5-digit PIN for your device and verify that
the rest of the device-specific key matches the one that can
be found on your device or the manual.
</p>
${
this._error
? html`<ha-alert alert-type="error"
>${this._error}</ha-alert
>`
: ""
}
<div class="flex-container">
<paper-input
label="PIN"
id="pin-input"
@keyup=${this._handlePinKeyUp}
no-label-float
></paper-input>
${this._dsk}
</div>
<mwc-button
slot="primaryAction"
@click=${this._validateDskAndEnterPin}
>
Submit
</mwc-button>
</div>
`
: this._status === "grant_security_classes"
? html`
<h3>The device has requested the following security classes:</h3>
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
<div class="flex-column">
${this._requestedGrant?.securityClasses
.sort()
.reverse()
.map(
(securityClass) => html`<ha-formfield
.label=${html`<b
>${this.hass.localize(
`ui.panel.config.zwave_js.add_node.security_classes.${SecurityClass[securityClass]}.title`
)}</b
>
<div class="secondary">
${this.hass.localize(
`ui.panel.config.zwave_js.add_node.security_classes.${SecurityClass[securityClass]}.description`
)}
</div>`}
>
<ha-checkbox
@change=${this._handleSecurityClassChange}
.value=${securityClass}
.checked=${this._securityClasses.includes(
securityClass
)}
>
</ha-checkbox>
</ha-formfield>`
)}
</div>
<mwc-button
slot="primaryAction"
.disabled=${!this._securityClasses.length}
@click=${this._grantSecurityClasses}
>
Submit
</mwc-button>
`
: ``}
${this._status === "started"
: this._status === "timed_out"
? html`
<h3>Timed out!</h3>
<p>
We have not found any device in inclusion mode. Make sure the
device is active and in inclusion mode.
</p>
<mwc-button slot="primaryAction" @click=${this._startInclusion}>
Retry
</mwc-button>
`
: this._status === "started"
? html`
<div class="flex-container">
<ha-circular-progress active></ha-circular-progress>
@ -117,6 +258,14 @@ class DialogZWaveJSAddNode extends LitElement {
"ui.panel.config.zwave_js.add_node.follow_device_instructions"
)}
</p>
<p>
<button
class="link"
@click=${this._chooseInclusionStrategy}
>
Advanced inclusion
</button>
</p>
</div>
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
@ -125,8 +274,7 @@ class DialogZWaveJSAddNode extends LitElement {
)}
</mwc-button>
`
: ``}
${this._status === "interviewing"
: this._status === "interviewing"
? html`
<div class="flex-container">
<ha-circular-progress active></ha-circular-progress>
@ -159,8 +307,7 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
</mwc-button>
`
: ``}
${this._status === "failed"
: this._status === "failed"
? html`
<div class="flex-container">
<ha-svg-icon
@ -194,13 +341,12 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
</mwc-button>
`
: ``}
${this._status === "finished"
: this._status === "finished"
? html`
<div class="flex-container">
<ha-svg-icon
.path=${mdiCheckCircle}
class="success"
.path=${this._lowSecurity ? mdiAlertCircle : mdiCheckCircle}
class=${this._lowSecurity ? "warning" : "success"}
></ha-svg-icon>
<div class="status">
<p>
@ -208,6 +354,15 @@ class DialogZWaveJSAddNode extends LitElement {
"ui.panel.config.zwave_js.add_node.inclusion_finished"
)}
</p>
${this._lowSecurity
? html`<ha-alert
alert-type="warning"
title="The device was added insecurely"
>
There was an error during secure inclusion. You can try
again by excluding the device and adding it again.
</ha-alert>`
: ""}
<a href="${`/config/devices/device/${this._device!.id}`}">
<mwc-button>
${this.hass.localize(
@ -236,78 +391,146 @@ class DialogZWaveJSAddNode extends LitElement {
${this.hass.localize("ui.panel.config.zwave_js.common.close")}
</mwc-button>
`
: ``}
: ""}
</ha-dialog>
`;
}
private async _secureInclusionToggleChanged(ev): Promise<void> {
const target = ev.target;
this._use_secure_inclusion = target.checked;
private _chooseInclusionStrategy(): void {
this._unsubscribe();
this._status = "choose_strategy";
}
private _handleStrategyChange(ev: CustomEvent): void {
this._inclusionStrategy = (ev.target as any).value;
}
private _handleSecurityClassChange(ev: CustomEvent): void {
const checkbox = ev.currentTarget as HaCheckbox;
const securityClass = Number(checkbox.value);
if (checkbox.checked && !this._securityClasses.includes(securityClass)) {
this._securityClasses = [...this._securityClasses, securityClass];
} else if (!checkbox.checked) {
this._securityClasses = this._securityClasses.filter(
(val) => val !== securityClass
);
}
}
private _handlePinKeyUp(ev: KeyboardEvent) {
if (ev.key === "Enter") {
this._validateDskAndEnterPin();
}
}
private async _validateDskAndEnterPin(): Promise<void> {
this._status = "loading";
this._error = undefined;
try {
await validateDskAndEnterPin(
this.hass,
this._entryId!,
this._pinInput!.value as string
);
} catch (err) {
this._error = err.message;
this._status = "validate_dsk_enter_pin";
}
}
private async _grantSecurityClasses(): Promise<void> {
this._status = "loading";
this._error = undefined;
try {
await grantSecurityClasses(
this.hass,
this._entryId!,
this._securityClasses
);
} catch (err) {
this._error = err.message;
this._status = "grant_security_classes";
}
}
private _startManualInclusion() {
if (!this._inclusionStrategy) {
this._inclusionStrategy = InclusionStrategy.Default;
}
this._startInclusion();
}
private _startInclusion(): void {
if (!this.hass) {
return;
}
this._subscribed = this.hass.connection.subscribeMessage(
(message) => this._handleMessage(message),
{
type: "zwave_js/add_node",
entry_id: this.entry_id,
secure: this._use_secure_inclusion,
}
);
this._addNodeTimeoutHandle = window.setTimeout(
() => this._unsubscribe(),
90000
);
}
private _handleMessage(message: any): void {
if (message.event === "inclusion started") {
this._status = "started";
}
if (message.event === "inclusion failed") {
this._unsubscribe();
this._status = "failed";
}
if (message.event === "inclusion stopped") {
// we get the inclusion stopped event before the node added event
// during a successful inclusion. so we set a timer to wait 3 seconds
// to give the node added event time to come in before assuming it
// timed out or was cancelled and unsubscribing.
this._stoppedTimeout = setTimeout(() => {
if (!this._nodeAdded) {
this._status = "";
this._unsubscribe();
this._stoppedTimeout = undefined;
this._lowSecurity = false;
this._subscribed = subscribeAddNode(
this.hass,
this._entryId!,
(message) => {
if (message.event === "inclusion started") {
this._status = "started";
}
if (message.event === "inclusion failed") {
this._unsubscribe();
this._status = "failed";
}
if (message.event === "inclusion stopped") {
// We either found a device, or it failed, either way, cancel the timeout as we are no longer searching
if (this._addNodeTimeoutHandle) {
clearTimeout(this._addNodeTimeoutHandle);
}
this._addNodeTimeoutHandle = undefined;
}
}, 3000);
}
if (message.event === "device registered") {
this._device = message.device;
}
if (message.event === "node added") {
this._nodeAdded = true;
if (this._stoppedTimeout) {
clearTimeout(this._stoppedTimeout);
}
this._status = "interviewing";
}
if (message.event === "interview completed") {
this._status = "finished";
if (message.event === "validate dsk and enter pin") {
this._status = "validate_dsk_enter_pin";
this._dsk = message.dsk;
}
if (message.event === "grant security classes") {
if (this._inclusionStrategy === undefined) {
grantSecurityClasses(
this.hass,
this._entryId!,
message.requested_grant.securityClasses,
message.requested_grant.clientSideAuth
);
return;
}
this._requestedGrant = message.requested_grant;
this._securityClasses = message.requested_grant.securityClasses;
this._status = "grant_security_classes";
}
if (message.event === "device registered") {
this._device = message.device;
}
if (message.event === "node added") {
this._status = "interviewing";
this._lowSecurity = message.node.low_security;
}
if (message.event === "interview completed") {
this._unsubscribe();
this._status = "finished";
}
if (message.event === "interview stage completed") {
if (this._stages === undefined) {
this._stages = [message.stage];
} else {
this._stages = [...this._stages, message.stage];
}
}
},
this._inclusionStrategy
);
this._addNodeTimeoutHandle = window.setTimeout(() => {
this._unsubscribe();
}
if (message.event === "interview stage completed") {
if (this._stages === undefined) {
this._stages = [message.stage];
} else {
this._stages = [...this._stages, message.stage];
}
}
this._status = "timed_out";
}, 90000);
}
private _unsubscribe(): void {
@ -315,48 +538,46 @@ class DialogZWaveJSAddNode extends LitElement {
this._subscribed.then((unsub) => unsub());
this._subscribed = undefined;
}
if (this._status === "started") {
this.hass.callWS({
type: "zwave_js/stop_inclusion",
entry_id: this.entry_id,
});
}
if (this._status !== "finished") {
this._status = "";
if (this._entryId) {
stopInclusion(this.hass, this._entryId);
}
this._requestedGrant = undefined;
this._dsk = undefined;
this._securityClasses = [];
this._status = undefined;
if (this._addNodeTimeoutHandle) {
clearTimeout(this._addNodeTimeoutHandle);
}
this._addNodeTimeoutHandle = undefined;
}
public closeDialog(): void {
this._unsubscribe();
this.entry_id = undefined;
this._status = "";
this._nodeAdded = false;
this._inclusionStrategy = undefined;
this._entryId = undefined;
this._status = undefined;
this._device = undefined;
this._stages = undefined;
if (this._stoppedTimeout) {
clearTimeout(this._stoppedTimeout);
this._stoppedTimeout = undefined;
}
this._use_secure_inclusion = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
static get styles(): CSSResultGroup {
return [
haStyleDialog,
haStyle,
css`
.secure_inclusion_field {
margin-top: 48px;
h3 {
margin-top: 0;
}
.success {
color: var(--success-color);
}
.warning {
color: var(--warning-color);
}
.failed {
color: var(--error-color);
}
@ -380,10 +601,22 @@ class DialogZWaveJSAddNode extends LitElement {
align-items: center;
}
.flex-column {
display: flex;
flex-direction: column;
}
.flex-column ha-formfield {
padding: 8px 0;
}
ha-svg-icon {
width: 68px;
height: 48px;
}
.secondary {
color: var(--secondary-text-color);
}
.flex-container ha-circular-progress,
.flex-container ha-svg-icon {

View File

@ -2753,33 +2753,33 @@
},
"common": {
"network": "Network",
"node_id": "Node ID",
"node_id": "Device ID",
"home_id": "Home ID",
"source": "Source",
"close": "Close",
"add_node": "Add Node",
"remove_node": "Remove Node",
"add_node": "Add device",
"remove_node": "Remove device",
"reconfigure_server": "Re-configure Server",
"heal_network": "Heal Network"
},
"dashboard": {
"header": "Manage your Z-Wave Network",
"introduction": "Manage your Z-Wave network and Z-Wave nodes",
"introduction": "Manage your Z-Wave network and Z-Wave devices",
"driver_version": "Driver Version",
"server_version": "Server Version",
"home_id": "Home ID",
"nodes_ready": "Nodes ready",
"nodes_ready": "Devices ready",
"dump_debug": "Download a dump of your network to help diagnose issues",
"dump_dead_nodes_title": "Some of your nodes are dead",
"dump_dead_nodes_text": "Some of your nodes didn't respond and are assumed dead. These will not be fully exported.",
"dump_not_ready_title": "Not all nodes are ready yet",
"dump_not_ready_text": "If you create an export while not all nodes are ready, you could miss needed data. Give your network some time to query all nodes. Do you want to continue with the dump?",
"dump_dead_nodes_title": "Some of your devices are dead",
"dump_dead_nodes_text": "Some of your devices didn't respond and are assumed dead. These will not be fully exported.",
"dump_not_ready_title": "Not all devices are ready yet",
"dump_not_ready_text": "If you create an export while not all devices are ready, you could miss needed data. Give your network some time to query all devices. Do you want to continue with the dump?",
"dump_not_ready_confirm": "Download"
},
"device_info": {
"zwave_info": "Z-Wave Info",
"node_status": "Node Status",
"node_ready": "Node Ready",
"node_status": "Device Status",
"node_ready": "Device Ready",
"device_config": "Configure Device",
"reinterview_device": "Re-interview Device",
"heal_node": "Heal Device",
@ -2787,7 +2787,7 @@
},
"node_config": {
"header": "Z-Wave Device Configuration",
"introduction": "Manage and adjust device (node) specific configuration parameters for the selected device",
"introduction": "Manage and adjust device specific configuration parameters for the selected device",
"attribution": "Device configuration parameters and descriptions are provided by the {device_database}",
"zwave_js_device_database": "Z-Wave JS Device Database",
"battery_device_notice": "Battery devices must be awake to update their config. Please refer to your device manual for instructions on how to wake the device.",
@ -2811,37 +2811,50 @@
"unknown": "Unknown"
},
"add_node": {
"title": "Add a Z-Wave Node",
"introduction": "This wizard will guide you through adding a node to your Z-Wave network.",
"use_secure_inclusion": "Use secure inclusion",
"secure_inclusion_warning": "Secure devices require additional bandwidth; too many secure devices can slow down your Z-Wave network. We recommend only using secure inclusion for devices that require it, like locks or garage door openers.",
"start_inclusion": "Start Inclusion",
"start_secure_inclusion": "Start Secure Inclusion",
"title": "Add a Z-Wave Device",
"cancel_inclusion": "Cancel Inclusion",
"controller_in_inclusion_mode": "Your Z-Wave controller is now in inclusion mode.",
"follow_device_instructions": "Follow the directions that came with your device to trigger pairing on the device.",
"inclusion_failed": "The node could not be added. Please check the logs for more information.",
"inclusion_finished": "The node has been added.",
"inclusion_failed": "The device could not be added. Please check the logs for more information.",
"inclusion_finished": "The device has been added.",
"view_device": "View Device",
"interview_started": "The device is being interviewed. This may take some time.",
"interview_failed": "The device interview failed. Additional information may be available in the logs."
"interview_failed": "The device interview failed. Additional information may be available in the logs.",
"security_classes": {
"S2_Unauthenticated": {
"title": "S2 Unauthenticated",
"description": "Like S2 Authenticated, but without verification that the correct device is included"
},
"S2_Authenticated": {
"title": "S2 Authenticated",
"description": "Example: Lighting, Sensors and Security Systems"
},
"S2_AccessControl": {
"title": "S2 Access Control",
"description": "Example: Door Locks and Garage Doors"
},
"S0_Legacy": {
"title": "S0 Legacy",
"description": "Example: Legacy Door Locks without S2 support"
}
}
},
"remove_node": {
"title": "Remove a Z-Wave Node",
"introduction": "Remove a node from your Z-Wave network, and remove the associated device and entities from Home Assistant.",
"title": "Remove a Z-Wave device",
"introduction": "Remove a device from your Z-Wave network, and remove the associated device and entities from Home Assistant.",
"start_exclusion": "Start Exclusion",
"cancel_exclusion": "Cancel Exclusion",
"controller_in_exclusion_mode": "Your Z-Wave controller is now in exclusion mode.",
"follow_device_instructions": "Follow the directions that came with your device to trigger exclusion on the device.",
"exclusion_failed": "The node could not be removed. Please check the logs for more information.",
"exclusion_finished": "Node {id} has been removed from your Z-Wave network."
"exclusion_failed": "The device could not be removed. Please check the logs for more information.",
"exclusion_finished": "Device {id} has been removed from your Z-Wave network."
},
"remove_failed_node": {
"title": "Remove a Failed Z-Wave Device",
"introduction": "Remove a failed device from your Z-Wave network. Use this if you are unable to exclude a device normally because it is broken.",
"remove_device": "Remove Device",
"in_progress": "The device removal is in progress.",
"removal_finished": "Node {id} has been removed from your Z-Wave network.",
"removal_finished": "Device {id} has been removed from your Z-Wave network.",
"removal_failed": "The device could not be removed from your Z-Wave network."
},
"reinterview_node": {