From c567a61dd72efbab2fabda32665ccbc53a000992 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Tue, 26 Sep 2023 18:17:45 +0200 Subject: [PATCH] Add wake word to assist pipeline settings (#18019) * Add wake word to assist pipeline settings * Update assist-pipeline-detail-wakeword.ts * Add icon for wake word domain * format state * implement `wake_word/info` command --- src/common/const.ts | 2 + src/common/entity/compute_state_display.ts | 1 + src/data/assist_pipeline.ts | 4 + src/data/wake_word.ts | 12 ++ .../assist-pipeline-detail-wakeword.ts | 138 ++++++++++++++++++ .../dialog-voice-assistant-pipeline-detail.ts | 9 ++ src/translations/en.json | 8 +- 7 files changed, 173 insertions(+), 1 deletion(-) create mode 100644 src/data/wake_word.ts create mode 100644 src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-wakeword.ts diff --git a/src/common/const.ts b/src/common/const.ts index 81119ca16e62..daead2ada95f 100644 --- a/src/common/const.ts +++ b/src/common/const.ts @@ -15,6 +15,7 @@ import { mdiCalendarClock, mdiCarCoolantLevel, mdiCash, + mdiChatSleep, mdiClock, mdiCloudUpload, mdiCog, @@ -124,6 +125,7 @@ export const FIXED_DOMAIN_ICONS = { tts: mdiSpeakerMessage, updater: mdiCloudUpload, vacuum: mdiRobotVacuum, + wake_word: mdiChatSleep, zone: mdiMapMarkerRadius, }; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index 4baad8357d8e..fdcb95f52391 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -193,6 +193,7 @@ export const computeStateDisplayFromEntityAttributes = ( "scene", "stt", "tts", + "wake_word", ].includes(domain) || (domain === "sensor" && attributes.device_class === "timestamp") ) { diff --git a/src/data/assist_pipeline.ts b/src/data/assist_pipeline.ts index 18cfab3cc571..3ee5a8a27a0c 100644 --- a/src/data/assist_pipeline.ts +++ b/src/data/assist_pipeline.ts @@ -14,6 +14,8 @@ export interface AssistPipeline { tts_engine: string | null; tts_language: string | null; tts_voice: string | null; + wake_word_entity: string | null; + wake_word_id: string | null; } export interface AssistPipelineMutableParams { @@ -26,6 +28,8 @@ export interface AssistPipelineMutableParams { tts_engine: string | null; tts_language: string | null; tts_voice: string | null; + wake_word_entity: string | null; + wake_word_id: string | null; } export interface assistRunListing { diff --git a/src/data/wake_word.ts b/src/data/wake_word.ts new file mode 100644 index 000000000000..563fde271e53 --- /dev/null +++ b/src/data/wake_word.ts @@ -0,0 +1,12 @@ +import type { HomeAssistant } from "../types"; + +export interface WakeWord { + id: string; + name: string; +} + +export const fetchWakeWordInfo = (hass: HomeAssistant, entity_id: string) => + hass.callWS<{ wake_words: WakeWord[] }>({ + type: "wake_word/info", + entity_id, + }); diff --git a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-wakeword.ts b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-wakeword.ts new file mode 100644 index 000000000000..01480f5441c3 --- /dev/null +++ b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-wakeword.ts @@ -0,0 +1,138 @@ +import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; +import { customElement, property, state } from "lit/decorators"; +import memoizeOne from "memoize-one"; +import { LocalizeKeys } from "../../../../common/translations/localize"; +import "../../../../components/ha-form/ha-form"; +import { AssistPipeline } from "../../../../data/assist_pipeline"; +import { HomeAssistant } from "../../../../types"; +import { fetchWakeWordInfo, WakeWord } from "../../../../data/wake_word"; + +@customElement("assist-pipeline-detail-wakeword") +export class AssistPipelineDetailWakeWord extends LitElement { + @property({ attribute: false }) public hass!: HomeAssistant; + + @property() public data?: Partial; + + @state() private _wakeWords?: WakeWord[]; + + private _schema = memoizeOne( + (wakeWords?: WakeWord[]) => + [ + { + name: "", + type: "grid", + schema: [ + { + name: "wake_word_entity", + selector: { + entity: { + domain: "wake_word", + }, + }, + }, + wakeWords?.length + ? { + name: "wake_word_id", + required: true, + selector: { + select: { + options: wakeWords.map((ww) => ({ + value: ww.id, + label: ww.name, + })), + }, + }, + } + : { name: "", type: "constant" }, + ] as const, + }, + ] as const + ); + + private _computeLabel = (schema): string => + schema.name + ? this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys + ) + : ""; + + protected willUpdate(changedProps: PropertyValues) { + if ( + changedProps.has("data") && + changedProps.get("data")?.wake_word_entity !== this.data?.wake_word_entity + ) { + this._fetchWakeWords(); + } + } + + protected render() { + return html` +
+
+
+

+ ${this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.wakeword.title` + )} +

+

+ ${this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.steps.wakeword.description` + )} +

