mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-29 04:06:35 +00:00
485 lines
14 KiB
TypeScript
485 lines
14 KiB
TypeScript
import type { PropertyValues } from "lit";
|
|
import { css, html, LitElement, nothing } from "lit";
|
|
import { customElement, property, state } from "lit/decorators";
|
|
import memoizeOne from "memoize-one";
|
|
import { isComponentLoaded } from "../../common/config/is_component_loaded";
|
|
import { fireEvent } from "../../common/dom/fire_event";
|
|
import { computeDomain } from "../../common/entity/compute_domain";
|
|
import { formatLanguageCode } from "../../common/language/format_language";
|
|
import type { LocalizeFunc } from "../../common/translations/localize";
|
|
import "../../components/ha-select-box";
|
|
import type { SelectBoxOption } from "../../components/ha-select-box";
|
|
import {
|
|
createAssistPipeline,
|
|
listAssistPipelines,
|
|
} from "../../data/assist_pipeline";
|
|
import type { AssistSatelliteConfiguration } from "../../data/assist_satellite";
|
|
import { fetchCloudStatus } from "../../data/cloud";
|
|
import type { LanguageScore, LanguageScores } from "../../data/conversation";
|
|
import { getLanguageScores, listAgents } from "../../data/conversation";
|
|
import { listSTTEngines } from "../../data/stt";
|
|
import { listTTSEngines, listTTSVoices } from "../../data/tts";
|
|
import type { HomeAssistant } from "../../types";
|
|
import { AssistantSetupStyles } from "./styles";
|
|
import { STEP } from "./voice-assistant-setup-dialog";
|
|
import { documentationUrl } from "../../util/documentation-url";
|
|
|
|
const OPTIONS = ["cloud", "focused_local", "full_local"] as const;
|
|
|
|
const EMPTY_SCORE: LanguageScore = {
|
|
cloud: 0,
|
|
focused_local: 0,
|
|
full_local: 0,
|
|
};
|
|
|
|
@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({ attribute: false }) public deviceId!: string;
|
|
|
|
@property({ attribute: false }) public assistEntityId?: string;
|
|
|
|
@property({ attribute: false }) public language?: string;
|
|
|
|
@property({ attribute: false }) public languages: string[] = [];
|
|
|
|
@state() private _cloudChecked = false;
|
|
|
|
@state() private _value?: (typeof OPTIONS)[number];
|
|
|
|
@state() private _languageScores?: LanguageScores;
|
|
|
|
protected override willUpdate(changedProperties: PropertyValues): void {
|
|
super.willUpdate(changedProperties);
|
|
|
|
if (!this.hasUpdated) {
|
|
this._fetchData();
|
|
}
|
|
|
|
if (
|
|
(changedProperties.has("language") ||
|
|
changedProperties.has("_languageScores")) &&
|
|
this.language &&
|
|
this._languageScores
|
|
) {
|
|
const lang = this.language;
|
|
if (this._value && this._languageScores[lang]?.[this._value] === 0) {
|
|
this._value = undefined;
|
|
}
|
|
if (!this._value) {
|
|
this._value = this._getOptions(
|
|
this._languageScores[lang] || EMPTY_SCORE,
|
|
this.hass.localize
|
|
).supportedOptions[0]?.value as
|
|
| "cloud"
|
|
| "focused_local"
|
|
| "full_local"
|
|
| undefined;
|
|
}
|
|
}
|
|
}
|
|
|
|
private _getOptions = memoizeOne((score, localize: LocalizeFunc) => {
|
|
const supportedOptions: SelectBoxOption[] = [];
|
|
const unsupportedOptions: SelectBoxOption[] = [];
|
|
|
|
OPTIONS.forEach((option) => {
|
|
if (score[option] > 0) {
|
|
supportedOptions.push({
|
|
label: localize(
|
|
`ui.panel.config.voice_assistants.satellite_wizard.pipeline.options.${option}.label`
|
|
),
|
|
description: localize(
|
|
`ui.panel.config.voice_assistants.satellite_wizard.pipeline.options.${option}.description`
|
|
),
|
|
value: option,
|
|
});
|
|
} else {
|
|
unsupportedOptions.push({
|
|
label: localize(
|
|
`ui.panel.config.voice_assistants.satellite_wizard.pipeline.options.${option}.label`
|
|
),
|
|
value: option,
|
|
});
|
|
}
|
|
});
|
|
|
|
return { supportedOptions, unsupportedOptions };
|
|
});
|
|
|
|
protected override render() {
|
|
if (!this._cloudChecked || !this._languageScores) {
|
|
return nothing;
|
|
}
|
|
|
|
if (!this.language) {
|
|
const language = formatLanguageCode(
|
|
this.hass.config.language,
|
|
this.hass.locale
|
|
);
|
|
return html`<div class="content">
|
|
<h1>
|
|
${this.hass.localize(
|
|
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported_language.header"
|
|
)}
|
|
</h1>
|
|
${this.hass.localize(
|
|
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported_language.secondary",
|
|
{ language }
|
|
)}
|
|
<ha-language-picker
|
|
.hass=${this.hass}
|
|
.label=${this.hass.localize(
|
|
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported_language.language_picker"
|
|
)}
|
|
.languages=${this.languages}
|
|
@value-changed=${this._languageChanged}
|
|
></ha-language-picker>
|
|
|
|
<a
|
|
href=${documentationUrl(
|
|
this.hass,
|
|
"/voice_control/contribute-voice/"
|
|
)}
|
|
>${this.hass.localize(
|
|
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported_language.contribute",
|
|
{ language }
|
|
)}</a
|
|
>
|
|
</div>`;
|
|
}
|
|
|
|
const score = this._languageScores[this.language] || EMPTY_SCORE;
|
|
|
|
const options = this._getOptions(score, this.hass.localize);
|
|
|
|
const performance = !this._value
|
|
? ""
|
|
: this._value === "full_local"
|
|
? "low"
|
|
: "high";
|
|
|
|
const commands = !this._value
|
|
? ""
|
|
: score[this._value] > 2
|
|
? "high"
|
|
: score[this._value] > 1
|
|
? "ready"
|
|
: score[this._value] > 0
|
|
? "low"
|
|
: "";
|
|
|
|
return html`<div class="content">
|
|
<h1>
|
|
${this.hass.localize(
|
|
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.title"
|
|
)}
|
|
</h1>
|
|
<div class="bar-header">
|
|
<span
|
|
>${this.hass.localize(
|
|
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.performance.header"
|
|
)}</span
|
|
><span
|
|
>${!performance
|
|
? ""
|
|
: this.hass.localize(
|
|
`ui.panel.config.voice_assistants.satellite_wizard.pipeline.performance.${performance}`
|
|
)}</span
|
|
>
|
|
</div>
|
|
<div class="perf-bar ${performance}">
|
|
<div class="segment"></div>
|
|
<div class="segment"></div>
|
|
<div class="segment"></div>
|
|
</div>
|
|
<div class="bar-header">
|
|
<span
|
|
>${this.hass.localize(
|
|
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.commands.header"
|
|
)}</span
|
|
><span
|
|
>${!commands
|
|
? ""
|
|
: this.hass.localize(
|
|
`ui.panel.config.voice_assistants.satellite_wizard.pipeline.commands.${commands}`
|
|
)}</span
|
|
>
|
|
</div>
|
|
<div class="perf-bar ${commands}">
|
|
<div class="segment"></div>
|
|
<div class="segment"></div>
|
|
<div class="segment"></div>
|
|
</div>
|
|
<ha-select-box
|
|
max_columns="1"
|
|
.options=${options.supportedOptions}
|
|
.value=${this._value}
|
|
@value-changed=${this._valueChanged}
|
|
></ha-select-box>
|
|
${options.unsupportedOptions.length
|
|
? html`<h3>
|
|
${this.hass.localize(
|
|
"ui.panel.config.voice_assistants.satellite_wizard.pipeline.unsupported"
|
|
)}
|
|
</h3>
|
|
<ha-select-box
|
|
max_columns="1"
|
|
.options=${options.unsupportedOptions}
|
|
disabled
|
|
></ha-select-box>`
|
|
: nothing}
|
|
</div>
|
|
<div class="footer">
|
|
<ha-button
|
|
@click=${this._createPipeline}
|
|
unelevated
|
|
.disabled=${!this._value}
|
|
>${this.hass.localize("ui.common.next")}</ha-button
|
|
>
|
|
</div>`;
|
|
}
|
|
|
|
private async _fetchData() {
|
|
const cloud =
|
|
(await this._hasCloud()) && (await this._createCloudPipeline(false));
|
|
if (!cloud) {
|
|
this._cloudChecked = true;
|
|
this._languageScores = (await getLanguageScores(this.hass)).languages;
|
|
}
|
|
}
|
|
|
|
private async _hasCloud(): Promise<boolean> {
|
|
if (!isComponentLoaded(this.hass, "cloud")) {
|
|
return false;
|
|
}
|
|
const cloudStatus = await fetchCloudStatus(this.hass);
|
|
if (!cloudStatus.logged_in || !cloudStatus.active_subscription) {
|
|
return false;
|
|
}
|
|
return true;
|
|
}
|
|
|
|
private async _createCloudPipeline(useLanguage: boolean): Promise<boolean> {
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
try {
|
|
const pipelines = await listAssistPipelines(this.hass);
|
|
|
|
if (pipelines.preferred_pipeline) {
|
|
pipelines.pipelines.sort((a) =>
|
|
a.id === pipelines.preferred_pipeline ? -1 : 0
|
|
);
|
|
}
|
|
|
|
let cloudPipeline = pipelines.pipelines.find(
|
|
(pipeline) =>
|
|
pipeline.conversation_engine === "conversation.home_assistant" &&
|
|
pipeline.tts_engine === cloudTtsEntityId &&
|
|
pipeline.stt_engine === cloudSttEntityId &&
|
|
(!useLanguage ||
|
|
pipeline.language.split("-")[0] === this.language!.split("-")[0])
|
|
);
|
|
|
|
if (!cloudPipeline) {
|
|
const agent = (
|
|
await listAgents(
|
|
this.hass,
|
|
this.language || this.hass.config.language,
|
|
this.hass.config.country || undefined
|
|
)
|
|
).agents.find((agnt) => agnt.id === "conversation.home_assistant");
|
|
|
|
if (!agent?.supported_languages.length) {
|
|
return false;
|
|
}
|
|
|
|
const ttsEngine = (
|
|
await listTTSEngines(
|
|
this.hass,
|
|
this.language || this.hass.config.language,
|
|
this.hass.config.country || undefined
|
|
)
|
|
).providers.find((provider) => provider.engine_id === cloudTtsEntityId);
|
|
|
|
if (!ttsEngine?.supported_languages?.length) {
|
|
return false;
|
|
}
|
|
|
|
const ttsVoices = await listTTSVoices(
|
|
this.hass,
|
|
cloudTtsEntityId,
|
|
ttsEngine.supported_languages[0]
|
|
);
|
|
|
|
const sttEngine = (
|
|
await listSTTEngines(
|
|
this.hass,
|
|
this.language || this.hass.config.language,
|
|
this.hass.config.country || undefined
|
|
)
|
|
).providers.find((provider) => provider.engine_id === cloudSttEntityId);
|
|
|
|
if (!sttEngine?.supported_languages?.length) {
|
|
return false;
|
|
}
|
|
|
|
let pipelineName = "Home Assistant Cloud";
|
|
let i = 1;
|
|
while (
|
|
pipelines.pipelines.find(
|
|
// eslint-disable-next-line no-loop-func
|
|
(pipeline) => pipeline.name === pipelineName
|
|
)
|
|
) {
|
|
pipelineName = `Home Assistant Cloud ${i}`;
|
|
i++;
|
|
}
|
|
|
|
cloudPipeline = await createAssistPipeline(this.hass, {
|
|
name: pipelineName,
|
|
language: (this.language || this.hass.config.language).split("-")[0],
|
|
conversation_engine: "conversation.home_assistant",
|
|
conversation_language: agent.supported_languages[0],
|
|
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 }
|
|
);
|
|
fireEvent(this, "next-step", { step: STEP.SUCCESS, noPrevious: true });
|
|
return true;
|
|
} catch (_e) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
private _valueChanged(ev: CustomEvent) {
|
|
this._value = ev.detail.value;
|
|
}
|
|
|
|
private async _setupCloud() {
|
|
if (await this._hasCloud()) {
|
|
this._createCloudPipeline(true);
|
|
return;
|
|
}
|
|
fireEvent(this, "next-step", { step: STEP.CLOUD });
|
|
}
|
|
|
|
private _createPipeline() {
|
|
if (this._value === "cloud") {
|
|
this._setupCloud();
|
|
} else if (this._value === "focused_local") {
|
|
this._setupLocalFocused();
|
|
} else {
|
|
this._setupLocalFull();
|
|
}
|
|
}
|
|
|
|
private _setupLocalFocused() {
|
|
fireEvent(this, "next-step", { step: STEP.LOCAL, option: this._value });
|
|
}
|
|
|
|
private _setupLocalFull() {
|
|
fireEvent(this, "next-step", { step: STEP.LOCAL, option: this._value });
|
|
}
|
|
|
|
private _languageChanged(ev: CustomEvent) {
|
|
if (!ev.detail.value) {
|
|
return;
|
|
}
|
|
fireEvent(this, "language-changed", { value: ev.detail.value });
|
|
}
|
|
|
|
static styles = [
|
|
AssistantSetupStyles,
|
|
css`
|
|
:host {
|
|
text-align: left;
|
|
}
|
|
.perf-bar {
|
|
width: 100%;
|
|
height: 10px;
|
|
display: flex;
|
|
gap: 4px;
|
|
margin: 8px 0;
|
|
}
|
|
.segment {
|
|
flex-grow: 1;
|
|
background-color: var(--disabled-color);
|
|
transition: background-color 0.3s;
|
|
}
|
|
.segment:first-child {
|
|
border-radius: 4px 0 0 4px;
|
|
}
|
|
.segment:last-child {
|
|
border-radius: 0 4px 4px 0;
|
|
}
|
|
.perf-bar.high .segment {
|
|
background-color: var(--success-color);
|
|
}
|
|
.perf-bar.ready .segment:nth-child(-n + 2) {
|
|
background-color: var(--warning-color);
|
|
}
|
|
.perf-bar.low .segment:nth-child(1) {
|
|
background-color: var(--error-color);
|
|
}
|
|
.bar-header {
|
|
display: flex;
|
|
justify-content: space-between;
|
|
margin: 8px 0;
|
|
margin-top: 16px;
|
|
}
|
|
ha-select-box {
|
|
display: block;
|
|
}
|
|
ha-select-box:first-of-type {
|
|
margin-top: 32px;
|
|
}
|
|
.footer {
|
|
margin-top: 16px;
|
|
}
|
|
ha-language-picker {
|
|
display: block;
|
|
margin-top: 16px;
|
|
margin-bottom: 16px;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"ha-voice-assistant-setup-step-pipeline": HaVoiceAssistantSetupStepPipeline;
|
|
}
|
|
}
|