From 305cecb21368265c348578d8ce5427f9b1c3d109 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 24 Sep 2024 20:38:00 +0200 Subject: [PATCH] Add MVP voice assist flow (#22061) * Add MVP voice assist flow * filter on supported features * check for unavailable * Update step-flow-create-entry.ts --- public/static/icons/casita/loading.png | Bin 0 -> 3967 bytes public/static/icons/casita/loving.png | Bin 0 -> 4644 bytes public/static/icons/casita/normal.png | Bin 0 -> 4699 bytes public/static/icons/casita/sad.png | Bin 0 -> 2382 bytes public/static/icons/casita/sleeping.png | Bin 0 -> 4468 bytes public/static/icons/casita/smiling.png | Bin 0 -> 4500 bytes src/data/assist_satellite.ts | 81 +++++ .../config-flow/step-flow-create-entry.ts | 54 +++- .../show-voice-assistant-setup-dialog.ts | 19 ++ src/dialogs/voice-assistant-setup/styles.ts | 36 +++ .../voice-assistant-setup-dialog.ts | 276 ++++++++++++++++ .../voice-assistant-setup-step-addons.ts | 187 +++++++++++ .../voice-assistant-setup-step-area.ts | 67 ++++ ...e-assistant-setup-step-change-wake-word.ts | 84 +++++ .../voice-assistant-setup-step-check.ts | 87 +++++ .../voice-assistant-setup-step-cloud.ts | 38 +++ .../voice-assistant-setup-step-pipeline.ts | 304 ++++++++++++++++++ .../voice-assistant-setup-step-success.ts | 226 +++++++++++++ .../voice-assistant-setup-step-update.ts | 119 +++++++ .../voice-assistant-setup-step-wake-word.ts | 129 ++++++++ .../config/devices/ha-config-device-page.ts | 28 ++ .../dialog-voice-assistant-pipeline-detail.ts | 24 +- ...-dialog-voice-assistant-pipeline-detail.ts | 5 +- 23 files changed, 1753 insertions(+), 11 deletions(-) create mode 100644 public/static/icons/casita/loading.png create mode 100644 public/static/icons/casita/loving.png create mode 100644 public/static/icons/casita/normal.png create mode 100644 public/static/icons/casita/sad.png create mode 100644 public/static/icons/casita/sleeping.png create mode 100644 public/static/icons/casita/smiling.png create mode 100644 src/data/assist_satellite.ts create mode 100644 src/dialogs/voice-assistant-setup/show-voice-assistant-setup-dialog.ts create mode 100644 src/dialogs/voice-assistant-setup/styles.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-addons.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-area.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-change-wake-word.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-pipeline.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts create mode 100644 src/dialogs/voice-assistant-setup/voice-assistant-setup-step-wake-word.ts diff --git a/public/static/icons/casita/loading.png b/public/static/icons/casita/loading.png new file mode 100644 index 0000000000000000000000000000000000000000..b2bcd76052d9f62cbddacf1e079673832a62907a GIT binary patch literal 3967 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 I>UsD70MO&7fdBvi literal 0 HcmV?d00001 diff --git a/public/static/icons/casita/normal.png b/public/static/icons/casita/normal.png new file mode 100644 index 0000000000000000000000000000000000000000..0b72c54b990551cb9a1dbdefd313b8e86dd5fc1f GIT binary patch literal 4699 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 literal 0 HcmV?d00001 diff --git a/public/static/icons/casita/sad.png b/public/static/icons/casita/sad.png new file mode 100644 index 0000000000000000000000000000000000000000..d97183685e1646e62dc9f3a05724f78b39a06ec8 GIT binary patch literal 2382 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 literal 0 HcmV?d00001 diff --git a/public/static/icons/casita/smiling.png b/public/static/icons/casita/smiling.png new file mode 100644 index 0000000000000000000000000000000000000000..5fb4d9458757e539a559e0ffa5620ac291242541 GIT binary patch literal 4500 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 XF%_P`pfM-@{t?l?VysiCjeqbTecBMi literal 0 HcmV?d00001 diff --git a/src/data/assist_satellite.ts b/src/data/assist_satellite.ts new file mode 100644 index 0000000000..15c91527b9 --- /dev/null +++ b/src/data/assist_satellite.ts @@ -0,0 +1,81 @@ +import { HassEntity } from "home-assistant-js-websocket"; +import { HomeAssistant } from "../types"; +import { supportsFeature } from "../common/entity/supports-feature"; +import { UNAVAILABLE } from "./entity"; + +export const enum AssistSatelliteEntityFeature { + ANNOUNCE = 1, +} + +export interface WakeWordInterceptMessage { + wake_word_phrase: string; +} + +export interface WakeWordOption { + id: string; + wake_word: string; + trained_languages: string[]; +} + +export interface AssistSatelliteConfiguration { + active_wake_words: string[]; + available_wake_words: WakeWordOption[]; + max_active_wake_words: number; + pipeline_entity_id: string; + vad_entity_id: string; +} + +export const interceptWakeWord = ( + hass: HomeAssistant, + entity_id: string, + callback: (result: WakeWordInterceptMessage) => void +) => + hass.connection.subscribeMessage(callback, { + type: "assist_satellite/intercept_wake_word", + entity_id, + }); + +export const testAssistSatelliteConnection = ( + hass: HomeAssistant, + entity_id: string +) => + hass.callWS<{ + status: "success" | "timeout"; + }>({ + type: "assist_satellite/test_connection", + entity_id, + }); + +export const assistSatelliteAnnounce = ( + hass: HomeAssistant, + entity_id: string, + message: string +) => + hass.callService("assist_satellite", "announce", { message }, { entity_id }); + +export const fetchAssistSatelliteConfiguration = ( + hass: HomeAssistant, + entity_id: string +) => + hass.callWS({ + type: "assist_satellite/get_configuration", + entity_id, + }); + +export const setWakeWords = ( + hass: HomeAssistant, + entity_id: string, + wake_word_ids: string[] +) => + hass.callWS({ + type: "assist_satellite/set_wake_words", + entity_id, + wake_word_ids, + }); + +export const assistSatelliteSupportsSetupFlow = ( + assistSatelliteEntity: HassEntity | undefined +) => + assistSatelliteEntity && + assistSatelliteEntity.state !== UNAVAILABLE && + supportsFeature(assistSatelliteEntity, AssistSatelliteEntityFeature.ANNOUNCE); diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index 43c5e93e3c..a897378c04 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -1,6 +1,14 @@ import "@material/mwc-button"; -import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValues, + TemplateResult, +} from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { fireEvent } from "../../common/dom/fire_event"; import "../../components/ha-area-picker"; import { DataEntryFlowStepCreateEntry } from "../../data/data_entry_flow"; @@ -9,10 +17,14 @@ import { DeviceRegistryEntry, updateDeviceRegistryEntry, } from "../../data/device_registry"; +import { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import { HomeAssistant } from "../../types"; import { showAlertDialog } from "../generic/show-dialog-box"; import { FlowConfig } from "./show-dialog-data-entry-flow"; import { configFlowContentStyles } from "./styles"; +import { computeDomain } from "../../common/entity/compute_domain"; +import { showVoiceAssistantSetupDialog } from "../voice-assistant-setup/show-voice-assistant-setup-dialog"; +import { assistSatelliteSupportsSetupFlow } from "../../data/assist_satellite"; @customElement("step-flow-create-entry") class StepFlowCreateEntry extends LitElement { @@ -24,6 +36,46 @@ class StepFlowCreateEntry extends LitElement { @property({ attribute: false }) public devices!: DeviceRegistryEntry[]; + private _deviceEntities = memoizeOne( + ( + deviceId: string, + entities: EntityRegistryDisplayEntry[], + domain?: string + ): EntityRegistryDisplayEntry[] => + entities.filter( + (entity) => + entity.device_id === deviceId && + (!domain || computeDomain(entity.entity_id) === domain) + ) + ); + + protected willUpdate(changedProps: PropertyValues) { + if ( + (changedProps.has("devices") || changedProps.has("hass")) && + this.devices.length === 1 + ) { + // integration_type === "device" + const assistSatellites = this._deviceEntities( + this.devices[0].id, + Object.values(this.hass.entities), + "assist_satellite" + ); + if ( + assistSatellites.length && + assistSatellites.some((satellite) => + assistSatelliteSupportsSetupFlow( + this.hass.states[satellite.entity_id] + ) + ) + ) { + this._flowDone(); + showVoiceAssistantSetupDialog(this, { + deviceId: this.devices[0].id, + }); + } + } + } + protected render(): TemplateResult { const localize = this.hass.localize; diff --git a/src/dialogs/voice-assistant-setup/show-voice-assistant-setup-dialog.ts b/src/dialogs/voice-assistant-setup/show-voice-assistant-setup-dialog.ts new file mode 100644 index 0000000000..26c85f979d --- /dev/null +++ b/src/dialogs/voice-assistant-setup/show-voice-assistant-setup-dialog.ts @@ -0,0 +1,19 @@ +import { fireEvent } from "../../common/dom/fire_event"; + +const loadVoiceAssistantSetupDialog = () => + import("./voice-assistant-setup-dialog"); + +export interface VoiceAssistantSetupDialogParams { + deviceId: string; +} + +export const showVoiceAssistantSetupDialog = ( + element: HTMLElement, + dialogParams: VoiceAssistantSetupDialogParams +): void => { + fireEvent(element, "show-dialog", { + dialogTag: "ha-voice-assistant-setup-dialog", + dialogImport: loadVoiceAssistantSetupDialog, + dialogParams: dialogParams, + }); +}; diff --git a/src/dialogs/voice-assistant-setup/styles.ts b/src/dialogs/voice-assistant-setup/styles.ts new file mode 100644 index 0000000000..e0357bb996 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/styles.ts @@ -0,0 +1,36 @@ +import { css } from "lit"; +import { haStyle } from "../../resources/styles"; + +export const AssistantSetupStyles = [ + haStyle, + css` + :host { + align-items: center; + text-align: center; + min-height: 300px; + max-width: 500px; + display: flex; + flex-direction: column; + justify-content: space-between; + height: 100%; + padding: 24px; + box-sizing: border-box; + } + .content { + flex: 1; + } + .content img { + width: 120px; + margin-top: 68px; + margin-bottom: 68px; + } + .footer { + width: 100%; + display: flex; + flex-direction: column; + } + .footer ha-button { + width: 100%; + } + `, +]; diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts new file mode 100644 index 0000000000..899d72ac89 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-dialog.ts @@ -0,0 +1,276 @@ +import "@material/mwc-button/mwc-button"; +import { mdiChevronLeft } from "@mdi/js"; +import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../common/dom/fire_event"; +import { computeDomain } from "../../common/entity/compute_domain"; +import "../../components/ha-dialog"; +import { + AssistSatelliteConfiguration, + fetchAssistSatelliteConfiguration, +} from "../../data/assist_satellite"; +import { EntityRegistryDisplayEntry } from "../../data/entity_registry"; +import { haStyleDialog } from "../../resources/styles"; +import type { HomeAssistant } from "../../types"; +import { VoiceAssistantSetupDialogParams } from "./show-voice-assistant-setup-dialog"; +import "./voice-assistant-setup-step-addons"; +import "./voice-assistant-setup-step-area"; +import "./voice-assistant-setup-step-change-wake-word"; +import "./voice-assistant-setup-step-check"; +import "./voice-assistant-setup-step-cloud"; +import "./voice-assistant-setup-step-pipeline"; +import "./voice-assistant-setup-step-success"; +import "./voice-assistant-setup-step-update"; +import "./voice-assistant-setup-step-wake-word"; +import { UNAVAILABLE } from "../../data/entity"; + +export const enum STEP { + INIT, + UPDATE, + CHECK, + WAKEWORD, + AREA, + PIPELINE, + SUCCESS, + CLOUD, + ADDONS, + CHANGE_WAKEWORD, +} + +@customElement("ha-voice-assistant-setup-dialog") +export class HaVoiceAssistantSetupDialog extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _params?: VoiceAssistantSetupDialogParams; + + @state() private _step: STEP = STEP.INIT; + + @state() private _assistConfiguration?: AssistSatelliteConfiguration; + + private _previousSteps: STEP[] = []; + + public async showDialog( + params: VoiceAssistantSetupDialogParams + ): Promise { + this._params = params; + + await this._fetchAssistConfiguration(); + + this._step = STEP.UPDATE; + } + + public async closeDialog(): Promise { + this.renderRoot.querySelector("ha-dialog")?.close(); + } + + private _dialogClosed() { + this._params = undefined; + this._assistConfiguration = undefined; + this._step = STEP.INIT; + fireEvent(this, "dialog-closed", { dialog: this.localName }); + } + + private _deviceEntities = memoizeOne( + ( + deviceId: string, + entities: HomeAssistant["entities"] + ): EntityRegistryDisplayEntry[] => + Object.values(entities).filter((entity) => entity.device_id === deviceId) + ); + + private _findDomainEntityId = memoizeOne( + ( + deviceId: string, + entities: HomeAssistant["entities"], + domain: string + ): string | undefined => { + const deviceEntities = this._deviceEntities(deviceId, entities); + return deviceEntities.find( + (ent) => computeDomain(ent.entity_id) === domain + )?.entity_id; + } + ); + + protected render() { + if (!this._params) { + return nothing; + } + + const assistSatelliteEntityId = this._findDomainEntityId( + this._params.deviceId, + this.hass.entities, + "assist_satellite" + ); + + const assistEntityState = assistSatelliteEntityId + ? this.hass.states[assistSatelliteEntityId] + : undefined; + + return html` + + + ${this._previousSteps.length + ? html`` + : nothing} + +
+ ${this._step === STEP.UPDATE + ? html`` + : assistEntityState?.state === UNAVAILABLE + ? html`Your voice assistant is not available.` + : this._step === STEP.CHECK + ? html`` + : this._step === STEP.WAKEWORD + ? html`` + : this._step === STEP.CHANGE_WAKEWORD + ? html` + + ` + : this._step === STEP.AREA + ? html` + + ` + : this._step === STEP.PIPELINE + ? html`` + : this._step === STEP.CLOUD + ? html`` + : this._step === STEP.ADDONS + ? html`` + : this._step === STEP.SUCCESS + ? html`` + : nothing} +
+
+ `; + } + + private async _fetchAssistConfiguration() { + this._assistConfiguration = await fetchAssistSatelliteConfiguration( + this.hass, + this._findDomainEntityId( + this._params!.deviceId, + this.hass.entities, + "assist_satellite" + )! + ); + return this._assistConfiguration; + } + + private _goToPreviousStep() { + if (!this._previousSteps.length) { + return; + } + this._step = this._previousSteps.pop()!; + } + + private _nextStep(ev) { + if (ev.detail?.updateConfig) { + this._fetchAssistConfiguration(); + } + if (!ev.detail?.noPrevious) { + this._previousSteps.push(this._step); + } + if (ev.detail?.step) { + this._step = ev.detail.step; + } else { + this._step += 1; + } + } + + static get styles(): CSSResultGroup { + return [ + haStyleDialog, + css` + ha-dialog { + --dialog-content-padding: 0; + } + ha-dialog-header { + height: 56px; + } + @media all and (max-width: 450px), all and (max-height: 500px) { + .content { + height: calc(100vh - 56px); + } + } + `, + ]; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-dialog": HaVoiceAssistantSetupDialog; + } + + interface HASSDomEvents { + "next-step": + | { step?: STEP; updateConfig?: boolean; noPrevious?: boolean } + | undefined; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-addons.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-addons.ts new file mode 100644 index 0000000000..69c3785e6f --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-addons.ts @@ -0,0 +1,187 @@ +import { css, html, LitElement, nothing, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { HomeAssistant } from "../../types"; +import { AssistantSetupStyles } from "./styles"; +import { STEP } from "./voice-assistant-setup-dialog"; +import { documentationUrl } from "../../util/documentation-url"; + +@customElement("ha-voice-assistant-setup-step-addons") +export class HaVoiceAssistantSetupStepAddons extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @state() private _showFirst = false; + + @state() private _showSecond = false; + + @state() private _showThird = false; + + @state() private _showFourth = false; + + protected override firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + setTimeout(() => { + this._showFirst = true; + }, 200); + setTimeout(() => { + this._showSecond = true; + }, 600); + setTimeout(() => { + this._showThird = true; + }, 3000); + setTimeout(() => { + this._showFourth = true; + }, 8000); + } + + protected override render() { + return html`
+

Local

+

+ Are you sure you want to use the local voice assistant? It requires a + powerful device to run. If you device is not powerful enough, Home + Assistant cloud might be a better option. +

+

Home Assistant Cloud:

+
+
+ ${!this._showFirst ? "…" : "Turn on the lights in the bedroom"} +
+ ${this._showFirst + ? html`
0.2 seconds
` + : nothing} + ${this._showFirst + ? html`
+ ${!this._showSecond ? "…" : "Turned on the lights"} +
` + : nothing} + ${this._showSecond + ? html`
0.4 seconds
` + : nothing} +
+

Raspberry Pi 4:

+
+
+ ${!this._showThird ? "…" : "Turn on the lights in the bedroom"} +
+ ${this._showThird + ? html`
3 seconds
` + : nothing} + ${this._showThird + ? html`
+ ${!this._showFourth ? "…" : "Turned on the lights"} +
` + : nothing} + ${this._showFourth + ? html`
5 seconds
` + : nothing} +
+
+ `; + } + + private _close() { + fireEvent(this, "closed"); + } + + private _skip() { + fireEvent(this, "next-step", { step: STEP.SUCCESS }); + } + + static styles = [ + AssistantSetupStyles, + css` + .messages-container { + padding: 24px; + box-sizing: border-box; + height: 195px; + background: var(--input-fill-color); + border-radius: 16px; + border: 1px solid var(--divider-color); + display: flex; + flex-direction: column; + } + .message { + white-space: nowrap; + font-size: 18px; + clear: both; + margin: 8px 0; + padding: 8px; + border-radius: 15px; + height: 36px; + box-sizing: border-box; + overflow: hidden; + text-overflow: ellipsis; + width: 30px; + } + .rpi .message { + transition: width 1s; + } + .cloud .message { + transition: width 0.5s; + } + + .message.user { + margin-left: 24px; + margin-inline-start: 24px; + margin-inline-end: initial; + align-self: self-end; + text-align: right; + border-bottom-right-radius: 0px; + background-color: var(--primary-color); + color: var(--text-primary-color); + direction: var(--direction); + } + .timing.user { + align-self: self-end; + } + + .message.user.show { + width: 295px; + } + + .message.hass { + margin-right: 24px; + margin-inline-end: 24px; + margin-inline-start: initial; + align-self: self-start; + border-bottom-left-radius: 0px; + background-color: var(--secondary-background-color); + color: var(--primary-text-color); + direction: var(--direction); + } + .timing.hass { + align-self: self-start; + } + + .message.hass.show { + width: 184px; + } + .footer { + margin-top: 24px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-addons": HaVoiceAssistantSetupStepAddons; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-area.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-area.ts new file mode 100644 index 0000000000..6552172e0d --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-area.ts @@ -0,0 +1,67 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { updateDeviceRegistryEntry } from "../../data/device_registry"; +import { HomeAssistant } from "../../types"; +import { showAlertDialog } from "../generic/show-dialog-box"; +import { AssistantSetupStyles } from "./styles"; + +@customElement("ha-voice-assistant-setup-step-area") +export class HaVoiceAssistantSetupStepArea extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public deviceId!: string; + + protected override render() { + const device = this.hass.devices[this.deviceId]; + + return html`
+ +

Select area

+

+ When you voice assistant knows where it is, it can better control the + devices around it. +

+ +
+ `; + } + + private async _setArea() { + const area = this.shadowRoot!.querySelector("ha-area-picker")!.value; + if (!area) { + showAlertDialog(this, { text: "Please select an area" }); + return; + } + await updateDeviceRegistryEntry(this.hass, this.deviceId, { + area_id: area, + }); + this._nextStep(); + } + + private _nextStep() { + fireEvent(this, "next-step"); + } + + static styles = [ + AssistantSetupStyles, + css` + ha-area-picker { + display: block; + width: 100%; + margin-bottom: 24px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-area": HaVoiceAssistantSetupStepArea; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-change-wake-word.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-change-wake-word.ts new file mode 100644 index 0000000000..4a229691b3 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-change-wake-word.ts @@ -0,0 +1,84 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { + AssistSatelliteConfiguration, + setWakeWords, +} from "../../data/assist_satellite"; +import { HomeAssistant } from "../../types"; +import { STEP } from "./voice-assistant-setup-dialog"; +import { AssistantSetupStyles } from "./styles"; +import "../../components/ha-md-list"; +import "../../components/ha-md-list-item"; + +@customElement("ha-voice-assistant-setup-step-change-wake-word") +export class HaVoiceAssistantSetupStepChangeWakeWord extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + public assistConfiguration?: AssistSatelliteConfiguration; + + @property() public assistEntityId?: string; + + protected override render() { + return html`
+ +

Change wake word

+

+ When you voice assistant knows where it is, it can better control the + devices around it. +

+
+ + ${this.assistConfiguration!.available_wake_words.map( + (wakeWord) => + html` + ${wakeWord.wake_word} + + ` + )} + `; + } + + private async _wakeWordPicked(ev) { + if (!this.assistEntityId) { + return; + } + + const wakeWordId = ev.currentTarget.value; + + await setWakeWords(this.hass, this.assistEntityId, [wakeWordId]); + this._nextStep(); + } + + private _nextStep() { + fireEvent(this, "next-step", { step: STEP.WAKEWORD, updateConfig: true }); + } + + static styles = [ + AssistantSetupStyles, + css` + :host { + padding: 0; + } + .padding { + padding: 24px; + } + ha-md-list { + width: 100%; + text-align: initial; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-change-wake-word": HaVoiceAssistantSetupStepChangeWakeWord; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts new file mode 100644 index 0000000000..0d61e8bd73 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-check.ts @@ -0,0 +1,87 @@ +import { html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { testAssistSatelliteConnection } from "../../data/assist_satellite"; +import { HomeAssistant } from "../../types"; +import { AssistantSetupStyles } from "./styles"; + +@customElement("ha-voice-assistant-setup-step-check") +export class HaVoiceAssistantSetupStepCheck extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public assistEntityId?: string; + + @state() private _status?: "success" | "timeout"; + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + if (!this.hasUpdated) { + this._testConnection(); + return; + } + if ( + this._status === "success" && + changedProperties.has("hass") && + this.hass.states[this.assistEntityId!]?.state === "listening_wake_word" + ) { + this._nextStep(); + } + } + + protected override render() { + return html`
+ ${this._status === "success" + ? html` +

Hi

+

+ With a couple of steps we are going to setup your voice assistant. +

` + : this._status === "timeout" + ? html` +

Error

+

+ Your device was unable to reach Home Assistant. Make sure you + have setup your + Home Assistant URL's + correctly. +

+ ` + : html` +

Checking...

+

+ We are checking if the device can reach your Home Assistant + instance. +

+ `} +
`; + } + + private async _testConnection() { + this._status = undefined; + const result = await testAssistSatelliteConnection( + this.hass, + this.assistEntityId! + ); + this._status = result.status; + } + + private _nextStep() { + fireEvent(this, "next-step", { noPrevious: true }); + } + + private _close() { + fireEvent(this, "closed"); + } + + static styles = AssistantSetupStyles; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-check": HaVoiceAssistantSetupStepCheck; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts new file mode 100644 index 0000000000..84a69f065e --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-cloud.ts @@ -0,0 +1,38 @@ +import { html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import { HomeAssistant } from "../../types"; +import { AssistantSetupStyles } from "./styles"; + +@customElement("ha-voice-assistant-setup-step-cloud") +export class HaVoiceAssistantSetupStepCloud extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + protected override render() { + return html`
+ +

Home Assistant Cloud

+

+ With Home Assistant Cloud, you get the best results for your voice + assistant, sign up for a free trial now. +

+
+ `; + } + + private _close() { + fireEvent(this, "closed"); + } + + static styles = AssistantSetupStyles; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-cloud": HaVoiceAssistantSetupStepCloud; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-pipeline.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-pipeline.ts new file mode 100644 index 0000000000..99204a3736 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-pipeline.ts @@ -0,0 +1,304 @@ +import { mdiOpenInNew } from "@mdi/js"; +import { css, html, LitElement, nothing, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { isComponentLoaded } from "../../common/config/is_component_loaded"; +import { fireEvent } from "../../common/dom/fire_event"; +import { computeDomain } from "../../common/entity/compute_domain"; +import { + createAssistPipeline, + listAssistPipelines, +} from "../../data/assist_pipeline"; +import { AssistSatelliteConfiguration } from "../../data/assist_satellite"; +import { fetchCloudStatus } from "../../data/cloud"; +import { listSTTEngines } from "../../data/stt"; +import { listTTSEngines, listTTSVoices } from "../../data/tts"; +import { HomeAssistant } from "../../types"; +import { documentationUrl } from "../../util/documentation-url"; +import { AssistantSetupStyles } from "./styles"; +import { STEP } from "./voice-assistant-setup-dialog"; + +@customElement("ha-voice-assistant-setup-step-pipeline") +export class HaVoiceAssistantSetupStepPipeline extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + public assistConfiguration?: AssistSatelliteConfiguration; + + @property() public deviceId!: string; + + @property() public assistEntityId?: string; + + @state() private _showFirst = false; + + @state() private _showSecond = false; + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + if (!this.hasUpdated) { + this._checkCloud(); + } + } + + protected override firstUpdated(changedProperties: PropertyValues) { + super.firstUpdated(changedProperties); + setTimeout(() => { + this._showFirst = true; + }, 1); + setTimeout(() => { + this._showSecond = true; + }, 1500); + } + + protected override render() { + return html`
+
+
+ ${!this._showFirst ? "…" : "Turn on the lights in the bedroom"} +
+ ${this._showFirst + ? html`
+ ${!this._showSecond ? "…" : "Turned on the lights"} +
` + : nothing} +
+

Select system

+

+ How quickly your voice assistant responds depends on the power of your + system. +

+
+ + + Home Assistant Cloud + Ideal if you don't have a powerful system at home + + + + On this system + Local setup with the Whisper and Piper add-ons + + + + Use external system + Learn more about how to host it on another system + + + `; + } + + private async _checkCloud() { + if (!isComponentLoaded(this.hass, "cloud")) { + return; + } + const cloudStatus = await fetchCloudStatus(this.hass); + if (!cloudStatus.logged_in || !cloudStatus.active_subscription) { + return; + } + let cloudTtsEntityId; + let cloudSttEntityId; + for (const entity of Object.values(this.hass.entities)) { + if (entity.platform === "cloud") { + const domain = computeDomain(entity.entity_id); + if (domain === "tts") { + cloudTtsEntityId = entity.entity_id; + } else if (domain === "stt") { + cloudSttEntityId = entity.entity_id; + } else { + continue; + } + if (cloudTtsEntityId && cloudSttEntityId) { + break; + } + } + } + const pipelines = await listAssistPipelines(this.hass); + const preferredPipeline = pipelines.pipelines.find( + (pipeline) => pipeline.id === pipelines.preferred_pipeline + ); + + if (preferredPipeline) { + if ( + preferredPipeline.tts_engine === cloudTtsEntityId && + preferredPipeline.stt_engine === cloudSttEntityId + ) { + await this.hass.callService( + "select", + "select_option", + { option: "preferred" }, + { entity_id: this.assistConfiguration?.pipeline_entity_id } + ); + this._nextStep(STEP.SUCCESS); + return; + } + } + + let cloudPipeline = pipelines.pipelines.find( + (pipeline) => + pipeline.tts_engine === cloudTtsEntityId && + pipeline.stt_engine === cloudSttEntityId + ); + + if (!cloudPipeline) { + const ttsEngine = ( + await listTTSEngines( + this.hass, + this.hass.config.language, + this.hass.config.country || undefined + ) + ).providers.find((provider) => provider.engine_id === cloudTtsEntityId); + const ttsVoices = await listTTSVoices( + this.hass, + cloudTtsEntityId, + ttsEngine?.supported_languages![0] || this.hass.config.language + ); + + const sttEngine = ( + await listSTTEngines( + this.hass, + this.hass.config.language, + this.hass.config.country || undefined + ) + ).providers.find((provider) => provider.engine_id === cloudSttEntityId); + + let pipelineName = "Home Assistant Cloud"; + let i = 1; + while ( + pipelines.pipelines.find( + // eslint-disable-next-line @typescript-eslint/no-loop-func + (pipeline) => pipeline.name === pipelineName + ) + ) { + pipelineName = `${pipelineName} ${i}`; + i++; + } + + cloudPipeline = await createAssistPipeline(this.hass, { + name: pipelineName, + language: this.hass.config.language, + conversation_engine: "conversation.home_assistant", + conversation_language: this.hass.config.language, + stt_engine: cloudSttEntityId, + stt_language: sttEngine!.supported_languages![0], + tts_engine: cloudTtsEntityId, + tts_language: ttsEngine!.supported_languages![0], + tts_voice: ttsVoices.voices![0].voice_id, + wake_word_entity: null, + wake_word_id: null, + }); + } + + await this.hass.callService( + "select", + "select_option", + { option: cloudPipeline.name }, + { entity_id: this.assistConfiguration?.pipeline_entity_id } + ); + this._nextStep(STEP.SUCCESS); + } + + private async _setupCloud() { + fireEvent(this, "next-step", { step: STEP.CLOUD }); + } + + private async _thisSystem() { + fireEvent(this, "next-step", { step: STEP.ADDONS }); + } + + private _nextStep(step?: STEP) { + fireEvent(this, "next-step", { step }); + } + + private _close() { + fireEvent(this, "closed"); + } + + static styles = [ + AssistantSetupStyles, + css` + :host { + padding: 0; + } + .padding { + padding: 24px; + } + ha-md-list { + width: 100%; + text-align: initial; + } + + .messages-container { + padding: 24px; + box-sizing: border-box; + height: 152px; + } + .message { + white-space: nowrap; + font-size: 18px; + clear: both; + margin: 8px 0; + padding: 8px; + border-radius: 15px; + height: 36px; + box-sizing: border-box; + overflow: hidden; + text-overflow: ellipsis; + transition: width 1s; + width: 30px; + } + + .message.user { + margin-left: 24px; + margin-inline-start: 24px; + margin-inline-end: initial; + float: var(--float-end); + text-align: right; + border-bottom-right-radius: 0px; + background-color: var(--primary-color); + color: var(--text-primary-color); + direction: var(--direction); + } + + .message.user.show { + width: 295px; + } + + .message.hass { + margin-right: 24px; + margin-inline-end: 24px; + margin-inline-start: initial; + float: var(--float-start); + border-bottom-left-radius: 0px; + background-color: var(--secondary-background-color); + color: var(--primary-text-color); + direction: var(--direction); + } + + .message.hass.show { + width: 184px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-pipeline": HaVoiceAssistantSetupStepPipeline; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts new file mode 100644 index 0000000000..bb97940892 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-success.ts @@ -0,0 +1,226 @@ +import { css, html, LitElement, nothing, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../common/dom/fire_event"; +import { stopPropagation } from "../../common/dom/stop_propagation"; +import "../../components/ha-md-list-item"; +import "../../components/ha-tts-voice-picker"; +import { + AssistPipeline, + listAssistPipelines, + setAssistPipelinePreferred, + updateAssistPipeline, +} from "../../data/assist_pipeline"; +import { + assistSatelliteAnnounce, + AssistSatelliteConfiguration, +} from "../../data/assist_satellite"; +import { fetchCloudStatus } from "../../data/cloud"; +import { showVoiceAssistantPipelineDetailDialog } from "../../panels/config/voice-assistants/show-dialog-voice-assistant-pipeline-detail"; +import "../../panels/lovelace/entity-rows/hui-select-entity-row"; +import { HomeAssistant } from "../../types"; +import { AssistantSetupStyles } from "./styles"; +import { STEP } from "./voice-assistant-setup-dialog"; + +@customElement("ha-voice-assistant-setup-step-success") +export class HaVoiceAssistantSetupStepSuccess extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + public assistConfiguration?: AssistSatelliteConfiguration; + + @property() public deviceId!: string; + + @property() public assistEntityId?: string; + + @state() private _ttsSettings?: any; + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + if (changedProperties.has("assistConfiguration")) { + this._setTtsSettings(); + return; + } + if (changedProperties.has("hass") && this.assistConfiguration) { + const oldHass = changedProperties.get("hass") as this["hass"] | undefined; + if (oldHass) { + const oldState = + oldHass.states[this.assistConfiguration.pipeline_entity_id]; + const newState = + this.hass.states[this.assistConfiguration.pipeline_entity_id]; + if (oldState.state !== newState.state) { + this._setTtsSettings(); + } + } + } + } + + private _activeWakeWord = memoizeOne( + (config: AssistSatelliteConfiguration | undefined) => { + if (!config) { + return ""; + } + const activeId = config.active_wake_words[0]; + return config.available_wake_words.find((ww) => ww.id === activeId) + ?.wake_word; + } + ); + + protected override render() { + return html`
+ +

Ready to assist!

+

+ Make your assistant more personal by customizing shizzle to the + manizzle +

+ + Change wake word + ${this._activeWakeWord(this.assistConfiguration)} + + + + ${this._ttsSettings + ? html`` + : nothing} +
+ `; + } + + private async _getPipeline(): Promise< + [AssistPipeline | undefined, string | undefined | null] + > { + if (!this.assistConfiguration?.pipeline_entity_id) { + return [undefined, undefined]; + } + + const pipelineName = + this.hass.states[this.assistConfiguration?.pipeline_entity_id].state; + + const pipelines = await listAssistPipelines(this.hass); + + let pipeline: AssistPipeline | undefined; + + if (pipelineName === "preferred") { + pipeline = pipelines.pipelines.find( + (ppln) => ppln.id === pipelines.preferred_pipeline + ); + } else { + pipeline = pipelines.pipelines.find((ppln) => ppln.name === pipelineName); + } + return [pipeline, pipelines.preferred_pipeline]; + } + + private async _setTtsSettings() { + const [pipeline] = await this._getPipeline(); + if (!pipeline) { + this._ttsSettings = undefined; + return; + } + this._ttsSettings = { + engine: pipeline.tts_engine, + voice: pipeline.tts_voice, + language: pipeline.tts_language, + }; + } + + private async _voicePicked(ev) { + const [pipeline] = await this._getPipeline(); + + if (!pipeline) { + return; + } + + await updateAssistPipeline(this.hass, pipeline.id, { + ...pipeline, + tts_voice: ev.detail.value, + }); + this._announce("Hello, how can I help you?"); + } + + private async _announce(message: string) { + if (!this.assistEntityId) { + return; + } + await assistSatelliteAnnounce(this.hass, this.assistEntityId, message); + } + + private _changeWakeWord() { + fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD }); + } + + private async _openPipeline() { + const [pipeline, preferred_pipeline] = await this._getPipeline(); + + if (!pipeline) { + return; + } + + const cloudStatus = await fetchCloudStatus(this.hass); + + showVoiceAssistantPipelineDetailDialog(this, { + cloudActiveSubscription: + cloudStatus.logged_in && cloudStatus.active_subscription, + pipeline, + preferred: pipeline.id === preferred_pipeline, + updatePipeline: async (values) => { + await updateAssistPipeline(this.hass!, pipeline!.id, values); + }, + setPipelinePreferred: async () => { + await setAssistPipelinePreferred(this.hass!, pipeline!.id); + }, + hideWakeWord: true, + }); + } + + private _close() { + fireEvent(this, "closed"); + } + + static styles = [ + AssistantSetupStyles, + css` + ha-md-list-item { + text-align: initial; + } + ha-tts-voice-picker { + margin-top: 16px; + display: block; + } + .footer { + margin-top: 24px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-success": HaVoiceAssistantSetupStepSuccess; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts new file mode 100644 index 0000000000..14add40019 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-update.ts @@ -0,0 +1,119 @@ +import { css, html, LitElement, PropertyValues } from "lit"; +import { customElement, property } from "lit/decorators"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-circular-progress"; +import { UNAVAILABLE } from "../../data/entity"; +import { HomeAssistant } from "../../types"; +import { AssistantSetupStyles } from "./styles"; + +@customElement("ha-voice-assistant-setup-step-update") +export class HaVoiceAssistantSetupStepUpdate extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public updateEntityId?: string; + + private _updated = false; + + protected override willUpdate(changedProperties: PropertyValues): void { + super.willUpdate(changedProperties); + + if (changedProperties.has("hass") && this.updateEntityId) { + const oldHass = changedProperties.get("hass") as this["hass"] | undefined; + if (oldHass) { + const oldState = oldHass.states[this.updateEntityId]; + const newState = this.hass.states[this.updateEntityId]; + if ( + oldState?.state === UNAVAILABLE && + newState?.state !== UNAVAILABLE + ) { + // Device is rebooted, let's move on + this._tryUpdate(); + } + } + } + + if (!changedProperties.has("updateEntityId")) { + return; + } + + if (!this.updateEntityId) { + this._nextStep(); + return; + } + + this._tryUpdate(); + } + + protected override render() { + const stateObj = this.hass.states[this.updateEntityId!]; + + const progressIsNumeric = + typeof stateObj?.attributes.in_progress === "number"; + + return html`
+ +

Updating your voice assistant

+

+ We are making sure you have the latest and greatest version of your + voice assistant. This may take a few minutes. +

+ +

+ ${stateObj.state === "unavailable" + ? "Restarting voice assistant" + : progressIsNumeric + ? `Installing ${stateObj.attributes.in_progress}%` + : ""} +

+
`; + } + + private async _tryUpdate() { + if (!this.updateEntityId) { + return; + } + const updateEntity = this.hass.states[this.updateEntityId]; + if ( + updateEntity && + this.hass.states[updateEntity.entity_id].state === "on" + ) { + this._updated = true; + await this.hass.callService( + "update", + "install", + {}, + { entity_id: updateEntity.entity_id } + ); + } else { + this._nextStep(); + } + } + + private _nextStep() { + fireEvent(this, "next-step", { + noPrevious: true, + updateConfig: this._updated, + }); + } + + static styles = [ + AssistantSetupStyles, + css` + ha-circular-progress { + margin-top: 24px; + margin-bottom: 24px; + } + `, + ]; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-update": HaVoiceAssistantSetupStepUpdate; + } +} diff --git a/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-wake-word.ts b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-wake-word.ts new file mode 100644 index 0000000000..4c9b9306a0 --- /dev/null +++ b/src/dialogs/voice-assistant-setup/voice-assistant-setup-step-wake-word.ts @@ -0,0 +1,129 @@ +import { UnsubscribeFunc } from "home-assistant-js-websocket"; +import { html, LitElement, nothing, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { fireEvent } from "../../common/dom/fire_event"; +import "../../components/ha-button"; +import "../../components/ha-dialog-header"; +import { + AssistSatelliteConfiguration, + interceptWakeWord, +} from "../../data/assist_satellite"; +import { HomeAssistant } from "../../types"; +import { AssistantSetupStyles } from "./styles"; +import { STEP } from "./voice-assistant-setup-dialog"; + +@customElement("ha-voice-assistant-setup-step-wake-word") +export class HaVoiceAssistantSetupStepWakeWord extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ attribute: false }) + public assistConfiguration?: AssistSatelliteConfiguration; + + @property() public assistEntityId?: string; + + @state() private _detected = false; + + private _sub?: Promise; + + disconnectedCallback(): void { + super.disconnectedCallback(); + this._stopListeningWakeWord(); + } + + protected override willUpdate(changedProperties: PropertyValues) { + super.willUpdate(changedProperties); + + if (changedProperties.has("assistEntityId")) { + this._detected = false; + this._listenWakeWord(); + } + } + + private _activeWakeWord = memoizeOne( + (config: AssistSatelliteConfiguration | undefined) => { + if (!config) { + return ""; + } + const activeId = config.active_wake_words[0]; + return config.available_wake_words.find((ww) => ww.id === activeId) + ?.wake_word; + } + ); + + protected override render() { + if (!this.assistEntityId) { + return nothing; + } + + const entityState = this.hass.states[this.assistEntityId]; + + if (entityState.state !== "listening_wake_word") { + return html``; + } + + return html`
+ ${!this._detected + ? html` + +

+ Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the + device up +

+

Setup will continue once the device is awake.

+
` + : html` +

+ Say “${this._activeWakeWord(this.assistConfiguration)}” again +

+

+ To make sure the wake word works for you. +

`} + + `; + } + + private async _listenWakeWord() { + const entityId = this.assistEntityId; + if (!entityId) { + return; + } + await this._stopListeningWakeWord(); + this._sub = interceptWakeWord(this.hass, entityId, () => { + this._stopListeningWakeWord(); + if (this._detected) { + this._nextStep(); + } else { + this._detected = true; + this._listenWakeWord(); + } + }); + } + + private async _stopListeningWakeWord() { + try { + (await this._sub)?.(); + } catch (_e) { + // ignore + } + this._sub = undefined; + } + + private _nextStep() { + fireEvent(this, "next-step"); + } + + private _changeWakeWord() { + fireEvent(this, "next-step", { step: STEP.CHANGE_WAKEWORD }); + } + + static styles = AssistantSetupStyles; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-voice-assistant-setup-step-wake-word": HaVoiceAssistantSetupStepWakeWord; + } +} diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 60bc1c78bf..ef6e98f2b3 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -5,6 +5,7 @@ import { mdiDelete, mdiDotsVertical, mdiDownload, + mdiMicrophone, mdiOpenInNew, mdiPencil, mdiPlusCircle, @@ -82,6 +83,8 @@ import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "./device-registry-detail/show-dialog-device-registry-detail"; +import { showVoiceAssistantSetupDialog } from "../../../dialogs/voice-assistant-setup/show-voice-assistant-setup-dialog"; +import { assistSatelliteSupportsSetupFlow } from "../../../data/assist_satellite"; export interface EntityRegistryStateEntry extends EntityRegistryEntry { stateName?: string | null; @@ -1062,6 +1065,25 @@ export class HaConfigDevicePage extends LitElement { }); } + const entities = this._entities(this.deviceId, this._entityReg); + + const assistSatellite = entities.find( + (ent) => computeDomain(ent.entity_id) === "assist_satellite" + ); + + if ( + assistSatellite && + assistSatelliteSupportsSetupFlow( + this.hass.states[assistSatellite.entity_id] + ) + ) { + deviceActions.push({ + action: this._voiceAssistantSetup, + label: "Set up voice assistant", + icon: mdiMicrophone, + }); + } + const domains = this._integrations( device, this.entries, @@ -1396,6 +1418,12 @@ export class HaConfigDevicePage extends LitElement { (ev.currentTarget as any).action(ev); } + private _voiceAssistantSetup = () => { + showVoiceAssistantSetupDialog(this, { + deviceId: this.deviceId, + }); + }; + static get styles(): CSSResultGroup { return [ haStyle, diff --git a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts index 14f4f804c4..fca855ec9d 100644 --- a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts +++ b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts @@ -215,14 +215,16 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { keys="tts_engine,tts_language,tts_voice" @value-changed=${this._valueChanged} > - + ${this._params.hideWakeWord + ? nothing + : html``} - ${this._params.pipeline?.id + ${this._params.pipeline?.id && this._params.deletePipeline ? html` Promise; + hideWakeWord?: boolean; updatePipeline: (updates: AssistPipelineMutableParams) => Promise; setPipelinePreferred: () => Promise; - deletePipeline: () => Promise; + createPipeline?: (values: AssistPipelineMutableParams) => Promise; + deletePipeline?: () => Promise; } export const loadVoiceAssistantPipelineDetailDialog = () =>