+
+ +
+
+ `; + } + + private async _fetchWakeWords() { + if (!this.data?.wake_word_entity) { + this._wakeWords = undefined; + return; + } + this._wakeWords = ( + await fetchWakeWordInfo(this.hass, this.data.wake_word_entity) + ).wake_words; + } + + static get styles(): CSSResultGroup { + return css` + .section { + border: 1px solid var(--divider-color); + border-radius: 8px; + } + .content { + padding: 16px; + } + .intro { + margin-bottom: 16px; + } + h3 { + font-weight: normal; + font-size: 22px; + line-height: 28px; + margin-top: 0; + margin-bottom: 4px; + } + p { + color: var(--secondary-text-color); + font-size: var(--mdc-typography-body2-font-size, 0.875rem); + margin-top: 0; + margin-bottom: 0; + } + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "assist-pipeline-detail-wakeword": AssistPipelineDetailWakeWord; + } +} 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 e8e60b561140..08d1cadfc387 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 @@ -25,6 +25,7 @@ import "./assist-pipeline-detail/assist-pipeline-detail-config"; import "./assist-pipeline-detail/assist-pipeline-detail-conversation"; import "./assist-pipeline-detail/assist-pipeline-detail-stt"; import "./assist-pipeline-detail/assist-pipeline-detail-tts"; +import "./assist-pipeline-detail/assist-pipeline-detail-wakeword"; import "./debug/assist-render-pipeline-events"; import { VoiceAssistantPipelineDetailsDialogParams } from "./show-dialog-voice-assistant-pipeline-detail"; @@ -192,6 +193,12 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { keys="tts_engine,tts_language,tts_voice" @value-changed=${this._valueChanged} > + ${this._params.pipeline?.id ? html` @@ -249,6 +256,8 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { tts_engine: data.tts_engine ?? null, tts_language: data.tts_language ?? null, tts_voice: data.tts_voice ?? null, + wake_word_entity: data.wake_word_entity ?? null, + wake_word_id: data.wake_word_id ?? null, }; if (this._params!.pipeline?.id) { await this._params!.updatePipeline(values); diff --git a/src/translations/en.json b/src/translations/en.json index a8760f02b307..0b3b957c8d80 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2173,7 +2173,9 @@ "stt_language": "[%key:ui::panel::config::voice_assistants::assistants::pipeline::detail::form::language%]", "tts_engine": "Text-to-speech", "tts_language": "[%key:ui::panel::config::voice_assistants::assistants::pipeline::detail::form::language%]", - "tts_voice": "Voice" + "tts_voice": "Voice", + "wake_word_entity": "Wake word engine", + "wake_word_id": "Wake word" }, "steps": { "config": { @@ -2191,6 +2193,10 @@ "tts": { "title": "Text-to-speech", "description": "When you are controlling your assistant with voice, the text-to-speech engine turns the conversation text responses into audio." + }, + "wakeword": { + "title": "Wake word", + "description": "If a device supports wake words, you can activate Assist by saying this word." } }, "no_cloud_message": "You should have an active cloud subscription to use cloud speech services.",