mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +00:00
Add MVP voice assist flow (#22061)
* Add MVP voice assist flow * filter on supported features * check for unavailable * Update step-flow-create-entry.ts
This commit is contained in:
parent
813feff12e
commit
305cecb213
BIN
public/static/icons/casita/loading.png
Normal file
BIN
public/static/icons/casita/loading.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 3.9 KiB |
BIN
public/static/icons/casita/loving.png
Normal file
BIN
public/static/icons/casita/loving.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.5 KiB |
BIN
public/static/icons/casita/normal.png
Normal file
BIN
public/static/icons/casita/normal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.6 KiB |
BIN
public/static/icons/casita/sad.png
Normal file
BIN
public/static/icons/casita/sad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.3 KiB |
BIN
public/static/icons/casita/sleeping.png
Normal file
BIN
public/static/icons/casita/sleeping.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
BIN
public/static/icons/casita/smiling.png
Normal file
BIN
public/static/icons/casita/smiling.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 4.4 KiB |
81
src/data/assist_satellite.ts
Normal file
81
src/data/assist_satellite.ts
Normal file
@ -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<AssistSatelliteConfiguration>({
|
||||
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);
|
@ -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;
|
||||
|
||||
|
@ -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,
|
||||
});
|
||||
};
|
36
src/dialogs/voice-assistant-setup/styles.ts
Normal file
36
src/dialogs/voice-assistant-setup/styles.ts
Normal file
@ -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%;
|
||||
}
|
||||
`,
|
||||
];
|
@ -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<void> {
|
||||
this._params = params;
|
||||
|
||||
await this._fetchAssistConfiguration();
|
||||
|
||||
this._step = STEP.UPDATE;
|
||||
}
|
||||
|
||||
public async closeDialog(): Promise<void> {
|
||||
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`
|
||||
<ha-dialog
|
||||
open
|
||||
@closed=${this._dialogClosed}
|
||||
.heading=${"Voice Satellite setup"}
|
||||
hideActions
|
||||
>
|
||||
<ha-dialog-header slot="heading">
|
||||
${this._previousSteps.length
|
||||
? html`<ha-icon-button
|
||||
slot="navigationIcon"
|
||||
.label=${this.hass.localize("ui.dialogs.generic.close") ??
|
||||
"Close"}
|
||||
.path=${mdiChevronLeft}
|
||||
@click=${this._goToPreviousStep}
|
||||
></ha-icon-button>`
|
||||
: nothing}
|
||||
</ha-dialog-header>
|
||||
<div class="content" @next-step=${this._nextStep}>
|
||||
${this._step === STEP.UPDATE
|
||||
? html`<ha-voice-assistant-setup-step-update
|
||||
.hass=${this.hass}
|
||||
.updateEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"update"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-update>`
|
||||
: assistEntityState?.state === UNAVAILABLE
|
||||
? html`Your voice assistant is not available.`
|
||||
: this._step === STEP.CHECK
|
||||
? html`<ha-voice-assistant-setup-step-check
|
||||
.hass=${this.hass}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-check>`
|
||||
: this._step === STEP.WAKEWORD
|
||||
? html`<ha-voice-assistant-setup-step-wake-word
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-wake-word>`
|
||||
: this._step === STEP.CHANGE_WAKEWORD
|
||||
? html`
|
||||
<ha-voice-assistant-setup-step-change-wake-word
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-change-wake-word>
|
||||
`
|
||||
: this._step === STEP.AREA
|
||||
? html`
|
||||
<ha-voice-assistant-setup-step-area
|
||||
.hass=${this.hass}
|
||||
.deviceId=${this._params.deviceId}
|
||||
></ha-voice-assistant-setup-step-area>
|
||||
`
|
||||
: this._step === STEP.PIPELINE
|
||||
? html`<ha-voice-assistant-setup-step-pipeline
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-pipeline>`
|
||||
: this._step === STEP.CLOUD
|
||||
? html`<ha-voice-assistant-setup-step-cloud
|
||||
.hass=${this.hass}
|
||||
></ha-voice-assistant-setup-step-cloud>`
|
||||
: this._step === STEP.ADDONS
|
||||
? html`<ha-voice-assistant-setup-step-addons
|
||||
.hass=${this.hass}
|
||||
></ha-voice-assistant-setup-step-addons>`
|
||||
: this._step === STEP.SUCCESS
|
||||
? html`<ha-voice-assistant-setup-step-success
|
||||
.hass=${this.hass}
|
||||
.assistConfiguration=${this
|
||||
._assistConfiguration}
|
||||
.assistEntityId=${this._findDomainEntityId(
|
||||
this._params.deviceId,
|
||||
this.hass.entities,
|
||||
"assist_satellite"
|
||||
)}
|
||||
></ha-voice-assistant-setup-step-success>`
|
||||
: nothing}
|
||||
</div>
|
||||
</ha-dialog>
|
||||
`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`<div class="content">
|
||||
<h1>Local</h1>
|
||||
<p class="secondary">
|
||||
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.
|
||||
</p>
|
||||
<h3>Home Assistant Cloud:</h3>
|
||||
<div class="messages-container cloud">
|
||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showFirst
|
||||
? html`<div class="timing user">0.2 seconds</div>`
|
||||
: nothing}
|
||||
${this._showFirst
|
||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showSecond
|
||||
? html`<div class="timing hass">0.4 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<h3>Raspberry Pi 4:</h3>
|
||||
<div class="messages-container rpi">
|
||||
<div class="message user ${this._showThird ? "show" : ""}">
|
||||
${!this._showThird ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showThird
|
||||
? html`<div class="timing user">3 seconds</div>`
|
||||
: nothing}
|
||||
${this._showThird
|
||||
? html`<div class="message hass ${this._showFourth ? "show" : ""}">
|
||||
${!this._showFourth ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
${this._showFourth
|
||||
? html`<div class="timing hass">5 seconds</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/voice_control/voice_remote_local_assistant/"
|
||||
)}
|
||||
target="_blank"
|
||||
rel="noreferrer noopenner"
|
||||
@click=${this._close}
|
||||
><ha-button unelevated
|
||||
>Learn how to setup local assistant</ha-button
|
||||
></a
|
||||
>
|
||||
<ha-button @click=${this._skip}
|
||||
>I already have a local assistant</ha-button
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Select area</h1>
|
||||
<p class="secondary">
|
||||
When you voice assistant knows where it is, it can better control the
|
||||
devices around it.
|
||||
</p>
|
||||
<ha-area-picker
|
||||
.hass=${this.hass}
|
||||
.value=${device.area_id}
|
||||
></ha-area-picker>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._setArea}>Next</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`<div class="padding content">
|
||||
<img src="/static/icons/casita/smiling.png" />
|
||||
<h1>Change wake word</h1>
|
||||
<p class="secondary">
|
||||
When you voice assistant knows where it is, it can better control the
|
||||
devices around it.
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
${this.assistConfiguration!.available_wake_words.map(
|
||||
(wakeWord) =>
|
||||
html`<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
@click=${this._wakeWordPicked}
|
||||
.value=${wakeWord.id}
|
||||
>
|
||||
${wakeWord.wake_word}
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>`
|
||||
)}
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`<div class="content">
|
||||
${this._status === "success"
|
||||
? html`<img src="/static/icons/casita/smiling.png" />
|
||||
<h1>Hi</h1>
|
||||
<p class="secondary">
|
||||
With a couple of steps we are going to setup your voice assistant.
|
||||
</p>`
|
||||
: this._status === "timeout"
|
||||
? html`<img src="/static/icons/casita/sad.png" />
|
||||
<h1>Error</h1>
|
||||
<p class="secondary">
|
||||
Your device was unable to reach Home Assistant. Make sure you
|
||||
have setup your
|
||||
<a href="/config/network" @click=${this._close}
|
||||
>Home Assistant URL's</a
|
||||
>
|
||||
correctly.
|
||||
</p>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._testConnection}>Retry</ha-button>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/loading.png" />
|
||||
<h1>Checking...</h1>
|
||||
<p class="secondary">
|
||||
We are checking if the device can reach your Home Assistant
|
||||
instance.
|
||||
</p>
|
||||
<ha-circular-progress indeterminate></ha-circular-progress>`}
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Home Assistant Cloud</h1>
|
||||
<p class="secondary">
|
||||
With Home Assistant Cloud, you get the best results for your voice
|
||||
assistant, sign up for a free trial now.
|
||||
</p>
|
||||
</div>
|
||||
<div class="footer">
|
||||
<a href="/config/cloud/register" @click=${this._close}
|
||||
><ha-button>Start your free trial</ha-button></a
|
||||
>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
private _close() {
|
||||
fireEvent(this, "closed");
|
||||
}
|
||||
|
||||
static styles = AssistantSetupStyles;
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"ha-voice-assistant-setup-step-cloud": HaVoiceAssistantSetupStepCloud;
|
||||
}
|
||||
}
|
@ -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`<div class="padding content">
|
||||
<div class="messages-container">
|
||||
<div class="message user ${this._showFirst ? "show" : ""}">
|
||||
${!this._showFirst ? "…" : "Turn on the lights in the bedroom"}
|
||||
</div>
|
||||
${this._showFirst
|
||||
? html` <div class="message hass ${this._showSecond ? "show" : ""}">
|
||||
${!this._showSecond ? "…" : "Turned on the lights"}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<h1>Select system</h1>
|
||||
<p class="secondary">
|
||||
How quickly your voice assistant responds depends on the power of your
|
||||
system.
|
||||
</p>
|
||||
</div>
|
||||
<ha-md-list>
|
||||
<ha-md-list-item interactive type="button" @click=${this._setupCloud}>
|
||||
Home Assistant Cloud
|
||||
<span slot="supporting-text"
|
||||
>Ideal if you don't have a powerful system at home</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item interactive type="button" @click=${this._thisSystem}>
|
||||
On this system
|
||||
<span slot="supporting-text"
|
||||
>Local setup with the Whisper and Piper add-ons</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="link"
|
||||
href=${documentationUrl(
|
||||
this.hass,
|
||||
"/voice_control/voice_remote_local_assistant/"
|
||||
)}
|
||||
rel="noreferrer noopenner"
|
||||
target="_blank"
|
||||
@click=${this._close}
|
||||
>
|
||||
Use external system
|
||||
<span slot="supporting-text"
|
||||
>Learn more about how to host it on another system</span
|
||||
>
|
||||
<ha-svg-icon slot="end" .path=${mdiOpenInNew}></ha-svg-icon>
|
||||
</ha-md-list-item>
|
||||
</ha-md-list>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`<div class="content">
|
||||
<img src="/static/icons/casita/loving.png" />
|
||||
<h1>Ready to assist!</h1>
|
||||
<p class="secondary">
|
||||
Make your assistant more personal by customizing shizzle to the
|
||||
manizzle
|
||||
</p>
|
||||
<ha-md-list-item
|
||||
interactive
|
||||
type="button"
|
||||
@click=${this._changeWakeWord}
|
||||
>
|
||||
Change wake word
|
||||
<span slot="supporting-text"
|
||||
>${this._activeWakeWord(this.assistConfiguration)}</span
|
||||
>
|
||||
<ha-icon-next slot="end"></ha-icon-next>
|
||||
</ha-md-list-item>
|
||||
<hui-select-entity-row
|
||||
.hass=${this.hass}
|
||||
._config=${{
|
||||
entity: this.assistConfiguration?.pipeline_entity_id,
|
||||
}}
|
||||
></hui-select-entity-row>
|
||||
${this._ttsSettings
|
||||
? html`<ha-tts-voice-picker
|
||||
.hass=${this.hass}
|
||||
required
|
||||
.engineId=${this._ttsSettings.engine}
|
||||
.language=${this._ttsSettings.language}
|
||||
.value=${this._ttsSettings.voice}
|
||||
@value-changed=${this._voicePicked}
|
||||
@closed=${stopPropagation}
|
||||
></ha-tts-voice-picker>`
|
||||
: nothing}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._openPipeline}
|
||||
>Change assistant settings</ha-button
|
||||
>
|
||||
<ha-button @click=${this._close} unelevated>Done</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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`<div class="content">
|
||||
<img src="/static/icons/casita/loading.png" />
|
||||
<h1>Updating your voice assistant</h1>
|
||||
<p class="secondary">
|
||||
We are making sure you have the latest and greatest version of your
|
||||
voice assistant. This may take a few minutes.
|
||||
</p>
|
||||
<ha-circular-progress
|
||||
.value=${progressIsNumeric
|
||||
? stateObj.attributes.in_progress / 100
|
||||
: undefined}
|
||||
.indeterminate=${!progressIsNumeric}
|
||||
></ha-circular-progress>
|
||||
<p>
|
||||
${stateObj.state === "unavailable"
|
||||
? "Restarting voice assistant"
|
||||
: progressIsNumeric
|
||||
? `Installing ${stateObj.attributes.in_progress}%`
|
||||
: ""}
|
||||
</p>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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<UnsubscribeFunc>;
|
||||
|
||||
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`<ha-circular-progress indeterminate></ha-circular-progress>`;
|
||||
}
|
||||
|
||||
return html`<div class="content">
|
||||
${!this._detected
|
||||
? html`
|
||||
<img src="/static/icons/casita/sleeping.png" />
|
||||
<h1>
|
||||
Say “${this._activeWakeWord(this.assistConfiguration)}” to wake the
|
||||
device up
|
||||
</h1>
|
||||
<p class="secondary">Setup will continue once the device is awake.</p>
|
||||
</div>`
|
||||
: html`<img src="/static/icons/casita/normal.png" />
|
||||
<h1>
|
||||
Say “${this._activeWakeWord(this.assistConfiguration)}” again
|
||||
</h1>
|
||||
<p class="secondary">
|
||||
To make sure the wake word works for you.
|
||||
</p>`}
|
||||
</div>
|
||||
<div class="footer">
|
||||
<ha-button @click=${this._changeWakeWord}>Change wake word</ha-button>
|
||||
</div>`;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
@ -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,
|
||||
|
@ -215,14 +215,16 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
keys="tts_engine,tts_language,tts_voice"
|
||||
@value-changed=${this._valueChanged}
|
||||
></assist-pipeline-detail-tts>
|
||||
<assist-pipeline-detail-wakeword
|
||||
.hass=${this.hass}
|
||||
.data=${this._data}
|
||||
keys="wake_word_entity,wake_word_id"
|
||||
@value-changed=${this._valueChanged}
|
||||
></assist-pipeline-detail-wakeword>
|
||||
${this._params.hideWakeWord
|
||||
? nothing
|
||||
: html`<assist-pipeline-detail-wakeword
|
||||
.hass=${this.hass}
|
||||
.data=${this._data}
|
||||
keys="wake_word_entity,wake_word_id"
|
||||
@value-changed=${this._valueChanged}
|
||||
></assist-pipeline-detail-wakeword>`}
|
||||
</div>
|
||||
${this._params.pipeline?.id
|
||||
${this._params.pipeline?.id && this._params.deletePipeline
|
||||
? html`
|
||||
<ha-button
|
||||
slot="secondaryAction"
|
||||
@ -283,8 +285,11 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
};
|
||||
if (this._params!.pipeline?.id) {
|
||||
await this._params!.updatePipeline(values);
|
||||
} else {
|
||||
} else if (this._params!.createPipeline) {
|
||||
await this._params!.createPipeline(values);
|
||||
} else {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("No createPipeline function provided");
|
||||
}
|
||||
this.closeDialog();
|
||||
} catch (err: any) {
|
||||
@ -313,6 +318,9 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
|
||||
}
|
||||
|
||||
private async _deletePipeline() {
|
||||
if (!this._params?.deletePipeline) {
|
||||
return;
|
||||
}
|
||||
this._submitting = true;
|
||||
try {
|
||||
if (await this._params!.deletePipeline()) {
|
||||
|
@ -8,10 +8,11 @@ export interface VoiceAssistantPipelineDetailsDialogParams {
|
||||
cloudActiveSubscription?: boolean;
|
||||
pipeline?: AssistPipeline;
|
||||
preferred?: boolean;
|
||||
createPipeline: (values: AssistPipelineMutableParams) => Promise<unknown>;
|
||||
hideWakeWord?: boolean;
|
||||
updatePipeline: (updates: AssistPipelineMutableParams) => Promise<unknown>;
|
||||
setPipelinePreferred: () => Promise<unknown>;
|
||||
deletePipeline: () => Promise<boolean>;
|
||||
createPipeline?: (values: AssistPipelineMutableParams) => Promise<unknown>;
|
||||
deletePipeline?: () => Promise<boolean>;
|
||||
}
|
||||
|
||||
export const loadVoiceAssistantPipelineDetailDialog = () =>
|
||||
|
Loading…
x
Reference in New Issue
Block a user