Add my support for matter (#15372)

* Add my support for matter

* Update ha-panel-config.ts
This commit is contained in:
Bram Kragten 2023-02-07 05:55:47 +01:00 committed by GitHub
parent d570d063c7
commit 3298bdd5a3
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 304 additions and 207 deletions

View File

@ -130,8 +130,9 @@ export const protocolIntegrationPicked = async (
text: hass.localize(
"ui.panel.config.integrations.config_flow.missing_matter",
{
integration: "Matter",
brand: options?.brand || options?.domain || "Matter",
link: html`<a
supported_hardware_link: html`<a
href=${documentationUrl(hass, "/integrations/matter")}
target="_blank"
rel="noreferrer"

View File

@ -14,7 +14,10 @@ import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
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 type { LocalizeFunc } from "../../../common/translations/localize";
@ -761,7 +764,11 @@ class HaConfigIntegrations extends SubscribeMixin(LitElement) {
}
),
confirm: async () => {
if (["zha", "zwave_js"].includes(integration.supported_by!)) {
if (
(PROTOCOL_INTEGRATIONS as ReadonlyArray<string>).includes(
integration.supported_by!
)
) {
protocolIntegrationPicked(
this,
this.hass,

View File

@ -0,0 +1,22 @@
import { customElement } from "lit/decorators";
import { navigate } from "../../../../../common/navigate";
import { HomeAssistant } from "../../../../../types";
import { showMatterAddDeviceDialog } from "./show-dialog-add-matter-device";
@customElement("matter-add-device")
export class MatterAddDevice extends HTMLElement {
public hass!: HomeAssistant;
connectedCallback() {
showMatterAddDeviceDialog(this);
navigate(`/config/devices`, {
replace: true,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"matter-add-device": MatterAddDevice;
}
}

View File

@ -0,0 +1,212 @@
import "@material/mwc-button";
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";
@customElement("matter-config-dashboard")
export class MatterConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@state() private _error?: string;
private _unsub?: UnsubscribeFunc;
disconnectedCallback() {
super.disconnectedCallback();
this._stopRedirect();
}
protected render(): TemplateResult {
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Matter">
${isComponentLoaded(this.hass, "otbr")
? html`
<a href="/config/thread" slot="toolbar-icon">
<mwc-button>Visit Thread Panel</mwc-button>
</a>
`
: ""}
<div class="content">
<ha-card header="Matter">
<ha-alert alert-type="warning"
>Matter is still in the early phase of development, it is not
meant to be used in production. This panel is for development
only.</ha-alert
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
You can add Matter devices by commissing them if they are not
setup yet, or share them from another controller and enter the
share code.
</div>
<div class="card-actions">
${canCommissionMatterExternal(this.hass)
? html`<mwc-button @click=${this._startMobileCommissioning}
>Commission device with mobile app</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
>
<mwc-button @click=${this._setThread}
>Set Thread Credentials</mwc-button
>
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private _redirectOnNewMatterDevice() {
if (this._unsub) {
return;
}
this._unsub = redirectOnNewMatterDevice(this.hass, () => {
this._unsub = undefined;
});
}
private _stopRedirect() {
this._unsub?.();
this._unsub = undefined;
}
private _startMobileCommissioning() {
this._redirectOnNewMatterDevice();
startExternalCommissioning(this.hass);
}
private async _setWifi(): Promise<void> {
this._error = undefined;
const networkName = await showPromptDialog(this, {
title: "Network name",
inputLabel: "Network name",
inputType: "string",
confirmText: "Continue",
});
if (!networkName) {
return;
}
const psk = await showPromptDialog(this, {
title: "Passcode",
inputLabel: "Code",
inputType: "password",
confirmText: "Set Wifi",
});
if (!psk) {
return;
}
try {
await matterSetWifi(this.hass, networkName, psk);
} catch (err: any) {
this._error = err.message;
}
}
private async _commission(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Commission device",
inputLabel: "Code",
inputType: "string",
confirmText: "Commission",
});
if (!code) {
return;
}
this._error = undefined;
this._redirectOnNewMatterDevice();
try {
await commissionMatterDevice(this.hass, code);
} catch (err: any) {
this._error = err.message;
this._stopRedirect();
}
}
private async _acceptSharedDevice(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Add shared device",
inputLabel: "Pin",
inputType: "number",
confirmText: "Accept device",
});
if (!code) {
return;
}
this._error = undefined;
this._redirectOnNewMatterDevice();
try {
await acceptSharedMatterDevice(this.hass, Number(code));
} catch (err: any) {
this._error = err.message;
this._stopRedirect();
}
}
private async _setThread(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Set Thread operation",
inputLabel: "Dataset",
inputType: "string",
confirmText: "Set Thread",
});
if (!code) {
return;
}
this._error = undefined;
try {
await matterSetThread(this.hass, code);
} catch (err: any) {
this._error = err.message;
}
}
static styles = [
haStyle,
css`
ha-alert[alert-type="warning"] {
position: relative;
top: -16px;
}
.content {
padding: 24px 0 32px;
max-width: 600px;
margin: 0 auto;
direction: ltr;
}
ha-card:first-child {
margin-bottom: 16px;
}
a[slot="toolbar-icon"] {
text-decoration: none;
}
`,
];
}

View File

@ -1,212 +1,58 @@
import "@material/mwc-button";
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 { mdiMathLog, mdiServerNetwork } from "@mdi/js";
import { customElement, property } from "lit/decorators";
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";
HassRouterPage,
RouterOptions,
} from "../../../../../layouts/hass-router-page";
import { PageNavigation } from "../../../../../layouts/hass-tabs-subpage";
import { HomeAssistant } from "../../../../../types";
export const configTabs: PageNavigation[] = [
{
translationKey: "ui.panel.config.zwave_js.navigation.network",
path: `/config/zwave_js/dashboard`,
iconPath: mdiServerNetwork,
},
{
translationKey: "ui.panel.config.zwave_js.navigation.logs",
path: `/config/zwave_js/logs`,
iconPath: mdiMathLog,
},
];
@customElement("matter-config-panel")
export class MatterConfigPanel extends LitElement {
class MatterConfigRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public narrow!: boolean;
@property() public isWide!: boolean;
@state() private _error?: string;
@property() public narrow!: boolean;
private _unsub?: UnsubscribeFunc;
protected routerOptions: RouterOptions = {
defaultPage: "dashboard",
showLoading: true,
routes: {
dashboard: {
tag: "matter-config-dashboard",
load: () => import("./matter-config-dashboard"),
},
add: {
tag: "matter-add-device",
load: () => import("./matter-add-device"),
},
},
};
disconnectedCallback() {
super.disconnectedCallback();
this._stopRedirect();
protected updatePageEl(el): void {
el.route = this.routeTail;
el.hass = this.hass;
el.isWide = this.isWide;
el.narrow = this.narrow;
}
}
declare global {
interface HTMLElementTagNameMap {
"matter-config-panel": MatterConfigRouter;
}
protected render(): TemplateResult {
return html`
<hass-subpage .narrow=${this.narrow} .hass=${this.hass} header="Matter">
${isComponentLoaded(this.hass, "otbr")
? html`
<a href="/config/thread" slot="toolbar-icon">
<mwc-button>Visit Thread Panel</mwc-button>
</a>
`
: ""}
<div class="content">
<ha-card header="Matter">
<ha-alert alert-type="warning"
>Matter is still in the early phase of development, it is not
meant to be used in production. This panel is for development
only.</ha-alert
>
<div class="card-content">
${this._error
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
: ""}
You can add Matter devices by commissing them if they are not
setup yet, or share them from another controller and enter the
share code.
</div>
<div class="card-actions">
${canCommissionMatterExternal(this.hass)
? html`<mwc-button @click=${this._startMobileCommissioning}
>Commission device with mobile app</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
>
<mwc-button @click=${this._setThread}
>Set Thread Credentials</mwc-button
>
</div>
</ha-card>
</div>
</hass-subpage>
`;
}
private _redirectOnNewMatterDevice() {
if (this._unsub) {
return;
}
this._unsub = redirectOnNewMatterDevice(this.hass, () => {
this._unsub = undefined;
});
}
private _stopRedirect() {
this._unsub?.();
this._unsub = undefined;
}
private _startMobileCommissioning() {
this._redirectOnNewMatterDevice();
startExternalCommissioning(this.hass);
}
private async _setWifi(): Promise<void> {
this._error = undefined;
const networkName = await showPromptDialog(this, {
title: "Network name",
inputLabel: "Network name",
inputType: "string",
confirmText: "Continue",
});
if (!networkName) {
return;
}
const psk = await showPromptDialog(this, {
title: "Passcode",
inputLabel: "Code",
inputType: "password",
confirmText: "Set Wifi",
});
if (!psk) {
return;
}
try {
await matterSetWifi(this.hass, networkName, psk);
} catch (err: any) {
this._error = err.message;
}
}
private async _commission(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Commission device",
inputLabel: "Code",
inputType: "string",
confirmText: "Commission",
});
if (!code) {
return;
}
this._error = undefined;
this._redirectOnNewMatterDevice();
try {
await commissionMatterDevice(this.hass, code);
} catch (err: any) {
this._error = err.message;
this._stopRedirect();
}
}
private async _acceptSharedDevice(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Add shared device",
inputLabel: "Pin",
inputType: "number",
confirmText: "Accept device",
});
if (!code) {
return;
}
this._error = undefined;
this._redirectOnNewMatterDevice();
try {
await acceptSharedMatterDevice(this.hass, Number(code));
} catch (err: any) {
this._error = err.message;
this._stopRedirect();
}
}
private async _setThread(): Promise<void> {
const code = await showPromptDialog(this, {
title: "Set Thread operation",
inputLabel: "Dataset",
inputType: "string",
confirmText: "Set Thread",
});
if (!code) {
return;
}
this._error = undefined;
try {
await matterSetThread(this.hass, code);
} catch (err: any) {
this._error = err.message;
}
}
static styles = [
haStyle,
css`
ha-alert[alert-type="warning"] {
position: relative;
top: -16px;
}
.content {
padding: 24px 0 32px;
max-width: 600px;
margin: 0 auto;
direction: ltr;
}
ha-card:first-child {
margin-bottom: 16px;
}
a[slot="toolbar-icon"] {
text-decoration: none;
}
`,
];
}

View File

@ -2,7 +2,10 @@ import { sanitizeUrl } from "@braintree/sanitize-url";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { protocolIntegrationPicked } from "../../common/integrations/protocolIntegrationPicked";
import {
protocolIntegrationPicked,
PROTOCOL_INTEGRATIONS,
} from "../../common/integrations/protocolIntegrationPicked";
import { navigate } from "../../common/navigate";
import {
createSearchParam,
@ -87,6 +90,10 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
component: "zwave_js",
redirect: "/config/zwave_js/add",
},
add_matter_device: {
component: "matter",
redirect: "/config/matter/add",
},
config_energy: {
component: "energy",
redirect: "/config/energy/dashboard",
@ -310,9 +317,11 @@ class HaPanelMy extends LitElement {
) {
this.hass.loadBackendTranslation("title", this._redirect.component);
this._error = "no_component";
if (["add_zwave_device", "add_zigbee_device"].includes(path)) {
const component = this._redirect.component;
if (
(PROTOCOL_INTEGRATIONS as ReadonlyArray<string>).includes(component)
) {
const params = extractSearchParamsObject();
const component = this._redirect.component;
this.hass
.loadFragmentTranslation("config")
.then()