From 5b504bf9ce5510f5431377ea7ed8244f2a713bbf Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Mon, 6 Feb 2023 18:18:30 +0100 Subject: [PATCH] Add "add matter device" link to add integration dialog (#15365) --- .../integrations/protocolIntegrationPicked.ts | 40 +++++++++ src/data/matter.ts | 49 +++++++++++ .../devices/ha-config-devices-dashboard.ts | 5 ++ .../integrations/dialog-add-integration.ts | 16 ++-- .../integrations/ha-domain-integrations.ts | 76 +++++++++------- .../integrations/ha-integration-card.ts | 5 +- .../matter/dialog-matter-add-device.ts | 87 ++++++++++++++++++ .../matter/matter-config-panel.ts | 88 +++++++------------ .../matter/show-dialog-add-matter-device.ts | 11 +++ src/translations/en.json | 5 +- 10 files changed, 286 insertions(+), 96 deletions(-) create mode 100644 src/panels/config/integrations/integration-panels/matter/dialog-matter-add-device.ts create mode 100644 src/panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device.ts diff --git a/src/common/integrations/protocolIntegrationPicked.ts b/src/common/integrations/protocolIntegrationPicked.ts index a1f11551c4..dfc3a89f7a 100644 --- a/src/common/integrations/protocolIntegrationPicked.ts +++ b/src/common/integrations/protocolIntegrationPicked.ts @@ -4,12 +4,15 @@ import { domainToName } from "../../data/integration"; import { getIntegrationDescriptions } from "../../data/integrations"; import { showConfigFlowDialog } from "../../dialogs/config-flow/show-dialog-config-flow"; import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box"; +import { showMatterAddDeviceDialog } from "../../panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device"; import { showZWaveJSAddNodeDialog } from "../../panels/config/integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import type { HomeAssistant } from "../../types"; import { documentationUrl } from "../../util/documentation-url"; import { isComponentLoaded } from "../config/is_component_loaded"; import { navigate } from "../navigate"; +export const PROTOCOL_INTEGRATIONS = ["zha", "zwave_js", "matter"] as const; + export const protocolIntegrationPicked = async ( element: HTMLElement, hass: HomeAssistant, @@ -113,5 +116,42 @@ export const protocolIntegrationPicked = async ( } navigate("/config/zha/add"); + } else if (domain === "matter") { + const entries = await getConfigEntries(hass, { + domain, + }); + if (!isComponentLoaded(hass, domain) || !entries.length) { + // If the component isn't loaded, ask them to load the integration first + showConfirmationDialog(element, { + title: hass.localize( + "ui.panel.config.integrations.config_flow.missing_zwave_zigbee_title", + { integration: "Matter" } + ), + text: hass.localize( + "ui.panel.config.integrations.config_flow.missing_matter", + { + brand: options?.brand || options?.domain || "Matter", + link: html`${hass.localize( + "ui.panel.config.integrations.config_flow.supported_hardware" + )}`, + } + ), + confirmText: hass.localize( + "ui.panel.config.integrations.config_flow.proceed" + ), + confirm: () => { + showConfigFlowDialog(element, { + startFlowHandler: "matter", + }); + }, + }); + return; + } + showMatterAddDeviceDialog(element); } }; diff --git a/src/data/matter.ts b/src/data/matter.ts index f270ead3ad..11686a7cb2 100644 --- a/src/data/matter.ts +++ b/src/data/matter.ts @@ -1,4 +1,53 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { navigate } from "../common/navigate"; import { HomeAssistant } from "../types"; +import { subscribeDeviceRegistry } from "./device_registry"; + +export const canCommissionMatterExternal = (hass: HomeAssistant) => + hass.auth.external?.config.canCommissionMatter; + +export const startExternalCommissioning = (hass: HomeAssistant) => + hass.auth.external!.fireMessage({ + type: "matter/commission", + }); + +export const redirectOnNewMatterDevice = ( + hass: HomeAssistant, + callback?: () => void +): UnsubscribeFunc => { + let curMatterDevices: Set | undefined; + const unsubDeviceReg = subscribeDeviceRegistry(hass.connection, (entries) => { + if (!curMatterDevices) { + curMatterDevices = new Set( + Object.values(entries) + .filter((device) => + device.identifiers.find((identifier) => identifier[0] === "matter") + ) + .map((device) => device.id) + ); + return; + } + const newMatterDevices = Object.values(entries).filter( + (device) => + device.identifiers.find((identifier) => identifier[0] === "matter") && + !curMatterDevices!.has(device.id) + ); + if (newMatterDevices.length) { + unsubDeviceReg(); + curMatterDevices = undefined; + callback?.(); + navigate(`/config/devices/device/${newMatterDevices[0].id}`); + } + }); + return () => { + unsubDeviceReg(); + curMatterDevices = undefined; + }; +}; + +export const addMatterDevice = (hass: HomeAssistant) => { + startExternalCommissioning(hass); +}; export const commissionMatterDevice = ( hass: HomeAssistant, diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index b336bf6b18..582bd9e3ce 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -39,6 +39,7 @@ import { HomeAssistant, Route } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { configSections } from "../ha-panel-config"; import "../integrations/ha-integration-overflow-menu"; +import { showMatterAddDeviceDialog } from "../integrations/integration-panels/matter/show-dialog-add-matter-device"; import { showZWaveJSAddNodeDialog } from "../integrations/integration-panels/zwave_js/show-dialog-zwave_js-add-node"; import { showAddIntegrationDialog } from "../integrations/show-add-integration-dialog"; @@ -543,6 +544,10 @@ export class HaConfigDeviceDashboard extends LitElement { this._showZJSAddDeviceDialog(filteredConfigEntry); return; } + if (filteredConfigEntry?.domain === "matter") { + showMatterAddDeviceDialog(this); + return; + } showAddIntegrationDialog(this); } diff --git a/src/panels/config/integrations/dialog-add-integration.ts b/src/panels/config/integrations/dialog-add-integration.ts index 15b52126d4..00f5b8af3d 100644 --- a/src/panels/config/integrations/dialog-add-integration.ts +++ b/src/panels/config/integrations/dialog-add-integration.ts @@ -7,7 +7,10 @@ import { styleMap } from "lit/directives/style-map"; import memoizeOne from "memoize-one"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; -import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { + protocolIntegrationPicked, + PROTOCOL_INTEGRATIONS, +} from "../../../common/integrations/protocolIntegrationPicked"; import { navigate } from "../../../common/navigate"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; import { LocalizeFunc } from "../../../common/translations/localize"; @@ -136,10 +139,9 @@ class AddIntegrationDialog extends LitElement { localize: LocalizeFunc, filter?: string ): IntegrationListItem[] => { - const addDeviceRows: IntegrationListItem[] = ( - ["zha", "zwave_js"] as const + const addDeviceRows: IntegrationListItem[] = PROTOCOL_INTEGRATIONS.filter( + (domain) => components.includes(domain) ) - .filter((domain) => components.includes(domain)) .map((domain) => ({ name: localize(`ui.panel.config.integrations.add_${domain}_device`), domain, @@ -371,7 +373,7 @@ class AddIntegrationDialog extends LitElement { ), confirm: () => { this.closeDialog(); - if (["zha", "zwave_js"].includes(integration.supported_by)) { + if (PROTOCOL_INTEGRATIONS.includes(integration.supported_by)) { protocolIntegrationPicked(this, this.hass, integration.supported_by); return; } @@ -519,7 +521,9 @@ class AddIntegrationDialog extends LitElement { } if ( - ["zha", "zwave_js"].includes(integration.domain) && + (PROTOCOL_INTEGRATIONS as ReadonlyArray).includes( + integration.domain + ) && isComponentLoaded(this.hass, integration.domain) ) { this._pickedBrand = integration.domain; diff --git a/src/panels/config/integrations/ha-domain-integrations.ts b/src/panels/config/integrations/ha-domain-integrations.ts index da4c1f7aed..0a96aa0856 100644 --- a/src/panels/config/integrations/ha-domain-integrations.ts +++ b/src/panels/config/integrations/ha-domain-integrations.ts @@ -3,7 +3,10 @@ import { css, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { fireEvent } from "../../../common/dom/fire_event"; -import { protocolIntegrationPicked } from "../../../common/integrations/protocolIntegrationPicked"; +import { + protocolIntegrationPicked, + PROTOCOL_INTEGRATIONS, +} from "../../../common/integrations/protocolIntegrationPicked"; import { shouldHandleRequestSelectedEvent } from "../../../common/mwc/handle-request-selected-event"; import { navigate } from "../../../common/navigate"; import { caseInsensitiveStringCompare } from "../../../common/string/compare"; @@ -77,38 +80,41 @@ class HaDomainIntegrations extends LitElement { : ""}` : ""} ${this.integration?.iot_standards - ? ( - this.integration.iot_standards.filter( - (standard) => standard in standardToDomain - ) as (keyof typeof standardToDomain)[] - ).map((standard) => { - const domain = standardToDomain[standard]; - return html` - - ${this.hass.localize( - `ui.panel.config.integrations.add_${domain}_device` - )} + (PROTOCOL_INTEGRATIONS as ReadonlyArray).includes( + standardToDomain[standard] || standard + ) + ) + .map((standard) => { + const domain: (typeof PROTOCOL_INTEGRATIONS)[number] = + standardToDomain[standard] || standard; + return html` - - `; - }) + + ${this.hass.localize( + `ui.panel.config.integrations.add_${domain}_device` + )} + + `; + }) : ""} ${this.integration && "integrations" in this.integration && @@ -144,7 +150,7 @@ class HaDomainIntegrations extends LitElement { ` ) : ""} - ${this.domain === "zha" || this.domain === "zwave_js" + ${(PROTOCOL_INTEGRATIONS as ReadonlyArray).includes(this.domain) ? html` ${this.hass.localize( - `ui.panel.config.integrations.add_${this.domain}_device` + `ui.panel.config.integrations.add_${ + this.domain as (typeof PROTOCOL_INTEGRATIONS)[number] + }_device` )} diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index cedf0c214a..a369f82157 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -73,6 +73,7 @@ import { documentationUrl } from "../../../util/documentation-url"; import { fileDownload } from "../../../util/file_download"; import type { ConfigEntryExtended } from "./ha-config-integrations"; import "./ha-integration-header"; +import { isDevVersion } from "../../../common/config/version"; const integrationsWithPanel = { matter: "/config/matter", @@ -346,7 +347,9 @@ export class HaIntegrationCard extends LitElement { ? html` ${this.hass.localize("ui.common.enable")} ` - : item.domain in integrationsWithPanel + : item.domain in integrationsWithPanel && + (item.domain !== "matter" || + isDevVersion(this.hass.config.version)) ? html` + this.closeDialog() + ); + addMatterDevice(this.hass); + } + + public closeDialog(): void { + this._open = false; + this._unsub?.(); + this._unsub = undefined; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + protected render(): TemplateResult { + if (!this._open) { + return html``; + } + + return html` + +
+ ${!canCommissionMatterExternal(this.hass) + ? this.hass.localize( + "ui.panel.config.integrations.config_flow.matter_mobile_app" + ) + : html``} +
+ + ${this.hass.localize("ui.common.cancel")} + +
+ `; + } + + static styles = [ + haStyleDialog, + css` + div { + display: grid; + } + ha-circular-progress { + justify-self: center; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "dialog-matter-add-device": DialogMatterAddDevice; + } +} diff --git a/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts b/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts index 1f99075dc3..20fcc722f9 100644 --- a/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts +++ b/src/panels/config/integrations/integration-panels/matter/matter-config-panel.ts @@ -1,21 +1,23 @@ import "@material/mwc-button"; -import { css, html, LitElement, PropertyValues, TemplateResult } from "lit"; +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { css, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; +import "../../../../../components/ha-alert"; import "../../../../../components/ha-card"; import { acceptSharedMatterDevice, + canCommissionMatterExternal, commissionMatterDevice, matterSetThread, matterSetWifi, + redirectOnNewMatterDevice, + startExternalCommissioning, } from "../../../../../data/matter"; +import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box"; import "../../../../../layouts/hass-subpage"; import { haStyle } from "../../../../../resources/styles"; import { HomeAssistant } from "../../../../../types"; -import "../../../../../components/ha-alert"; -import { showPromptDialog } from "../../../../../dialogs/generic/show-dialog-box"; -import { navigate } from "../../../../../common/navigate"; -import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; -import { isDevVersion } from "../../../../../common/config/version"; @customElement("matter-config-panel") export class MatterConfigPanel extends LitElement { @@ -25,10 +27,11 @@ export class MatterConfigPanel extends LitElement { @state() private _error?: string; - private _curMatterDevices?: Set; + private _unsub?: UnsubscribeFunc; - private get _canCommissionMatter() { - return this.hass.auth.external?.config.canCommissionMatter; + disconnectedCallback() { + super.disconnectedCallback(); + this._stopRedirect(); } protected render(): TemplateResult { @@ -57,19 +60,17 @@ export class MatterConfigPanel extends LitElement { share code.
- ${this._canCommissionMatter + ${canCommissionMatterExternal(this.hass) ? html`Commission device with mobile app` : ""} - ${isDevVersion(this.hass.config.version) - ? html`Commission device - Add shared device` - : ""} + Commission device + Add shared device Set WiFi Credentials @@ -83,33 +84,23 @@ export class MatterConfigPanel extends LitElement { `; } - protected override updated(changedProps: PropertyValues) { - super.updated(changedProps); - - if (!this._curMatterDevices || !changedProps.has("hass")) { + private _redirectOnNewMatterDevice() { + if (this._unsub) { return; } + this._unsub = redirectOnNewMatterDevice(this.hass, () => { + this._unsub = undefined; + }); + } - const oldHass = changedProps.get("hass") as HomeAssistant | undefined; - if (!oldHass || oldHass.devices === this.hass.devices) { - return; - } - - const newMatterDevices = Object.values(this.hass.devices).filter( - (device) => - device.identifiers.find((identifier) => identifier[0] === "matter") && - !this._curMatterDevices!.has(device.id) - ); - if (newMatterDevices.length) { - navigate(`/config/devices/device/${newMatterDevices[0].id}`); - } + private _stopRedirect() { + this._unsub?.(); + this._unsub = undefined; } private _startMobileCommissioning() { - this._redirectOnNewDevice(); - this.hass.auth.external!.fireMessage({ - type: "matter/commission", - }); + this._redirectOnNewMatterDevice(); + startExternalCommissioning(this.hass); } private async _setWifi(): Promise { @@ -150,11 +141,12 @@ export class MatterConfigPanel extends LitElement { return; } this._error = undefined; - this._redirectOnNewDevice(); + this._redirectOnNewMatterDevice(); try { await commissionMatterDevice(this.hass, code); } catch (err: any) { this._error = err.message; + this._stopRedirect(); } } @@ -169,11 +161,12 @@ export class MatterConfigPanel extends LitElement { return; } this._error = undefined; - this._redirectOnNewDevice(); + this._redirectOnNewMatterDevice(); try { await acceptSharedMatterDevice(this.hass, Number(code)); } catch (err: any) { this._error = err.message; + this._stopRedirect(); } } @@ -195,19 +188,6 @@ export class MatterConfigPanel extends LitElement { } } - private _redirectOnNewDevice() { - if (this._curMatterDevices) { - return; - } - this._curMatterDevices = new Set( - Object.values(this.hass.devices) - .filter((device) => - device.identifiers.find((identifier) => identifier[0] === "matter") - ) - .map((device) => device.id) - ); - } - static styles = [ haStyle, css` diff --git a/src/panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device.ts b/src/panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device.ts new file mode 100644 index 0000000000..3559482e7c --- /dev/null +++ b/src/panels/config/integrations/integration-panels/matter/show-dialog-add-matter-device.ts @@ -0,0 +1,11 @@ +import { fireEvent } from "../../../../../common/dom/fire_event"; + +export const loadAddDeviceDialog = () => import("./dialog-matter-add-device"); + +export const showMatterAddDeviceDialog = (element: HTMLElement): void => { + fireEvent(element, "show-dialog", { + dialogTag: "dialog-matter-add-device", + dialogImport: loadAddDeviceDialog, + dialogParams: {}, + }); +}; diff --git a/src/translations/en.json b/src/translations/en.json index 4c08100009..9a268b7d5f 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2993,6 +2993,7 @@ "search_brand": "Search for a brand name", "add_zwave_js_device": "Add Z-Wave device", "add_zha_device": "Add Zigbee device", + "add_matter_device": "Add Matter device", "disable": { "show_disabled": "Show disabled integrations", "disabled_integrations": "{number} disabled", @@ -3106,8 +3107,10 @@ "could_not_load": "Config flow could not be loaded", "not_loaded": "The integration could not be loaded, try to restart Home Assistant.", "supported_brand_flow": "Support for {supported_brand} devices is provided by {flow_domain_name}. Do you want to continue?", - "missing_zwave_zigbee": "To add a {brand} device, you first need {supported_hardware_link} and the {integration} integration set up. If you already have the hardware then you can proceed with the setup of {integration}.", "missing_zwave_zigbee_title": "{integration} is not setup", + "missing_zwave_zigbee": "To add a {brand} device, you first need {supported_hardware_link} and the {integration} integration set up. If you already have the hardware then you can proceed with the setup of {integration}.", + "missing_matter": "To add a {brand} device, you first need the {integration} integration and {supported_hardware_link}. Do you want to proceed with the setup of {integration}?", + "matter_mobile_app": "You need to use the Home Assistant Companion app on your mobile phone to commission Matter devices.", "supported_hardware": "supported hardware", "proceed": "Proceed" }