home-assistant-frontend/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts

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;
}
}