Add "add matter device" link to add integration dialog (#15365)

This commit is contained in:
Bram Kragten 2023-02-06 18:18:30 +01:00 committed by GitHub
parent e47b59f826
commit 5b504bf9ce
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
10 changed files with 286 additions and 96 deletions

View File

@ -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`<a
href=${documentationUrl(hass, "/integrations/matter")}
target="_blank"
rel="noreferrer"
>${hass.localize(
"ui.panel.config.integrations.config_flow.supported_hardware"
)}</a
>`,
}
),
confirmText: hass.localize(
"ui.panel.config.integrations.config_flow.proceed"
),
confirm: () => {
showConfigFlowDialog(element, {
startFlowHandler: "matter",
});
},
});
return;
}
showMatterAddDeviceDialog(element);
}
};

View File

@ -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<string> | 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,

View File

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

View File

@ -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<string>).includes(
integration.domain
) &&
isComponentLoaded(this.hass, integration.domain)
) {
this._pickedBrand = integration.domain;

View File

@ -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`<mwc-list-item
graphic="medium"
.domain=${domain}
@request-selected=${this._standardPicked}
hasMeta
>
<img
slot="graphic"
loading="lazy"
alt=""
src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<span
>${this.hass.localize(
`ui.panel.config.integrations.add_${domain}_device`
)}</span
? this.integration.iot_standards
.filter((standard) =>
(PROTOCOL_INTEGRATIONS as ReadonlyArray<string>).includes(
standardToDomain[standard] || standard
)
)
.map((standard) => {
const domain: (typeof PROTOCOL_INTEGRATIONS)[number] =
standardToDomain[standard] || standard;
return html`<mwc-list-item
graphic="medium"
.domain=${domain}
@request-selected=${this._standardPicked}
hasMeta
>
<ha-icon-next slot="meta"></ha-icon-next>
</mwc-list-item>`;
})
<img
slot="graphic"
loading="lazy"
alt=""
src=${brandsUrl({
domain,
type: "icon",
useFallback: true,
darkOptimized: this.hass.themes?.darkMode,
})}
referrerpolicy="no-referrer"
/>
<span
>${this.hass.localize(
`ui.panel.config.integrations.add_${domain}_device`
)}</span
>
<ha-icon-next slot="meta"></ha-icon-next>
</mwc-list-item>`;
})
: ""}
${this.integration &&
"integrations" in this.integration &&
@ -144,7 +150,7 @@ class HaDomainIntegrations extends LitElement {
</ha-integration-list-item>`
)
: ""}
${this.domain === "zha" || this.domain === "zwave_js"
${(PROTOCOL_INTEGRATIONS as ReadonlyArray<string>).includes(this.domain)
? html`<mwc-list-item
graphic="medium"
.domain=${this.domain}
@ -165,7 +171,9 @@ class HaDomainIntegrations extends LitElement {
/>
<span
>${this.hass.localize(
`ui.panel.config.integrations.add_${this.domain}_device`
`ui.panel.config.integrations.add_${
this.domain as (typeof PROTOCOL_INTEGRATIONS)[number]
}_device`
)}</span
>
<ha-icon-next slot="meta"></ha-icon-next>

View File

@ -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`<mwc-button unelevated @click=${this._handleEnable}>
${this.hass.localize("ui.common.enable")}
</mwc-button>`
: item.domain in integrationsWithPanel
: item.domain in integrationsWithPanel &&
(item.domain !== "matter" ||
isDevVersion(this.hass.config.version))
? html`<a
href=${`${integrationsWithPanel[item.domain]}?config_entry=${
item.entry_id

View File

@ -0,0 +1,87 @@
import "@material/mwc-button/mwc-button";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-circular-progress";
import { createCloseHeading } from "../../../../../components/ha-dialog";
import {
addMatterDevice,
canCommissionMatterExternal,
redirectOnNewMatterDevice,
} from "../../../../../data/matter";
import { haStyleDialog } from "../../../../../resources/styles";
import { HomeAssistant } from "../../../../../types";
@customElement("dialog-matter-add-device")
class DialogMatterAddDevice extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@state() private _open = false;
private _unsub?: UnsubscribeFunc;
public showDialog(): void {
this._open = true;
if (!canCommissionMatterExternal(this.hass)) {
return;
}
this._unsub = redirectOnNewMatterDevice(this.hass, () =>
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`
<ha-dialog
open
@closed=${this.closeDialog}
.heading=${createCloseHeading(this.hass, "Add Matter device")}
>
<div>
${!canCommissionMatterExternal(this.hass)
? this.hass.localize(
"ui.panel.config.integrations.config_flow.matter_mobile_app"
)
: html`<ha-circular-progress
size="large"
active
></ha-circular-progress>`}
</div>
<mwc-button slot="primaryAction" @click=${this.closeDialog}>
${this.hass.localize("ui.common.cancel")}
</mwc-button>
</ha-dialog>
`;
}
static styles = [
haStyleDialog,
css`
div {
display: grid;
}
ha-circular-progress {
justify-self: center;
}
`,
];
}
declare global {
interface HTMLElementTagNameMap {
"dialog-matter-add-device": DialogMatterAddDevice;
}
}

View File

@ -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<string>;
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.
</div>
<div class="card-actions">
${this._canCommissionMatter
${canCommissionMatterExternal(this.hass)
? html`<mwc-button @click=${this._startMobileCommissioning}
>Commission device with mobile app</mwc-button
>`
: ""}
${isDevVersion(this.hass.config.version)
? html`<mwc-button @click=${this._commission}
>Commission device</mwc-button
>
<mwc-button @click=${this._acceptSharedDevice}
>Add shared device</mwc-button
>`
: ""}
<mwc-button @click=${this._commission}
>Commission device</mwc-button
>
<mwc-button @click=${this._acceptSharedDevice}
>Add shared device</mwc-button
>
<mwc-button @click=${this._setWifi}
>Set WiFi Credentials</mwc-button
>
@ -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<void> {
@ -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`

View File

@ -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: {},
});
};

View File

@ -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"
}