diff --git a/src/components/ha-list-item.ts b/src/components/ha-list-item.ts index 8107ae4f2a..252e80954a 100644 --- a/src/components/ha-list-item.ts +++ b/src/components/ha-list-item.ts @@ -57,6 +57,9 @@ export class HaListItem extends ListItemBase { .mdc-deprecated-list-item__primary-text::before { display: none; } + :host([disabled]) { + color: var(--disabled-text-color); + } `, ]; } diff --git a/src/components/ha-selector/ha-selector-tts.ts b/src/components/ha-selector/ha-selector-tts.ts new file mode 100644 index 0000000000..fbf83fb167 --- /dev/null +++ b/src/components/ha-selector/ha-selector-tts.ts @@ -0,0 +1,50 @@ +import { css, html, LitElement } from "lit"; +import { customElement, property } from "lit/decorators"; +import { TTSSelector } from "../../data/selector"; +import { HomeAssistant } from "../../types"; +import "../ha-tts-picker"; + +@customElement("ha-selector-tts") +export class HaTTSSelector extends LitElement { + @property() public hass!: HomeAssistant; + + @property() public selector!: TTSSelector; + + @property() public value?: any; + + @property() public label?: string; + + @property() public helper?: string; + + @property({ type: Boolean }) public disabled = false; + + @property({ type: Boolean }) public required = true; + + @property({ attribute: false }) public context?: { + language?: string; + }; + + protected render() { + return html``; + } + + static styles = css` + ha-tts-picker { + width: 100%; + } + `; +} + +declare global { + interface HTMLElementTagNameMap { + "ha-selector-tts": HaTTSSelector; + } +} diff --git a/src/components/ha-selector/ha-selector.ts b/src/components/ha-selector/ha-selector.ts index 8f4d5bb343..12cd14d2b9 100644 --- a/src/components/ha-selector/ha-selector.ts +++ b/src/components/ha-selector/ha-selector.ts @@ -39,6 +39,7 @@ const LOAD_ELEMENTS = { icon: () => import("./ha-selector-icon"), media: () => import("./ha-selector-media"), theme: () => import("./ha-selector-theme"), + tts: () => import("./ha-selector-tts"), location: () => import("./ha-selector-location"), color_temp: () => import("./ha-selector-color-temp"), "ui-action": () => import("./ha-selector-ui-action"), diff --git a/src/components/ha-tts-picker.ts b/src/components/ha-tts-picker.ts new file mode 100644 index 0000000000..c0d5916a4d --- /dev/null +++ b/src/components/ha-tts-picker.ts @@ -0,0 +1,105 @@ +import { + css, + CSSResultGroup, + html, + LitElement, + PropertyValueMap, + TemplateResult, +} from "lit"; +import { customElement, property, state } from "lit/decorators"; +import { fireEvent } from "../common/dom/fire_event"; +import { stopPropagation } from "../common/dom/stop_propagation"; +import { computeStateName } from "../common/entity/compute_state_name"; +import { TTSEngine, listTTSEngines } from "../data/tts"; +import { HomeAssistant } from "../types"; +import "./ha-select"; +import "./ha-list-item"; +import type { HaSelect } from "./ha-select"; + +const DEFAULT = "default_engine_option"; + +@customElement("ha-tts-picker") +export class HaTTSPicker extends LitElement { + @property() public value?: string; + + @property() public label?: string; + + @property() public language?: string; + + @property({ attribute: false }) public hass!: HomeAssistant; + + @property({ type: Boolean, reflect: true }) public disabled = false; + + @property({ type: Boolean }) public required = false; + + @state() _engines: TTSEngine[] = []; + + protected render(): TemplateResult { + const value = this.value ?? DEFAULT; + return html` + + + ${this.hass!.localize("ui.components.tts-picker.default")} + + ${this._engines.map((engine) => { + const stateObj = this.hass!.states[engine.engine_id]; + return html` + ${stateObj ? computeStateName(stateObj) : engine.engine_id} + `; + })} + + `; + } + + protected willUpdate( + changedProperties: PropertyValueMap | Map + ): void { + super.willUpdate(changedProperties); + if (!this.hasUpdated || changedProperties.has("language")) { + listTTSEngines(this.hass, this.language).then((engines) => { + this._engines = engines.providers; + }); + } + } + + static get styles(): CSSResultGroup { + return css` + ha-select { + width: 100%; + } + `; + } + + private _changed(ev): void { + const target = ev.target as HaSelect; + if ( + !this.hass || + target.value === "" || + target.value === this.value || + (this.value === undefined && target.value === DEFAULT) + ) { + return; + } + this.value = target.value === DEFAULT ? undefined : target.value; + fireEvent(this, "value-changed", { value: this.value }); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-tts-picker": HaTTSPicker; + } +} diff --git a/src/data/selector.ts b/src/data/selector.ts index 429e40d3dd..e3aec23946 100644 --- a/src/data/selector.ts +++ b/src/data/selector.ts @@ -40,6 +40,7 @@ export type Selector = | TemplateSelector | ThemeSelector | TimeSelector + | TTSSelector | UiActionSelector | UiColorSelector; @@ -326,6 +327,10 @@ export interface TimeSelector { time: {} | null; } +export interface TTSSelector { + tts: { language?: string } | null; +} + export interface UiActionSelector { "ui-action": { actions?: UiAction[]; diff --git a/src/data/tts.ts b/src/data/tts.ts index f4d0d1346b..63a237d2b8 100644 --- a/src/data/tts.ts +++ b/src/data/tts.ts @@ -1,5 +1,15 @@ import { HomeAssistant } from "../types"; +export interface TTSEngine { + engine_id: string; + language_supported?: boolean; +} + +export interface TTSVoice { + voice_id: string; + name: string; +} + export const convertTextToSpeech = ( hass: HomeAssistant, data: { @@ -18,3 +28,23 @@ export const isTTSMediaSource = (mediaContentId: string) => export const getProviderFromTTSMediaSource = (mediaContentId: string) => mediaContentId.substring(TTS_MEDIA_SOURCE_PREFIX.length); + +export const listTTSEngines = ( + hass: HomeAssistant, + language?: string +): Promise<{ providers: TTSEngine[] }> => + hass.callWS({ + type: "tts/engine/list", + language, + }); + +export const listTTSVoices = ( + hass: HomeAssistant, + engine_id: string, + language: string +): Promise<{ voices: TTSVoice[] }> => + hass.callWS({ + type: "tts/engine/voices", + engine_id, + language, + }); 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 1cc48117d4..b70f1c2619 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 @@ -123,7 +123,6 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { }, { name: "conversation_engine", - required: true, selector: { conversation_agent: {}, }, @@ -137,7 +136,6 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { }, { name: "stt_engine", - required: true, selector: { stt: {}, }, @@ -145,10 +143,10 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { }, { name: "tts_engine", - required: true, selector: { - text: {}, + tts: {}, }, + context: { language: "language" }, }, ] as const ); diff --git a/src/translations/en.json b/src/translations/en.json index 2f3cbbe2bf..43097bd8f8 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -409,6 +409,10 @@ "theme": "Theme", "no_theme": "No theme" }, + "tts-picker": { + "tts": "Text to Speech", + "default": "Default" + }, "user-picker": { "no_user": "No user", "add_user": "Add user",