diff --git a/src/data/device_registry.ts b/src/data/device_registry.ts index 7d52f8dea9..8fc1807586 100644 --- a/src/data/device_registry.ts +++ b/src/data/device_registry.ts @@ -71,7 +71,7 @@ export const updateDeviceRegistryEntry = ( ...updates, }); -const fetchDeviceRegistry = (conn) => +export const fetchDeviceRegistry = (conn) => conn.sendMessagePromise({ type: "config/device_registry/list", }); diff --git a/src/data/ozw.ts b/src/data/ozw.ts index 0e5d73565a..b8af5798f1 100644 --- a/src/data/ozw.ts +++ b/src/data/ozw.ts @@ -73,6 +73,14 @@ export interface OZWDeviceConfig { help: string; } +export interface OZWMigrationData { + migration_device_map: Record; + zwave_entity_ids: string[]; + ozw_entity_ids: string[]; + migration_entity_map: Record; + migrated: boolean; +} + export const nodeQueryStages = [ "ProtocolInfo", "Probe", @@ -147,7 +155,7 @@ export const fetchOZWNetworkStatus = ( ): Promise => hass.callWS({ type: "ozw/network_status", - ozw_instance: ozw_instance, + ozw_instance, }); export const fetchOZWNetworkStatistics = ( @@ -156,7 +164,7 @@ export const fetchOZWNetworkStatistics = ( ): Promise => hass.callWS({ type: "ozw/network_statistics", - ozw_instance: ozw_instance, + ozw_instance, }); export const fetchOZWNodes = ( @@ -165,7 +173,7 @@ export const fetchOZWNodes = ( ): Promise => hass.callWS({ type: "ozw/get_nodes", - ozw_instance: ozw_instance, + ozw_instance, }); export const fetchOZWNodeStatus = ( @@ -175,8 +183,8 @@ export const fetchOZWNodeStatus = ( ): Promise => hass.callWS({ type: "ozw/node_status", - ozw_instance: ozw_instance, - node_id: node_id, + ozw_instance, + node_id, }); export const fetchOZWNodeMetadata = ( @@ -186,8 +194,8 @@ export const fetchOZWNodeMetadata = ( ): Promise => hass.callWS({ type: "ozw/node_metadata", - ozw_instance: ozw_instance, - node_id: node_id, + ozw_instance, + node_id, }); export const fetchOZWNodeConfig = ( @@ -197,8 +205,8 @@ export const fetchOZWNodeConfig = ( ): Promise => hass.callWS({ type: "ozw/get_config_parameters", - ozw_instance: ozw_instance, - node_id: node_id, + ozw_instance, + node_id, }); export const refreshNodeInfo = ( @@ -208,6 +216,15 @@ export const refreshNodeInfo = ( ): Promise => hass.callWS({ type: "ozw/refresh_node_info", - ozw_instance: ozw_instance, - node_id: node_id, + ozw_instance, + node_id, + }); + +export const migrateZwave = ( + hass: HomeAssistant, + dry_run = true +): Promise => + hass.callWS({ + type: "ozw/migrate_zwave", + dry_run, }); diff --git a/src/data/zwave.ts b/src/data/zwave.ts index 77740c8051..34c336322e 100644 --- a/src/data/zwave.ts +++ b/src/data/zwave.ts @@ -42,6 +42,11 @@ export interface ZWaveAttributes { wake_up_interval?: number; } +export interface ZWaveMigrationConfig { + usb_path: string; + network_key: string; +} + export const ZWAVE_NETWORK_STATE_STOPPED = 0; export const ZWAVE_NETWORK_STATE_FAILED = 1; export const ZWAVE_NETWORK_STATE_STARTED = 5; @@ -55,6 +60,20 @@ export const fetchNetworkStatus = ( type: "zwave/network_status", }); +export const startOzwConfigFlow = ( + hass: HomeAssistant +): Promise<{ flow_id: string }> => + hass.callWS({ + type: "zwave/start_ozw_config_flow", + }); + +export const fetchMigrationConfig = ( + hass: HomeAssistant +): Promise => + hass.callWS({ + type: "zwave/get_migration_config", + }); + export const fetchValues = (hass: HomeAssistant, nodeId: number) => hass.callApi("GET", `zwave/values/${nodeId}`); diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 9fc0b65543..bc101ca782 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -293,9 +293,9 @@ class HaPanelConfig extends HassRouterPage { ), }, zwave: { - tag: "ha-config-zwave", + tag: "zwave-config-router", load: () => - import("./integrations/integration-panels/zwave/ha-config-zwave"), + import("./integrations/integration-panels/zwave/zwave-config-router"), }, mqtt: { tag: "mqtt-config-panel", diff --git a/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js b/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js index 48f07b0989..3835c47499 100644 --- a/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js +++ b/src/panels/config/integrations/integration-panels/zwave/ha-config-zwave.js @@ -102,6 +102,36 @@ class HaConfigZwave extends LocalizeMixin(EventsMixin(PolymerElement)) { + + +
+

+ If you are experiencing problems with your Z-Wave devices, you + can migrate to the newer OZW integration, that is currently in + beta. +

+

+ Be aware that the future of OZW is not guaranteed, as the + development has stopped. +

+

+ If you are currently not experiencing issues with your Z-Wave + devices, we recommend you to wait for the successor of the OZW + integration, Z-Wave JS, that is in active development at the + moment. +

+
+ +
+
+ + import(/* webpackChunkName: "ha-config-zwave" */ "./ha-config-zwave"), + }, + migration: { + tag: "zwave-migration", + load: () => + import(/* webpackChunkName: "zwave-migration" */ "./zwave-migration"), + }, + }, + }; + + protected updatePageEl(el): void { + el.route = this.routeTail; + el.hass = this.hass; + el.isWide = this.isWide; + el.narrow = this.narrow; + el.configEntryId = this._configEntry; + + const searchParams = new URLSearchParams(window.location.search); + if (this._configEntry && !searchParams.has("config_entry")) { + searchParams.append("config_entry", this._configEntry); + navigate( + this, + `${this.routeTail.prefix}${ + this.routeTail.path + }?${searchParams.toString()}`, + true + ); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "zwave-config-router": ZWaveConfigRouter; + } +} diff --git a/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts b/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts new file mode 100644 index 0000000000..0dc8d750a4 --- /dev/null +++ b/src/panels/config/integrations/integration-panels/zwave/zwave-migration.ts @@ -0,0 +1,480 @@ +import "@polymer/app-layout/app-header/app-header"; +import "@polymer/app-layout/app-toolbar/app-toolbar"; +import "@material/mwc-button/mwc-button"; +import "../../../../../components/ha-icon-button"; +import "../../../../../components/ha-circular-progress"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { + css, + CSSResult, + customElement, + html, + LitElement, + property, + internalProperty, + TemplateResult, +} from "lit-element"; +import "../../../../../components/buttons/ha-call-api-button"; +import "../../../../../components/buttons/ha-call-service-button"; +import "../../../../../components/ha-card"; +import "../../../../../components/ha-icon"; +import { + fetchNetworkStatus, + ZWaveNetworkStatus, + ZWAVE_NETWORK_STATE_STOPPED, + fetchMigrationConfig, + ZWaveMigrationConfig, + startOzwConfigFlow, +} from "../../../../../data/zwave"; +import { haStyle } from "../../../../../resources/styles"; +import type { HomeAssistant, Route } from "../../../../../types"; +import "../../../ha-config-section"; +import "../../../../../layouts/hass-subpage"; +import { showConfigFlowDialog } from "../../../../../dialogs/config-flow/show-dialog-config-flow"; +import { migrateZwave, OZWMigrationData } from "../../../../../data/ozw"; +import { navigate } from "../../../../../common/navigate"; +import { showAlertDialog } from "../../../../../dialogs/generic/show-dialog-box"; +import { computeStateName } from "../../../../../common/entity/compute_state_name"; +import { + computeDeviceName, + DeviceRegistryEntry, + fetchDeviceRegistry, +} from "../../../../../data/device_registry"; + +@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; + + @internalProperty() private _networkStatus?: ZWaveNetworkStatus; + + @internalProperty() private _unsub?: Promise; + + @internalProperty() private _step = 0; + + @internalProperty() private _stoppingNetwork = false; + + @internalProperty() private _migrationConfig?: ZWaveMigrationConfig; + + @internalProperty() private _migrationData?: OZWMigrationData; + + @internalProperty() private _migratedZwaveEntities?: string[]; + + @internalProperty() private _deviceRegistry?: DeviceRegistryEntry[]; + + public disconnectedCallback(): void { + this._unsubscribe(); + } + + protected render(): TemplateResult { + return html` + + +
+ ${this.hass.localize("ui.panel.config.zwave.migration.ozw.header")} +
+ +
+ ${this.hass.localize( + "ui.panel.config.zwave.migration.ozw.introduction" + )} +
+ ${!this.hass.config.components.includes("mqtt") + ? html` + +
+

+ OpenZWave requires MQTT. Please setup an MQTT broker and + the MQTT integration to proceed with the migration. +

+
+
+ ` + : html` + ${this._step === 0 + ? html` + +
+

+ This wizard will walk through the following steps to + migrate from the legacy Z-Wave integration to + OpenZWave. +

+
    +
  1. Stop the Z-Wave network
  2. +
  3. + If running Home Assistant Core in Docker or in + Python venv: + Configure and start OZWDaemon +
  4. +
  5. Set up the OpenZWave integration
  6. +
  7. + Migrate entities and devices to the new + integration +
  8. +
  9. Remove legacy Z-Wave integration
  10. +
+

+ + Please take a backup or a snapshot of your + environment before proceeding. + +

+
+
+ + Continue + +
+
+ ` + : ``} + ${this._step === 1 + ? html` + +
+

+ 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. +

+ ${this._stoppingNetwork + ? html` +
+ +

Stopping Z-Wave Network...

+
+ ` + : ``} +
+
+ + Stop Network + +
+
+ ` + : ``} + ${this._step === 2 + ? html` + +
+

+ Now it's time to set up the OZW integration. +

+ ${this.hass.config.components.includes("hassio") + ? html` +

+ The OZWDaemon runs in a Home Assistant addon + that will be setup next. Make sure to check + the checkbox for the addon. +

+ ` + : html` +

+ If you're using Home Assistant Core in Docker + or a Python venv, see the + + OZWDaemon readme + + for setup instructions. +

+

+ Here's the current Z-Wave configuration. + You'll need these values when setting up OZW + daemon. +

+ ${this._migrationConfig + ? html`
+ USB Path: + ${this._migrationConfig.usb_path}
+ Network Key: + ${this._migrationConfig.network_key} +
` + : ``} +

+ Once OZWDaemon is installed, running, and + connected to the MQTT broker click Continue to + set up the OpenZWave integration and migrate + your devices and entities. +

+ `} +
+
+ + Continue + +
+
+ ` + : ``} + ${this._step === 3 + ? html` + +
+

+ Now it's time to migrate your devices and entities + from the legacy Z-Wave integration to the OZW + integration, to make sure all your UI and + automations keep working. +

+ ${this._migrationData + ? html` +

Below is a list of what will be migrated.

+ ${this._migratedZwaveEntities!.length !== + this._migrationData.zwave_entity_ids.length + ? html`

+ Not all entities can be migrated! The + following entities will not be migrated + and might need manual adjustments to + your config: +

+
    + ${this._migrationData.zwave_entity_ids.map( + (entity_id) => + !this._migratedZwaveEntities!.includes( + entity_id + ) + ? html`
  • + ${computeStateName( + this.hass.states[entity_id] + )} + (${entity_id}) +
  • ` + : "" + )} +
` + : ""} + ${Object.keys( + this._migrationData.migration_device_map + ).length + ? html`

Devices that will be migrated:

+
    + ${Object.keys( + this._migrationData + .migration_device_map + ).map( + (device_id) => + html`
  • + ${this._computeDeviceName( + device_id + )} +
  • ` + )} +
` + : ""} + ${Object.keys( + this._migrationData.migration_entity_map + ).length + ? html`

+ Entities that will be migrated: +

+
    + ${Object.keys( + this._migrationData + .migration_entity_map + ).map( + (entity_id) => html`
  • + ${computeStateName( + this.hass.states[entity_id] + )} + (${entity_id}) +
  • ` + )} +
` + : ""} + ` + : html`
+

Loading migration data...

+ + +
`} +
+
+ + Migrate + +
+
+ ` + : ``} + ${this._step === 4 + ? html` +
+ That was all! You are now migrated to the new OZW + 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. +
+
+ + Go to OZW config panel + +
+
` + : ""} + `} +
+
+ `; + } + + private async _getMigrationConfig(): Promise { + this._migrationConfig = await fetchMigrationConfig(this.hass!); + } + + private async _unsubscribe(): Promise { + if (this._unsub) { + (await this._unsub)(); + this._unsub = undefined; + } + } + + private _continue(): void { + this._step++; + } + + private async _stopNetwork(): Promise { + 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 _setupOzw() { + const ozwConfigFlow = await startOzwConfigFlow(this.hass); + if ( + !this.hass.config.components.includes("hassio") && + this.hass.config.components.includes("ozw") + ) { + this._getMigrationData(); + this._step = 3; + return; + } + showConfigFlowDialog(this, { + continueFlowId: ozwConfigFlow.flow_id, + dialogClosedCallback: () => { + if (this.hass.config.components.includes("ozw")) { + this._getMigrationData(); + this._step = 3; + } + }, + showAdvanced: this.hass.userData?.showAdvanced, + }); + this.hass.loadBackendTranslation("title", "ozw", true); + } + + private async _getMigrationData() { + this._migrationData = await migrateZwave(this.hass, true); + this._migratedZwaveEntities = Object.keys( + this._migrationData.migration_entity_map + ); + if (Object.keys(this._migrationData.migration_device_map).length) { + this._deviceRegistry = await fetchDeviceRegistry(this.hass); + } + } + + private _computeDeviceName(deviceId) { + const device = this._deviceRegistry?.find( + (devReg) => devReg.id === deviceId + ); + if (!device) { + return deviceId; + } + return computeDeviceName(device, this.hass); + } + + private async _doMigrate() { + const data = await migrateZwave(this.hass, false); + if (!data.migrated) { + showAlertDialog(this, { title: "Migration failed!" }); + return; + } + this._step = 4; + } + + private _navigateOzw() { + navigate(this, "/config/ozw"); + } + + private _networkStopped(): void { + this._unsubscribe(); + this._getMigrationConfig(); + this._stoppingNetwork = false; + this._step = 2; + } + + private async _getNetworkStatus(): Promise { + this._networkStatus = await fetchNetworkStatus(this.hass!); + } + + static get styles(): CSSResult[] { + 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; + } +} diff --git a/src/translations/en.json b/src/translations/en.json index 3255368b8c..50429653ce 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2335,6 +2335,12 @@ "unknown": "unknown", "wakeup_interval": "Wakeup Interval" }, + "migration": { + "ozw": { + "header": "Migrate to OpenZWave", + "introduction": "This wizard will help you migrate from the legacy Z-Wave integration to the OpenZWave integration that is currently in beta." + } + }, "network_management": { "header": "Z-Wave Network Management", "introduction": "Run commands that affect the Z-Wave network. You won't get feedback on whether most commands succeeded, but you can check the OZW Log to try to find out."