Compare commits

...
Author SHA1 Message Date
Petar Petrov cc45bf1f8b Drop hass from the speech engine pickers
The TTS and STT helpers take callWS instead of hass, so the TTS, STT and TTS voice pickers and their selectors read translations, registries, states and the API from contexts.
2026-10-26 17:59:32 +02:00
12 changed files with 174 additions and 81 deletions

No files matched your search

@@ -1,13 +1,10 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { STTSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-stt-picker";
@customElement("ha-selector-stt")
export class HaSTTSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: STTSelector;
@property() public value?: any;
@@ -26,7 +23,6 @@ export class HaSTTSelector extends LitElement {
protected render() {
return html`<ha-stt-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -1,13 +1,10 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { TTSVoiceSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-tts-voice-picker";
@customElement("ha-selector-tts_voice")
export class HaTTSVoiceSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: TTSVoiceSelector;
@property() public value?: any;
@@ -27,7 +24,6 @@ export class HaTTSVoiceSelector extends LitElement {
protected render() {
return html`<ha-tts-voice-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
@@ -1,13 +1,10 @@
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import type { TTSSelector } from "../../data/selector";
import type { HomeAssistant } from "../../types";
import "../ha-tts-picker";
@customElement("ha-selector-tts")
export class HaTTSSelector extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public selector!: TTSSelector;
@property() public value?: any;
@@ -26,7 +23,6 @@ export class HaTTSSelector extends LitElement {
protected render() {
return html`<ha-tts-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.helper=${this.helper}
+72 -23
View File
@@ -1,12 +1,28 @@
import type { ContextType } from "@lit/context";
import type { HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consume } from "../common/decorators/consume";
import {
consumeLocalize,
preserveUnchangedEntityStatesRecord,
} from "../common/decorators/consume-context-entry";
import { transform } from "../common/decorators/transform";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import type { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import {
apiContext,
configContext,
entitiesContext,
formattersContext,
statesContext,
} from "../data/context";
import type { STTEngine } from "../data/stt";
import { listSTTEngines } from "../data/stt";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantFormatters } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
@@ -20,14 +36,60 @@ export class HaSTTPicker extends LitElement {
@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?: STTEngine[];
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: entitiesContext, subscribe: true })
@transform<HomeAssistant["entities"], string | undefined>({
transformer: (entities) =>
Object.values(entities ?? {}).find(
(entity) =>
entity.platform === "cloud" &&
computeDomain(entity.entity_id) === "stt"
)?.entity_id,
})
private _cloudEngineId?: string;
@state()
@consume({ context: statesContext, subscribe: true })
@transform<HassEntities, HassEntities>({
transformer: function (this: HaSTTPicker, states) {
const engineStates: HassEntities = {};
this._engines?.forEach(({ engine_id }) => {
if (states?.[engine_id]) {
engineStates[engine_id] = states[engine_id];
}
});
return preserveUnchangedEntityStatesRecord(this._states, engineStates);
},
watch: ["_engines"],
})
private _states: HassEntities = {};
@state()
@consume({ context: formattersContext, subscribe: true })
@transform<
HomeAssistantFormatters,
HomeAssistantFormatters["formatEntityName"]
>({
transformer: ({ formatEntityName }) => formatEntityName,
})
private _formatEntityName!: HomeAssistantFormatters["formatEntityName"];
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
protected render() {
if (!this._engines) {
return nothing;
@@ -35,15 +97,7 @@ export class HaSTTPicker extends LitElement {
let value = this.value;
if (!value && this.required) {
for (const entity of Object.values(this.hass.entities)) {
if (
entity.platform === "cloud" &&
computeDomain(entity.entity_id) === "stt"
) {
value = entity.entity_id;
break;
}
}
value = this._cloudEngineId;
if (!value) {
for (const sttEngine of this._engines) {
@@ -61,10 +115,8 @@ export class HaSTTPicker extends LitElement {
const options: HaSelectOption[] = this._engines.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
label = stateObj
? this.hass.formatEntityName(stateObj)
: engine.engine_id;
const stateObj = this._states[engine.engine_id];
label = stateObj ? this._formatEntityName(stateObj) : engine.engine_id;
} else {
label = engine.name || engine.engine_id;
}
@@ -78,15 +130,13 @@ export class HaSTTPicker extends LitElement {
if (this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.stt-picker.none") || "None",
label: this._localize("ui.components.stt-picker.none") || "None",
});
}
return html`
<ha-select
.label=${
this.label || this.hass!.localize("ui.components.stt-picker.stt")
}
.label=${this.label || this._localize("ui.components.stt-picker.stt")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@@ -111,9 +161,9 @@ export class HaSTTPicker extends LitElement {
private async _updateEngines() {
this._engines = (
await listSTTEngines(
this.hass,
this._api.callWS,
this.language,
this.hass.config.country || undefined
this._config.config.country || undefined
)
).providers;
@@ -144,7 +194,6 @@ export class HaSTTPicker extends LitElement {
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
+72 -21
View File
@@ -1,12 +1,28 @@
import type { ContextType } from "@lit/context";
import type { HassEntities } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consume } from "../common/decorators/consume";
import {
consumeLocalize,
preserveUnchangedEntityStatesRecord,
} from "../common/decorators/consume-context-entry";
import { transform } from "../common/decorators/transform";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import type { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import {
apiContext,
configContext,
entitiesContext,
formattersContext,
statesContext,
} from "../data/context";
import type { TTSEngine } from "../data/tts";
import { listTTSEngines } from "../data/tts";
import type { HomeAssistant } from "../types";
import type { HomeAssistant, HomeAssistantFormatters } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
@@ -20,14 +36,60 @@ export class HaTTSPicker extends LitElement {
@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[];
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@state()
@consume({ context: entitiesContext, subscribe: true })
@transform<HomeAssistant["entities"], string | undefined>({
transformer: (entities) =>
Object.values(entities ?? {}).find(
(entity) =>
entity.platform === "cloud" &&
computeDomain(entity.entity_id) === "tts"
)?.entity_id,
})
private _cloudEngineId?: string;
@state()
@consume({ context: statesContext, subscribe: true })
@transform<HassEntities, HassEntities>({
transformer: function (this: HaTTSPicker, states) {
const engineStates: HassEntities = {};
this._engines?.forEach(({ engine_id }) => {
if (states?.[engine_id]) {
engineStates[engine_id] = states[engine_id];
}
});
return preserveUnchangedEntityStatesRecord(this._states, engineStates);
},
watch: ["_engines"],
})
private _states: HassEntities = {};
@state()
@consume({ context: formattersContext, subscribe: true })
@transform<
HomeAssistantFormatters,
HomeAssistantFormatters["formatEntityName"]
>({
transformer: ({ formatEntityName }) => formatEntityName,
})
private _formatEntityName!: HomeAssistantFormatters["formatEntityName"];
@consume({ context: configContext, subscribe: true })
private _config!: ContextType<typeof configContext>;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
protected render() {
if (!this._engines) {
return nothing;
@@ -35,15 +97,7 @@ export class HaTTSPicker extends LitElement {
let value = this.value;
if (!value && this.required) {
for (const entity of Object.values(this.hass.entities)) {
if (
entity.platform === "cloud" &&
computeDomain(entity.entity_id) === "tts"
) {
value = entity.entity_id;
break;
}
}
value = this._cloudEngineId;
if (!value) {
for (const ttsEngine of this._engines) {
@@ -63,9 +117,9 @@ export class HaTTSPicker extends LitElement {
.map((engine) => {
let label: string;
if (engine.engine_id.includes(".")) {
const stateObj = this.hass.states[engine.engine_id];
const stateObj = this._states[engine.engine_id];
label = stateObj
? this.hass.formatEntityName(stateObj)
? this._formatEntityName(stateObj)
: engine.engine_id;
} else {
label = engine.name || engine.engine_id;
@@ -80,15 +134,13 @@ export class HaTTSPicker extends LitElement {
if (!this.required || value === NONE) {
options.unshift({
value: NONE,
label: this.hass.localize("ui.components.tts-picker.none"),
label: this._localize("ui.components.tts-picker.none"),
});
}
return html`
<ha-select
.label=${
this.label || this.hass!.localize("ui.components.tts-picker.tts")
}
.label=${this.label || this._localize("ui.components.tts-picker.tts")}
.value=${value}
.required=${this.required}
.disabled=${this.disabled}
@@ -113,9 +165,9 @@ export class HaTTSPicker extends LitElement {
private async _updateEngines() {
this._engines = (
await listTTSEngines(
this.hass,
this._api.callWS,
this.language,
this.hass.config.country || undefined
this._config.config.country || undefined
)
).providers;
@@ -146,7 +198,6 @@ export class HaTTSPicker extends LitElement {
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
+15 -8
View File
@@ -1,11 +1,15 @@
import type { ContextType } from "@lit/context";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { consume } from "../common/decorators/consume";
import { consumeLocalize } from "../common/decorators/consume-context-entry";
import { fireEvent } from "../common/dom/fire_event";
import type { LocalizeFunc } from "../common/translations/localize";
import { debounce } from "../common/util/debounce";
import { apiContext } from "../data/context";
import type { TTSVoice } from "../data/tts";
import { listTTSVoices } from "../data/tts";
import type { HomeAssistant } from "../types";
import type { HaSelectOption, HaSelectSelectEvent } from "./ha-select";
import "./ha-select";
@@ -21,14 +25,19 @@ export class HaTTSVoicePicker extends LitElement {
@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() _voices?: TTSVoice[] | null;
@state()
@consumeLocalize()
private _localize!: LocalizeFunc;
@consume({ context: apiContext, subscribe: true })
private _api!: ContextType<typeof apiContext>;
protected render() {
if (!this._voices) {
return nothing;
@@ -44,15 +53,14 @@ export class HaTTSVoicePicker extends LitElement {
if (!this.required || !this.value) {
options.unshift({
value: NONE,
label: this.hass!.localize("ui.components.tts-voice-picker.none"),
label: this._localize("ui.components.tts-voice-picker.none"),
});
}
return html`
<ha-select
.label=${
this.label ||
this.hass!.localize("ui.components.tts-voice-picker.voice")
this.label || this._localize("ui.components.tts-voice-picker.voice")
}
.value=${value}
.required=${this.required}
@@ -84,7 +92,7 @@ export class HaTTSVoicePicker extends LitElement {
return;
}
this._voices = (
await listTTSVoices(this.hass, this.engineId, this.language)
await listTTSVoices(this._api.callWS, this.engineId, this.language)
).voices;
const valueIsValid =
@@ -121,7 +129,6 @@ export class HaTTSVoicePicker extends LitElement {
private _changed(ev: HaSelectSelectEvent): void {
const value = ev.detail.value;
if (
!this.hass ||
value === "" ||
value === this.value ||
(this.value === undefined && value === NONE)
@@ -83,7 +83,6 @@ class BrowseMediaTTS extends LitElement {
@value-changed=${this._languageChanged}
></ha-language-picker>
<ha-tts-voice-picker
.hass=${this.hass}
.value=${this._voice}
.engineId=${this._provider.engine_id}
.language=${this._language}
@@ -149,7 +148,7 @@ class BrowseMediaTTS extends LitElement {
);
if (provider !== this._provider?.engine_id) {
this._provider = undefined;
getTTSEngine(this.hass, provider).then((engine) => {
getTTSEngine(this.hass.callWS, provider).then((engine) => {
this._provider = engine.provider;
if (
!this._language &&
+2 -2
View File
@@ -17,11 +17,11 @@ export interface STTEngine {
}
export const listSTTEngines = (
hass: HomeAssistant,
callWS: HomeAssistant["callWS"],
language?: string,
country?: string
): Promise<{ providers: STTEngine[] }> =>
hass.callWS({
callWS({
type: "stt/engine/list",
language,
country,
+6 -6
View File
@@ -32,31 +32,31 @@ export const getProviderFromTTSMediaSource = (mediaContentId: string) =>
mediaContentId.substring(TTS_MEDIA_SOURCE_PREFIX.length);
export const listTTSEngines = (
hass: HomeAssistant,
callWS: HomeAssistant["callWS"],
language?: string,
country?: string
): Promise<{ providers: TTSEngine[] }> =>
hass.callWS({
callWS({
type: "tts/engine/list",
language,
country,
});
export const getTTSEngine = (
hass: HomeAssistant,
callWS: HomeAssistant["callWS"],
engine_id: string
): Promise<{ provider: TTSEngine }> =>
hass.callWS({
callWS({
type: "tts/engine/get",
engine_id,
});
export const listTTSVoices = (
hass: HomeAssistant,
callWS: HomeAssistant["callWS"],
engine_id: string,
language: string
): Promise<{ voices: TTSVoice[] | null }> =>
hass.callWS({
callWS({
type: "tts/engine/voices",
engine_id,
language,
@@ -420,7 +420,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
const ttsEngine = (
await listTTSEngines(
this.hass,
this.hass.callWS,
this.language,
this.hass.config.country || undefined
)
@@ -431,7 +431,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
}
const ttsVoices = await listTTSVoices(
this.hass,
this.hass.callWS,
ttsEntityId,
ttsEngine.supported_languages[0]
);
@@ -442,7 +442,7 @@ export class HaVoiceAssistantSetupStepLocal extends LitElement {
const sttEngine = (
await listSTTEngines(
this.hass,
this.hass.callWS,
this.language,
this.hass.config.country || undefined
)
@@ -322,7 +322,7 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
const ttsEngine = (
await listTTSEngines(
this.hass,
this.hass.callWS,
this.language || this.hass.config.language,
this.hass.config.country || undefined
)
@@ -333,14 +333,14 @@ export class HaVoiceAssistantSetupStepPipeline extends LitElement {
}
const ttsVoices = await listTTSVoices(
this.hass,
this.hass.callWS,
cloudTtsEntityId!,
ttsEngine.supported_languages[0]
);
const sttEngine = (
await listSTTEngines(
this.hass,
this.hass.callWS,
this.language || this.hass.config.language,
this.hass.config.country || undefined
)
@@ -184,7 +184,6 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
this._ttsSettings
? html`<div class="row">
<ha-tts-voice-picker
.hass=${this.hass}
.engineId=${this._ttsSettings.engine}
.language=${this._ttsSettings.language}
.value=${this._ttsSettings.voice}