574 lines
21 KiB
TypeScript
574 lines
21 KiB
TypeScript
import "@material/mwc-button/mwc-button";
|
|
import "@polymer/app-layout/app-header/app-header";
|
|
import "@polymer/app-layout/app-toolbar/app-toolbar";
|
|
import { UnsubscribeFunc } from "home-assistant-js-websocket";
|
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
import { customElement, property, state } from "lit/decorators";
|
|
import { isComponentLoaded } from "../../../../../common/config/is_component_loaded";
|
|
import { computeStateDomain } from "../../../../../common/entity/compute_state_domain";
|
|
import { computeStateName } from "../../../../../common/entity/compute_state_name";
|
|
import "../../../../../components/buttons/ha-call-api-button";
|
|
import "../../../../../components/buttons/ha-call-service-button";
|
|
import "../../../../../components/ha-alert";
|
|
import "../../../../../components/ha-card";
|
|
import "../../../../../components/ha-circular-progress";
|
|
import "../../../../../components/ha-icon";
|
|
import "../../../../../components/ha-icon-button";
|
|
import {
|
|
computeDeviceName,
|
|
DeviceRegistryEntry,
|
|
fetchDeviceRegistry,
|
|
subscribeDeviceRegistry,
|
|
} from "../../../../../data/device_registry";
|
|
import {
|
|
fetchMigrationConfig,
|
|
fetchNetworkStatus,
|
|
startZwaveJsConfigFlow,
|
|
ZWaveMigrationConfig,
|
|
ZWaveNetworkStatus,
|
|
ZWAVE_NETWORK_STATE_STOPPED,
|
|
} from "../../../../../data/zwave";
|
|
import {
|
|
fetchZwaveNetworkStatus as fetchZwaveJsNetworkStatus,
|
|
fetchZwaveNodeStatus,
|
|
getZwaveJsIdentifiersFromDevice,
|
|
migrateZwave,
|
|
subscribeZwaveNodeReady,
|
|
ZWaveJsMigrationData,
|
|
} from "../../../../../data/zwave_js";
|
|
import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow";
|
|
import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box";
|
|
import "../../../../../layouts/hass-subpage";
|
|
import { haStyle } from "../../../../../resources/styles";
|
|
import type { HomeAssistant, Route } from "../../../../../types";
|
|
import "../../../ha-config-section";
|
|
|
|
@customElement("zwave-migration")
|
|
export class ZwaveMigration extends LitElement {
|
|
@property({ type: Object }) public hass!: HomeAssistant;
|
|
|
|
@property({ type: Object }) public route!: Route;
|
|
|
|
@property({ type: Boolean }) public narrow!: boolean;
|
|
|
|
@property({ type: Boolean }) public isWide!: boolean;
|
|
|
|
@state() private _networkStatus?: ZWaveNetworkStatus;
|
|
|
|
@state() private _step = 0;
|
|
|
|
@state() private _stoppingNetwork = false;
|
|
|
|
@state() private _migrationConfig?: ZWaveMigrationConfig;
|
|
|
|
@state() private _migrationData?: ZWaveJsMigrationData;
|
|
|
|
@state() private _migratedZwaveEntities?: string[];
|
|
|
|
@state() private _deviceNameLookup: { [id: string]: string } = {};
|
|
|
|
@state() private _waitingOnDevices?: DeviceRegistryEntry[];
|
|
|
|
private _zwaveJsEntryId?: string;
|
|
|
|
private _nodeReadySubscriptions?: Promise<UnsubscribeFunc>[];
|
|
|
|
private _unsub?: Promise<UnsubscribeFunc>;
|
|
|
|
private _unsubDevices?: UnsubscribeFunc;
|
|
|
|
public disconnectedCallback(): void {
|
|
this._unsubscribe();
|
|
if (this._unsubDevices) {
|
|
this._unsubDevices();
|
|
this._unsubDevices = undefined;
|
|
}
|
|
}
|
|
|
|
protected render(): TemplateResult {
|
|
return html`
|
|
<hass-subpage
|
|
.hass=${this.hass}
|
|
.narrow=${this.narrow}
|
|
.route=${this.route}
|
|
back-path="/config/zwave"
|
|
>
|
|
<ha-config-section .narrow=${this.narrow} .isWide=${this.isWide}>
|
|
<div slot="header">
|
|
${this.hass.localize(
|
|
"ui.panel.config.zwave.migration.zwave_js.header"
|
|
)}
|
|
</div>
|
|
|
|
<div slot="introduction">
|
|
${this.hass.localize(
|
|
"ui.panel.config.zwave.migration.zwave_js.introduction"
|
|
)}
|
|
</div>
|
|
${html`
|
|
${this._step === 0
|
|
? html`
|
|
<ha-card class="content" header="Introduction">
|
|
<div class="card-content">
|
|
<p>
|
|
This wizard will walk through the following steps to
|
|
migrate from the legacy Z-Wave integration to Z-Wave JS.
|
|
</p>
|
|
<ol>
|
|
<li>Stop the Z-Wave network</li>
|
|
${!isComponentLoaded(this.hass, "hassio")
|
|
? html`<li>Configure and start Z-Wave JS</li>`
|
|
: ""}
|
|
<li>Set up the Z-Wave JS integration</li>
|
|
<li>
|
|
Migrate entities and devices to the new integration
|
|
</li>
|
|
<li>Remove legacy Z-Wave integration</li>
|
|
</ol>
|
|
<p>
|
|
<b>
|
|
${isComponentLoaded(this.hass, "hassio")
|
|
? html`Please
|
|
<a href="/hassio/backups">make a backup</a>
|
|
before proceeding.`
|
|
: "Please make a backup of your installation before proceeding."}
|
|
</b>
|
|
</p>
|
|
</div>
|
|
<div class="card-actions">
|
|
<mwc-button @click=${this._continue}>
|
|
Continue
|
|
</mwc-button>
|
|
</div>
|
|
</ha-card>
|
|
`
|
|
: this._step === 1
|
|
? html`
|
|
<ha-card class="content" header="Stop Z-Wave Network">
|
|
<div class="card-content">
|
|
<p>
|
|
We need to stop the Z-Wave network to perform the
|
|
migration. Home Assistant will not be able to control
|
|
Z-Wave devices while the network is stopped.
|
|
</p>
|
|
${Object.values(this.hass.states)
|
|
.filter(
|
|
(entityState) =>
|
|
computeStateDomain(entityState) === "zwave" &&
|
|
!["ready", "sleeping"].includes(entityState.state)
|
|
)
|
|
.map(
|
|
(entityState) =>
|
|
html`<ha-alert alert-type="warning">
|
|
Device ${computeStateName(entityState)}
|
|
(${entityState.entity_id}) is not ready yet! For
|
|
the best result, wake the device up if it is
|
|
battery powered and wait for this device to become
|
|
ready.
|
|
</ha-alert>`
|
|
)}
|
|
${this._stoppingNetwork
|
|
? html`
|
|
<div class="flex-container">
|
|
<ha-circular-progress
|
|
active
|
|
></ha-circular-progress>
|
|
<div><p>Stopping Z-Wave Network...</p></div>
|
|
</div>
|
|
`
|
|
: ``}
|
|
</div>
|
|
<div class="card-actions">
|
|
<mwc-button @click=${this._stopNetwork}>
|
|
Stop Network
|
|
</mwc-button>
|
|
</div>
|
|
</ha-card>
|
|
`
|
|
: this._step === 2
|
|
? html`
|
|
<ha-card class="content" header="Set up Z-Wave JS">
|
|
<div class="card-content">
|
|
<p>Now it's time to set up the Z-Wave JS integration.</p>
|
|
${isComponentLoaded(this.hass, "hassio")
|
|
? html`
|
|
<p>
|
|
Z-Wave JS runs as a Home Assistant add-on that
|
|
will be setup next. Make sure to check the
|
|
checkbox to use the add-on.
|
|
</p>
|
|
`
|
|
: html`
|
|
<p>
|
|
You are not running Home Assistant OS (the default
|
|
installation type) or Home Assistant Supervised,
|
|
so we can not setup Z-Wave JS automaticaly. Follow
|
|
the
|
|
<a
|
|
href="https://www.home-assistant.io/integrations/zwave_js/#advanced-installation-instructions"
|
|
target="_blank"
|
|
rel="noreferrer"
|
|
>advanced installation instructions</a
|
|
>
|
|
to install Z-Wave JS.
|
|
</p>
|
|
<p>
|
|
Here's the current Z-Wave configuration. You'll
|
|
need these values when setting up Z-Wave JS.
|
|
</p>
|
|
${this._migrationConfig
|
|
? html`<blockquote>
|
|
USB Path: ${this._migrationConfig.usb_path}<br />
|
|
Network Key:
|
|
${this._migrationConfig.network_key}
|
|
</blockquote>`
|
|
: ``}
|
|
<p>
|
|
Once Z-Wave JS is installed and running, click
|
|
'Continue' to set up the Z-Wave JS integration and
|
|
migrate your devices and entities.
|
|
</p>
|
|
`}
|
|
</div>
|
|
<div class="card-actions">
|
|
<mwc-button @click=${this._setupZwaveJs}>
|
|
Continue
|
|
</mwc-button>
|
|
</div>
|
|
</ha-card>
|
|
`
|
|
: this._step === 3
|
|
? html`
|
|
<ha-card
|
|
class="content"
|
|
header="Migrate devices and entities"
|
|
>
|
|
<div class="card-content">
|
|
<p>
|
|
Now it's time to migrate your devices and entities from
|
|
the legacy Z-Wave integration to the Z-Wave JS
|
|
integration, to make sure all your UI's and automations
|
|
keep working.
|
|
</p>
|
|
${this._waitingOnDevices?.map(
|
|
(device) =>
|
|
html`<ha-alert alert-type="warning">
|
|
Device ${computeDeviceName(device, this.hass)} is
|
|
not ready yet! For the best result, wake the device
|
|
up if it is battery powered and wait for this device
|
|
to become ready.
|
|
</ha-alert>`
|
|
)}
|
|
${this._migrationData
|
|
? html`
|
|
<p>Below is a list of what will be migrated.</p>
|
|
${this._migratedZwaveEntities!.length !==
|
|
this._migrationData.zwave_entity_ids.length
|
|
? html`<ha-alert
|
|
alert-type="warning"
|
|
title="Not all entities can be migrated!"
|
|
>
|
|
The following entities will not be migrated
|
|
and might need manual adjustments to your
|
|
config:
|
|
</ha-alert>
|
|
<ul>
|
|
${this._migrationData.zwave_entity_ids.map(
|
|
(entity_id) =>
|
|
!this._migratedZwaveEntities!.includes(
|
|
entity_id
|
|
)
|
|
? html`<li>
|
|
${entity_id in this.hass.states
|
|
? computeStateName(
|
|
this.hass.states[entity_id]
|
|
)
|
|
: ""}
|
|
(${entity_id})
|
|
</li>`
|
|
: ""
|
|
)}
|
|
</ul>`
|
|
: ""}
|
|
${Object.keys(
|
|
this._migrationData.migration_device_map
|
|
).length
|
|
? html`<h3>Devices that will be migrated:</h3>
|
|
<ul>
|
|
${Object.keys(
|
|
this._migrationData.migration_device_map
|
|
).map(
|
|
(device_id) =>
|
|
html`<li>
|
|
${this._deviceNameLookup[device_id] ||
|
|
device_id}
|
|
</li>`
|
|
)}
|
|
</ul>`
|
|
: ""}
|
|
${Object.keys(
|
|
this._migrationData.migration_entity_map
|
|
).length
|
|
? html`<h3>Entities that will be migrated:</h3>
|
|
<ul>
|
|
${Object.keys(
|
|
this._migrationData.migration_entity_map
|
|
).map(
|
|
(entity_id) => html`<li>
|
|
${entity_id in this.hass.states
|
|
? computeStateName(
|
|
this.hass.states[entity_id]
|
|
)
|
|
: ""}
|
|
(${entity_id})
|
|
</li>`
|
|
)}
|
|
</ul>`
|
|
: ""}
|
|
`
|
|
: html` <div class="flex-container">
|
|
<p>Loading migration data...</p>
|
|
<ha-circular-progress active>
|
|
</ha-circular-progress>
|
|
</div>`}
|
|
</div>
|
|
<div class="card-actions">
|
|
<mwc-button @click=${this._doMigrate}>
|
|
Migrate
|
|
</mwc-button>
|
|
</div>
|
|
</ha-card>
|
|
`
|
|
: this._step === 4
|
|
? html`<ha-card class="content" header="Done!">
|
|
<div class="card-content">
|
|
That was all! You are now migrated to the new Z-Wave JS
|
|
integration, check if all your devices and entities are back
|
|
the way they where, if not all entities could be migrated
|
|
you might have to change those manually.
|
|
<p>
|
|
If you have 'zwave' in your configurtion.yaml file, you
|
|
should remove it now.
|
|
</p>
|
|
</div>
|
|
<div class="card-actions">
|
|
<a
|
|
href=${`/config/zwave_js?config_entry=${this._zwaveJsEntryId}`}
|
|
>
|
|
<mwc-button> Go to Z-Wave JS config panel </mwc-button>
|
|
</a>
|
|
</div>
|
|
</ha-card>`
|
|
: ""}
|
|
`}
|
|
</ha-config-section>
|
|
</hass-subpage>
|
|
`;
|
|
}
|
|
|
|
private async _getMigrationConfig(): Promise<void> {
|
|
this._migrationConfig = await fetchMigrationConfig(this.hass!);
|
|
}
|
|
|
|
private async _unsubscribe(): Promise<void> {
|
|
if (this._unsub) {
|
|
(await this._unsub)();
|
|
this._unsub = undefined;
|
|
}
|
|
}
|
|
|
|
private _continue(): void {
|
|
this._step++;
|
|
}
|
|
|
|
private async _stopNetwork(): Promise<void> {
|
|
this._stoppingNetwork = true;
|
|
await this._getNetworkStatus();
|
|
if (this._networkStatus?.state === ZWAVE_NETWORK_STATE_STOPPED) {
|
|
this._networkStopped();
|
|
return;
|
|
}
|
|
|
|
this._unsub = this.hass!.connection.subscribeEvents(
|
|
() => this._networkStopped(),
|
|
"zwave.network_stop"
|
|
);
|
|
this.hass!.callService("zwave", "stop_network");
|
|
}
|
|
|
|
private async _setupZwaveJs() {
|
|
const zwaveJsConfigFlow = await startZwaveJsConfigFlow(this.hass);
|
|
showConfigFlowDialog(this, {
|
|
continueFlowId: zwaveJsConfigFlow.flow_id,
|
|
dialogClosedCallback: (params) => {
|
|
if (params.entryId) {
|
|
this._zwaveJsEntryId = params.entryId;
|
|
this._getZwaveJSNodesStatus();
|
|
this._step = 3;
|
|
}
|
|
},
|
|
showAdvanced: this.hass.userData?.showAdvanced,
|
|
});
|
|
this.hass.loadBackendTranslation("title", "zwave_js", true);
|
|
}
|
|
|
|
private async _getZwaveJSNodesStatus() {
|
|
if (this._nodeReadySubscriptions?.length) {
|
|
const unsubs = await Promise.all(this._nodeReadySubscriptions);
|
|
unsubs.forEach((unsub) => {
|
|
unsub();
|
|
});
|
|
}
|
|
this._nodeReadySubscriptions = [];
|
|
const networkStatus = await fetchZwaveJsNetworkStatus(
|
|
this.hass,
|
|
this._zwaveJsEntryId!
|
|
);
|
|
const nodeStatePromisses = networkStatus.controller.nodes.map((nodeId) =>
|
|
fetchZwaveNodeStatus(this.hass, this._zwaveJsEntryId!, nodeId)
|
|
);
|
|
const nodesNotReady = (await Promise.all(nodeStatePromisses)).filter(
|
|
(node) => !node.ready
|
|
);
|
|
|
|
// eslint-disable-next-line no-console
|
|
console.log("waiting for nodes to be ready", nodesNotReady);
|
|
|
|
this._getMigrationData();
|
|
if (nodesNotReady.length === 0) {
|
|
this._waitingOnDevices = [];
|
|
return;
|
|
}
|
|
this._nodeReadySubscriptions = nodesNotReady.map((node) =>
|
|
subscribeZwaveNodeReady(
|
|
this.hass,
|
|
this._zwaveJsEntryId!,
|
|
node.node_id,
|
|
() => {
|
|
this._getZwaveJSNodesStatus();
|
|
}
|
|
)
|
|
);
|
|
const deviceReg: DeviceRegistryEntry[] = await fetchDeviceRegistry(
|
|
this.hass.connection
|
|
);
|
|
this._waitingOnDevices = deviceReg.filter((device) => {
|
|
const identifiers = getZwaveJsIdentifiersFromDevice(device);
|
|
if (
|
|
!identifiers ||
|
|
Number(identifiers.home_id) !== networkStatus.controller.home_id
|
|
) {
|
|
return false;
|
|
}
|
|
return nodesNotReady.some((node) => identifiers.node_id === node.node_id);
|
|
});
|
|
}
|
|
|
|
private async _getMigrationData() {
|
|
try {
|
|
this._migrationData = await migrateZwave(
|
|
this.hass,
|
|
this._zwaveJsEntryId!,
|
|
true
|
|
);
|
|
} catch (err: any) {
|
|
showAlertDialog(this, {
|
|
title: "Failed to get migration data!",
|
|
text:
|
|
err.code === "unknown_command"
|
|
? "Restart Home Assistant and try again."
|
|
: err.message,
|
|
});
|
|
return;
|
|
}
|
|
this._migratedZwaveEntities = Object.keys(
|
|
this._migrationData.migration_entity_map
|
|
);
|
|
if (Object.keys(this._migrationData.migration_device_map).length) {
|
|
this._fetchDevices();
|
|
}
|
|
}
|
|
|
|
private _fetchDevices() {
|
|
this._unsubDevices = subscribeDeviceRegistry(
|
|
this.hass.connection,
|
|
(devices) => {
|
|
if (!this._migrationData) {
|
|
return;
|
|
}
|
|
const migrationDevices = Object.keys(
|
|
this._migrationData.migration_device_map
|
|
);
|
|
const deviceNameLookup = {};
|
|
devices.forEach((device) => {
|
|
if (migrationDevices.includes(device.id)) {
|
|
deviceNameLookup[device.id] = computeDeviceName(device, this.hass);
|
|
}
|
|
});
|
|
this._deviceNameLookup = deviceNameLookup;
|
|
}
|
|
);
|
|
}
|
|
|
|
private async _doMigrate() {
|
|
const data = await migrateZwave(this.hass, this._zwaveJsEntryId!, false);
|
|
if (!data.migrated) {
|
|
showAlertDialog(this, { title: "Migration failed!" });
|
|
return;
|
|
}
|
|
this._step = 4;
|
|
}
|
|
|
|
private _networkStopped(): void {
|
|
this._unsubscribe();
|
|
this._getMigrationConfig();
|
|
this._stoppingNetwork = false;
|
|
this._step = 2;
|
|
}
|
|
|
|
private async _getNetworkStatus(): Promise<void> {
|
|
this._networkStatus = await fetchNetworkStatus(this.hass!);
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return [
|
|
haStyle,
|
|
css`
|
|
.content {
|
|
margin-top: 24px;
|
|
}
|
|
|
|
.flex-container {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
|
|
.flex-container ha-circular-progress {
|
|
margin-right: 20px;
|
|
}
|
|
|
|
blockquote {
|
|
display: block;
|
|
background-color: var(--secondary-background-color);
|
|
color: var(--primary-text-color);
|
|
padding: 8px;
|
|
margin: 8px 0;
|
|
font-size: 0.9em;
|
|
font-family: monospace;
|
|
}
|
|
|
|
ha-card {
|
|
margin: 0 auto;
|
|
max-width: 600px;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"zwave-migration": ZwaveMigration;
|
|
}
|
|
}
|