20230502.0 (#16382)

This commit is contained in:
Bram Kragten 2023-05-02 22:01:32 +02:00 committed by GitHub
commit 29aa762f7c
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
42 changed files with 560 additions and 357 deletions

View File

@ -9,7 +9,6 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { supervisorTabs } from "../hassio-tabs"; import { supervisorTabs } from "../hassio-tabs";
import "./hassio-addons"; import "./hassio-addons";
import "./hassio-update";
import "../../../src/layouts/hass-subpage"; import "../../../src/layouts/hass-subpage";
@customElement("hassio-dashboard") @customElement("hassio-dashboard")
@ -22,6 +21,12 @@ class HassioDashboard extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
firstUpdated() {
if (!atLeastVersion(this.hass.config.version, 2022, 5)) {
import("./hassio-update");
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (atLeastVersion(this.hass.config.version, 2022, 5)) { if (atLeastVersion(this.hass.config.version, 2022, 5)) {
return html`<hass-subpage return html`<hass-subpage
@ -44,7 +49,7 @@ class HassioDashboard extends LitElement {
<ha-svg-icon <ha-svg-icon
slot="icon" slot="icon"
.path=${mdiStorePlus} .path=${mdiStorePlus}
></ha-svg-icon> </ha-fab ></ha-svg-icon></ha-fab
></a> ></a>
</hass-subpage>`; </hass-subpage>`;
} }

View File

@ -2,6 +2,7 @@
import "../../src/resources/compatibility"; import "../../src/resources/compatibility";
import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings";
import "../../src/resources/roboto"; import "../../src/resources/roboto";
import "../../src/resources/ha-style";
import "../../src/resources/safari-14-attachshadow-patch"; import "../../src/resources/safari-14-attachshadow-patch";
import "./hassio-main"; import "./hassio-main";

View File

@ -9,7 +9,6 @@ import { navigate } from "../../src/common/navigate";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor"; import { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant } from "../../src/types";
import "./hassio-router"; import "./hassio-router";
import { SupervisorBaseElement } from "./supervisor-base-element"; import { SupervisorBaseElement } from "./supervisor-base-element";

View File

@ -5,12 +5,8 @@ import {
RouterOptions, RouterOptions,
} from "../../src/layouts/hass-router-page"; } from "../../src/layouts/hass-router-page";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import "./addon-store/hassio-addon-store";
// Don't codesplit it, that way the dashboard always loads fast. // Don't codesplit it, that way the dashboard always loads fast.
import "./dashboard/hassio-dashboard"; import "./dashboard/hassio-dashboard";
// Don't codesplit the others, because it breaks the UI when pushed to a Pi
import "./backups/hassio-backups";
import "./system/hassio-system";
@customElement("hassio-panel-router") @customElement("hassio-panel-router")
class HassioPanelRouter extends HassRouterPage { class HassioPanelRouter extends HassRouterPage {
@ -31,12 +27,15 @@ class HassioPanelRouter extends HassRouterPage {
}, },
store: { store: {
tag: "hassio-addon-store", tag: "hassio-addon-store",
load: () => import("./addon-store/hassio-addon-store"),
}, },
backups: { backups: {
tag: "hassio-backups", tag: "hassio-backups",
load: () => import("./backups/hassio-backups"),
}, },
system: { system: {
tag: "hassio-system", tag: "hassio-system",
load: () => import("./system/hassio-system"),
}, },
}, },
}; };

View File

@ -4,6 +4,7 @@ import {
Supervisor, Supervisor,
supervisorCollection, supervisorCollection,
} from "../../src/data/supervisor/supervisor"; } from "../../src/data/supervisor/supervisor";
import "../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router"; import "./hassio-panel-router";

View File

@ -5,7 +5,6 @@ import {
HassRouterPage, HassRouterPage,
RouterOptions, RouterOptions,
} from "../../src/layouts/hass-router-page"; } from "../../src/layouts/hass-router-page";
import "../../src/resources/ha-style";
import { HomeAssistant } from "../../src/types"; import { HomeAssistant } from "../../src/types";
// Don't codesplit it, that way the dashboard always loads fast. // Don't codesplit it, that way the dashboard always loads fast.
import "./hassio-panel"; import "./hassio-panel";

View File

@ -42,9 +42,6 @@ import { updateCore } from "../../../src/data/supervisor/core";
import { StoreAddon } from "../../../src/data/supervisor/store"; import { StoreAddon } from "../../../src/data/supervisor/store";
import { Supervisor } from "../../../src/data/supervisor/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-subpage";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
import { addonArchIsSupported, extractChangelog } from "../util/addon"; import { addonArchIsSupported, extractChangelog } from "../util/addon";

View File

@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project] [project]
name = "home-assistant-frontend" name = "home-assistant-frontend"
version = "20230501.0" version = "20230502.0"
license = {text = "Apache-2.0"} license = {text = "Apache-2.0"}
description = "The Home Assistant frontend" description = "The Home Assistant frontend"
readme = "README.md" readme = "README.md"

View File

@ -15,6 +15,8 @@ class AliasesEditor extends LitElement {
@property() public aliases!: string[]; @property() public aliases!: string[];
@property({ type: Boolean }) public disabled = false;
protected render() { protected render() {
if (!this.aliases) { if (!this.aliases) {
return nothing; return nothing;
@ -25,6 +27,7 @@ class AliasesEditor extends LitElement {
(alias, index) => html` (alias, index) => html`
<div class="layout horizontal center-center row"> <div class="layout horizontal center-center row">
<ha-textfield <ha-textfield
.disabled=${this.disabled}
dialogInitialFocus=${index} dialogInitialFocus=${index}
.index=${index} .index=${index}
class="flex-auto" class="flex-auto"
@ -37,6 +40,7 @@ class AliasesEditor extends LitElement {
@keydown=${this._keyDownAlias} @keydown=${this._keyDownAlias}
></ha-textfield> ></ha-textfield>
<ha-icon-button <ha-icon-button
.disabled=${this.disabled}
.index=${index} .index=${index}
slot="navigationIcon" slot="navigationIcon"
label=${this.hass!.localize("ui.dialogs.aliases.remove_alias", { label=${this.hass!.localize("ui.dialogs.aliases.remove_alias", {
@ -49,7 +53,7 @@ class AliasesEditor extends LitElement {
` `
)} )}
<div class="layout horizontal center-center"> <div class="layout horizontal center-center">
<mwc-button @click=${this._addAlias}> <mwc-button @click=${this._addAlias} .disabled=${this.disabled}>
${this.hass!.localize("ui.dialogs.aliases.add_alias")} ${this.hass!.localize("ui.dialogs.aliases.add_alias")}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button> </mwc-button>

View File

@ -19,7 +19,10 @@ import type { HaSelect } from "./ha-select";
const NONE = "__NONE_OPTION__"; const NONE = "__NONE_OPTION__";
const NAME_MAP = { cloud: "Home Assistant Cloud" }; const NAME_MAP = {
cloud: "Home Assistant Cloud",
google_translate: "Google Translate",
};
@customElement("ha-tts-picker") @customElement("ha-tts-picker")
export class HaTTSPicker extends LitElement { export class HaTTSPicker extends LitElement {

View File

@ -21,6 +21,7 @@ import { buttonLinkStyle } from "../../resources/styles";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-select"; import "../ha-select";
import "../ha-textarea"; import "../ha-textarea";
import "../ha-language-picker";
export interface TtsMediaPickedEvent { export interface TtsMediaPickedEvent {
item: MediaPlayerItem; item: MediaPlayerItem;
@ -103,21 +104,17 @@ class BrowseMediaTTS extends LitElement {
return html` return html`
<div class="cloud-options"> <div class="cloud-options">
<ha-select <ha-language-picker
fixedMenuPosition .hass=${this.hass}
naturalMenuWidth
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.media-browser.tts.language" "ui.components.media-browser.tts.language"
)} )}
.value=${selectedVoice[0]} .value=${selectedVoice[0]}
@selected=${this._handleLanguageChange} .languages=${languages}
@closed=${stopPropagation} @closed=${stopPropagation}
@value-changed=${this._handleLanguageChange}
> >
${languages.map( </ha-language-picker>
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</ha-select>
<ha-select <ha-select
fixedMenuPosition fixedMenuPosition
@ -184,10 +181,10 @@ class BrowseMediaTTS extends LitElement {
} }
async _handleLanguageChange(ev) { async _handleLanguageChange(ev) {
if (ev.target.value === this._cloudOptions![0]) { if (ev.detail.value === this._cloudOptions![0]) {
return; return;
} }
this._cloudOptions = [ev.target.value, this._cloudOptions![1]]; this._cloudOptions = [ev.detail.value, this._cloudOptions![1]];
} }
async _handleGenderChange(ev) { async _handleGenderChange(ev) {
@ -256,7 +253,8 @@ class BrowseMediaTTS extends LitElement {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
} }
.cloud-options ha-select { .cloud-options ha-select,
ha-language-picker {
width: 48%; width: 48%;
} }
ha-textarea { ha-textarea {

View File

@ -1,6 +1,5 @@
import { caseInsensitiveStringCompare } from "../../common/string/compare"; import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { LocalizeFunc } from "../../common/translations/localize"; import { LocalizeFunc } from "../../common/translations/localize";
import { translationMetadata } from "../../resources/translations-metadata";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
export interface CloudTTSInfo { export interface CloudTTSInfo {
@ -11,7 +10,7 @@ export const getCloudTTSInfo = (hass: HomeAssistant) =>
hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" }); hass.callWS<CloudTTSInfo>({ type: "cloud/tts/info" });
export const getCloudTtsLanguages = (info?: CloudTTSInfo) => { export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
const languages: Array<[string, string]> = []; const languages: string[] = [];
if (!info) { if (!info) {
return languages; return languages;
@ -23,25 +22,9 @@ export const getCloudTtsLanguages = (info?: CloudTTSInfo) => {
continue; continue;
} }
seen.add(lang); seen.add(lang);
languages.push(lang);
let label = lang;
if (lang in translationMetadata.translations) {
label = translationMetadata.translations[lang].nativeName;
} else {
const [langFamily, dialect] = lang.split("-");
if (langFamily in translationMetadata.translations) {
label = `${translationMetadata.translations[langFamily].nativeName}`;
if (langFamily.toLowerCase() !== dialect.toLowerCase()) {
label += ` (${dialect})`;
}
}
}
languages.push([lang, label]);
} }
return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1])); return languages;
}; };
export const getCloudTtsSupportedGenders = ( export const getCloudTtsSupportedGenders = (

View File

@ -12,6 +12,12 @@ export const voiceAssistants = {
}, },
} as const; } as const;
export interface ExposeEntitySettings {
conversation?: boolean;
"cloud.alexa"?: boolean;
"cloud.google_assistant"?: boolean;
}
export const setExposeNewEntities = ( export const setExposeNewEntities = (
hass: HomeAssistant, hass: HomeAssistant,
assistant: string, assistant: string,
@ -41,3 +47,8 @@ export const exposeEntities = (
entity_ids, entity_ids,
should_expose, should_expose,
}); });
export const listExposedEntities = (hass: HomeAssistant) =>
hass.callWS<{ exposed_entities: Record<string, ExposeEntitySettings> }>({
type: "homeassistant/expose_entity/list",
});

View File

@ -1,6 +1,8 @@
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ExtEntityRegistryEntry } from "../../../../data/entity_registry"; import { ExtEntityRegistryEntry } from "../../../../data/entity_registry";
import { ExposeEntitySettings, voiceAssistants } from "../../../../data/expose";
import "../../../../panels/config/voice-assistants/entity-voice-settings"; import "../../../../panels/config/voice-assistants/entity-voice-settings";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
@ -12,13 +14,23 @@ class MoreInfoViewVoiceAssistants extends LitElement {
@property() public params?; @property() public params?;
private _calculateExposed = memoizeOne((entry: ExtEntityRegistryEntry) => {
const exposed: ExposeEntitySettings = {};
Object.keys(voiceAssistants).forEach((key) => {
exposed[key] = entry.options?.[key]?.should_expose;
});
return exposed;
});
protected render() { protected render() {
if (!this.params) { if (!this.params) {
return nothing; return nothing;
} }
return html`<entity-voice-settings return html`<entity-voice-settings
.hass=${this.hass} .hass=${this.hass}
.entityId=${this.entry.entity_id}
.entry=${this.entry} .entry=${this.entry}
.exposed=${this._calculateExposed(this.entry)}
></entity-voice-settings>`; ></entity-voice-settings>`;
} }

View File

@ -5,11 +5,12 @@ import {
CSSResultGroup, CSSResultGroup,
html, html,
LitElement, LitElement,
PropertyValues,
nothing, nothing,
PropertyValues,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { dynamicElement } from "../../common/dom/dynamic-element-directive"; import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import "../../components/ha-alert";
import { import {
EntityRegistryEntry, EntityRegistryEntry,
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
@ -39,18 +40,17 @@ export class HaMoreInfoSettings extends LitElement {
if (this.entry === null) { if (this.entry === null) {
return html` return html`
<div class="content"> <div class="content">
${this.hass.localize( <ha-alert alert-type="warning">
"ui.dialogs.entity_registry.no_unique_id", ${this.hass.localize("ui.dialogs.entity_registry.no_unique_id", {
"entity_id", entity_id: this.entityId,
this.entityId, faq_link: html`<a
"faq_link", href=${documentationUrl(this.hass, "/faq/unique_id")}
html`<a target="_blank"
href=${documentationUrl(this.hass, "/faq/unique_id")} rel="noreferrer"
target="_blank" >${this.hass.localize("ui.dialogs.entity_registry.faq")}</a
rel="noreferrer" >`,
>${this.hass.localize("ui.dialogs.entity_registry.faq")}</a })}
>` </ha-alert>
)}
</div> </div>
`; `;
} }

View File

@ -1,5 +1,6 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { import {
mdiAlertCircle,
mdiChevronDown, mdiChevronDown,
mdiClose, mdiClose,
mdiHelpCircleOutline, mdiHelpCircleOutline,
@ -14,6 +15,7 @@ import {
LitElement, LitElement,
nothing, nothing,
PropertyValues, PropertyValues,
TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, query, state } from "lit/decorators"; import { customElement, property, query, state } from "lit/decorators";
import { LocalStorage } from "../../common/decorators/local-storage"; import { LocalStorage } from "../../common/decorators/local-storage";
@ -42,7 +44,7 @@ import { showAlertDialog } from "../generic/show-dialog-box";
interface Message { interface Message {
who: string; who: string;
text?: string; text?: string | TemplateResult;
error?: boolean; error?: boolean;
} }
@ -109,7 +111,10 @@ export class HaVoiceCommandDialog extends LitElement {
if (!this._opened) { if (!this._opened) {
return nothing; return nothing;
} }
const supportsSTT = this._pipeline?.stt_engine && AudioRecorder.isSupported;
const supportsMicrophone = AudioRecorder.isSupported;
const supportsSTT = this._pipeline?.stt_engine;
return html` return html`
<ha-dialog <ha-dialog
open open
@ -150,10 +155,12 @@ export class HaVoiceCommandDialog extends LitElement {
.hasMeta=${pipeline.id === this._preferredPipeline} .hasMeta=${pipeline.id === this._preferredPipeline}
> >
${pipeline.name}${pipeline.id === this._preferredPipeline ${pipeline.name}${pipeline.id === this._preferredPipeline
? html`<ha-svg-icon ? html`
slot="meta" <ha-svg-icon
.path=${mdiStar} slot="meta"
></ha-svg-icon>` .path=${mdiStar}
></ha-svg-icon>
`
: nothing} : nothing}
</ha-list-item>` </ha-list-item>`
)} )}
@ -203,7 +210,7 @@ export class HaVoiceCommandDialog extends LitElement {
iconTrailing iconTrailing
> >
<span slot="trailingIcon"> <span slot="trailingIcon">
${this._showSendButton ${this._showSendButton || !supportsSTT
? html` ? html`
<ha-icon-button <ha-icon-button
class="listening-icon" class="listening-icon"
@ -215,8 +222,7 @@ export class HaVoiceCommandDialog extends LitElement {
> >
</ha-icon-button> </ha-icon-button>
` `
: supportsSTT : html`
? html`
${this._audioRecorder?.active ${this._audioRecorder?.active
? html` ? html`
<div class="bouncer"> <div class="bouncer">
@ -224,18 +230,27 @@ export class HaVoiceCommandDialog extends LitElement {
<div class="double-bounce2"></div> <div class="double-bounce2"></div>
</div> </div>
` `
: ""} : nothing}
<ha-icon-button
class="listening-icon" <div class="listening-icon">
.path=${mdiMicrophone} <ha-icon-button
@click=${this._toggleListening} .path=${mdiMicrophone}
.label=${this.hass.localize( @click=${this._toggleListening}
"ui.dialogs.voice_command.start_listening" .label=${this.hass.localize(
)} "ui.dialogs.voice_command.start_listening"
> )}
</ha-icon-button> >
` </ha-icon-button>
: ""} ${!supportsMicrophone
? html`
<ha-svg-icon
.path=${mdiAlertCircle}
class="unsupported"
></ha-svg-icon>
`
: null}
</div>
`}
</span> </span>
</ha-textfield> </ha-textfield>
${this._agentInfo && this._agentInfo.attribution ${this._agentInfo && this._agentInfo.attribution
@ -382,7 +397,14 @@ export class HaVoiceCommandDialog extends LitElement {
} }
} }
private _toggleListening() { private _toggleListening(ev) {
ev.stopPropagation();
ev.preventDefault();
const supportsMicrophone = AudioRecorder.isSupported;
if (!supportsMicrophone) {
this._showNotSupportedMessage();
return;
}
if (!this._audioRecorder?.active) { if (!this._audioRecorder?.active) {
this._startListening(); this._startListening();
} else { } else {
@ -390,6 +412,40 @@ export class HaVoiceCommandDialog extends LitElement {
} }
} }
private async _showNotSupportedMessage() {
this._addMessage({
who: "hass",
text: html`
<p>
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone"
)}
</p>
<p>
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation",
{
documentation_link: html`
<a
target="_blank"
rel="noopener noreferrer"
href=${documentationUrl(
this.hass,
"/docs/configuration/securing/#remote-access"
)}
>
${this.hass.localize(
"ui.dialogs.voice_command.not_supported_microphone_documentation_link"
)}
</a>
`,
}
)}
</p>
`,
});
}
private async _startListening() { private async _startListening() {
this._audio?.pause(); this._audio?.pause();
if (!this._audioRecorder) { if (!this._audioRecorder) {
@ -561,7 +617,8 @@ export class HaVoiceCommandDialog extends LitElement {
return [ return [
haStyleDialog, haStyleDialog,
css` css`
ha-icon-button.listening-icon { .listening-icon {
position: relative;
color: var(--secondary-text-color); color: var(--secondary-text-color);
margin-right: -24px; margin-right: -24px;
margin-inline-end: -24px; margin-inline-end: -24px;
@ -569,10 +626,18 @@ export class HaVoiceCommandDialog extends LitElement {
direction: var(--direction); direction: var(--direction);
} }
ha-icon-button.listening-icon[active] { .listening-icon[active] {
color: var(--primary-color); color: var(--primary-color);
} }
.unsupported {
color: var(--error-color);
position: absolute;
--mdc-icon-size: 16px;
right: 5px;
top: 0px;
}
ha-dialog { ha-dialog {
--primary-action-button-flex: 1; --primary-action-button-flex: 1;
--secondary-action-button-flex: 0; --secondary-action-button-flex: 0;
@ -616,6 +681,19 @@ export class HaVoiceCommandDialog extends LitElement {
ha-button-menu ha-button ha-svg-icon { ha-button-menu ha-button ha-svg-icon {
height: 28px; height: 28px;
margin-left: 4px; margin-left: 4px;
margin-inline-start: 4px;
margin-inline-end: 4px;
direction: var(--direction);
}
ha-list-item {
--mdc-list-item-meta-size: 16px;
}
ha-list-item ha-svg-icon {
margin-left: 4px;
margin-inline-start: 4px;
margin-inline-end: 4px;
direction: var(--direction);
display: block;
} }
ha-button-menu a { ha-button-menu a {
text-decoration: none; text-decoration: none;
@ -648,6 +726,7 @@ export class HaVoiceCommandDialog extends LitElement {
display: block; display: block;
height: 400px; height: 400px;
box-sizing: border-box; box-sizing: border-box;
position: relative;
} }
@media all and (max-width: 450px), all and (max-height: 500px) { @media all and (max-width: 450px), all and (max-height: 500px) {
ha-dialog { ha-dialog {
@ -655,6 +734,7 @@ export class HaVoiceCommandDialog extends LitElement {
} }
.messages { .messages {
height: 100%; height: 100%;
flex: 1;
} }
} }
.messages-container { .messages-container {
@ -674,6 +754,12 @@ export class HaVoiceCommandDialog extends LitElement {
padding: 8px; padding: 8px;
border-radius: 15px; border-radius: 15px;
} }
.message p {
margin: 0;
}
.message p:not(:last-child) {
margin-bottom: 8px;
}
.message.user { .message.user {
margin-left: 24px; margin-left: 24px;

View File

@ -11,7 +11,6 @@ import { customElement, property } from "lit/decorators";
import { atLeastVersion } from "../common/config/version"; import { atLeastVersion } from "../common/config/version";
import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element";
import "../components/ha-card"; import "../components/ha-card";
import "../resources/ha-style";
import { haStyle } from "../resources/styles"; import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./hass-subpage"; import "./hass-subpage";

View File

@ -21,7 +21,6 @@ import {
subscribeEntityRegistry, subscribeEntityRegistry,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
@ -222,10 +221,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {
return css` return css`
hass-loading-screen {
--app-header-background-color: var(--sidebar-background-color);
--app-header-text-color: var(--sidebar-text-color);
}
.container { .container {
display: grid; display: grid;
grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); grid-template-columns: repeat(auto-fit, minmax(250px, 1fr));

View File

@ -8,6 +8,7 @@ import "../../../../components/ha-card";
import "../../../../components/ha-select"; import "../../../../components/ha-select";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import "../../../../components/ha-switch"; import "../../../../components/ha-switch";
import "../../../../components/ha-language-picker";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
import { import {
CloudTTSInfo, CloudTTSInfo,
@ -54,34 +55,33 @@ export class CloudTTSPref extends LitElement {
'"tts.cloud_say"' '"tts.cloud_say"'
)} )}
<br /><br /> <br /><br />
<div class="row">
<ha-language-picker
.hass=${this.hass}
.label=${this.hass.localize(
"ui.panel.config.cloud.account.tts.default_language"
)}
.disabled=${this.savingPreferences}
.value=${defaultVoice[0]}
.languages=${languages}
@value-changed=${this._handleLanguageChange}
>
</ha-language-picker>
<ha-select <ha-select
.label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.cloud.account.tts.default_language" "ui.panel.config.cloud.account.tts.default_gender"
)} )}
.disabled=${this.savingPreferences} .disabled=${this.savingPreferences}
.value=${defaultVoice[0]} .value=${defaultVoice[1]}
@selected=${this._handleLanguageChange} @selected=${this._handleGenderChange}
> >
${languages.map( ${genders.map(
([key, label]) => ([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>` html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)} )}
</ha-select> </ha-select>
</div>
<ha-select
.label=${this.hass.localize(
"ui.panel.config.cloud.account.tts.default_gender"
)}
.disabled=${this.savingPreferences}
.value=${defaultVoice[1]}
@selected=${this._handleGenderChange}
>
${genders.map(
([key, label]) =>
html`<mwc-list-item .value=${key}>${label}</mwc-list-item>`
)}
</ha-select>
</div> </div>
<div class="card-actions"> <div class="card-actions">
<mwc-button @click=${this._openTryDialog}> <mwc-button @click=${this._openTryDialog}>
@ -115,11 +115,11 @@ export class CloudTTSPref extends LitElement {
} }
async _handleLanguageChange(ev) { async _handleLanguageChange(ev) {
if (ev.target.value === this.cloudStatus!.prefs.tts_default_voice[0]) { if (ev.detail.value === this.cloudStatus!.prefs.tts_default_voice[0]) {
return; return;
} }
this.savingPreferences = true; this.savingPreferences = true;
const language = ev.target.value; const language = ev.detail.value;
const curGender = this.cloudStatus!.prefs.tts_default_voice[1]; const curGender = this.cloudStatus!.prefs.tts_default_voice[1];
const genders = this.getSupportedGenders( const genders = this.getSupportedGenders(
@ -185,6 +185,18 @@ export class CloudTTSPref extends LitElement {
right: auto; right: auto;
left: 24px; left: 24px;
} }
.row {
display: flex;
}
.row > * {
flex: 1;
}
.row > *:first-child {
margin-right: 8px;
}
.row > *:last-child {
margin-left: 8px;
}
.card-actions { .card-actions {
display: flex; display: flex;
flex-direction: row-reverse; flex-direction: row-reverse;

View File

@ -62,7 +62,6 @@ import {
} from "../../../dialogs/generic/show-dialog-box"; } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-error-screen";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";

View File

@ -33,7 +33,6 @@ import { customElement, property, state } from "lit/decorators";
import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { listenMediaQuery } from "../../common/dom/media_query"; import { listenMediaQuery } from "../../common/dom/media_query";
import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud";
import "../../layouts/hass-loading-screen";
import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page";
import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { PageNavigation } from "../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../types"; import { HomeAssistant, Route } from "../../types";

View File

@ -174,9 +174,11 @@ export class HaIntegrationCard extends LitElement {
${this.items.map( ${this.items.map(
(item) => (item) =>
html`<ha-list-item html`<ha-list-item
dense
hasMeta hasMeta
.entryId=${item.entry_id} .entryId=${item.entry_id}
@click=${this._selectConfigEntry} @click=${this._selectConfigEntry}
class="config-entry"
>${item.title || >${item.title ||
this.hass.localize( this.hass.localize(
"ui.panel.config.integrations.config_entry.unnamed_entry" "ui.panel.config.integrations.config_entry.unnamed_entry"
@ -1026,6 +1028,9 @@ export class HaIntegrationCard extends LitElement {
ha-list-item ha-svg-icon { ha-list-item ha-svg-icon {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
.config-entry {
height: 36px;
}
ha-icon-next { ha-icon-next {
width: 24px; width: 24px;
} }

View File

@ -13,7 +13,6 @@ import {
ZHADeviceEndpoint, ZHADeviceEndpoint,
ZHAGroup, ZHAGroup,
} from "../../../../../data/zha"; } from "../../../../../data/zha";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-subpage"; import "../../../../../layouts/hass-subpage";
import type { PolymerChangedEvent } from "../../../../../polymer-types"; import type { PolymerChangedEvent } from "../../../../../polymer-types";
import type { HomeAssistant } from "../../../../../types"; import type { HomeAssistant } from "../../../../../types";

View File

@ -39,6 +39,8 @@ import {
ZwaveJSNodeMetadata, ZwaveJSNodeMetadata,
ZWaveJSSetConfigParamResult, ZWaveJSSetConfigParamResult,
} from "../../../../../data/zwave_js"; } from "../../../../../data/zwave_js";
import "../../../../../layouts/hass-error-screen";
import "../../../../../layouts/hass-loading-screen";
import "../../../../../layouts/hass-tabs-subpage"; import "../../../../../layouts/hass-tabs-subpage";
import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../../resources/styles"; import { haStyle } from "../../../../../resources/styles";

View File

@ -9,7 +9,6 @@ import "../../../components/search-input";
import { LogProvider } from "../../../data/error_log"; import { LogProvider } from "../../../data/error_log";
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon"; import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
import "../../../layouts/hass-subpage"; import "../../../layouts/hass-subpage";
import "../../../layouts/hass-tabs-subpage";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import "./error-log-card"; import "./error-log-card";

View File

@ -9,7 +9,6 @@ import "../../../components/ha-list-item";
import "../../../components/ha-svg-icon"; import "../../../components/ha-svg-icon";
import { domainToName } from "../../../data/integration"; import { domainToName } from "../../../data/integration";
import type { RepairsIssue } from "../../../data/repairs"; import type { RepairsIssue } from "../../../data/repairs";
import "../../../layouts/hass-subpage";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { showRepairsFlowDialog } from "./show-dialog-repair-flow"; import { showRepairsFlowDialog } from "./show-dialog-repair-flow";

View File

@ -1,9 +1,10 @@
import { css, CSSResultGroup, html, LitElement } from "lit"; import { css, CSSResultGroup, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { SchemaUnion } from "../../../../components/ha-form/types"; import { LocalizeKeys } from "../../../../common/translations/localize";
import { AssistPipeline } from "../../../../data/assist_pipeline"; import { AssistPipeline } from "../../../../data/assist_pipeline";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import "../../../../components/ha-form/ha-form";
@customElement("assist-pipeline-detail-config") @customElement("assist-pipeline-detail-config")
export class AssistPipelineDetailConfig extends LitElement { export class AssistPipelineDetailConfig extends LitElement {
@ -33,26 +34,28 @@ export class AssistPipelineDetailConfig extends LitElement {
text: {}, text: {},
}, },
}, },
{ supportedLanguages
name: "language", ? {
required: true, name: "language",
selector: { required: true,
language: { selector: {
languages: supportedLanguages ?? [], language: {
}, languages: supportedLanguages,
}, },
}, },
}
: { name: "", type: "constant" },
] as const, ] as const,
}, },
] as const ] as const
); );
private _computeLabel = ( private _computeLabel = (schema): string =>
schema: SchemaUnion<ReturnType<typeof this._schema>> schema.name
): string => ? this.hass.localize(
this.hass.localize( `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys
`ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` )
); : "";
protected render() { protected render() {
return html` return html`

View File

@ -20,7 +20,7 @@ import {
updateAssistPipeline, updateAssistPipeline,
} from "../../../data/assist_pipeline"; } from "../../../data/assist_pipeline";
import { CloudStatus } from "../../../data/cloud"; import { CloudStatus } from "../../../data/cloud";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; import { ExposeEntitySettings } from "../../../data/expose";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
@ -29,7 +29,10 @@ import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assi
export class AssistPref extends LitElement { export class AssistPref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() private extEntities?: Record<string, ExtEntityRegistryEntry>; @property({ attribute: false }) public exposedEntities?: Record<
string,
ExposeEntitySettings
>;
@state() private _pipelines: AssistPipeline[] = []; @state() private _pipelines: AssistPipeline[] = [];
@ -46,11 +49,10 @@ export class AssistPref extends LitElement {
}); });
} }
private _exposedEntities = memoizeOne( private _exposedEntitiesCount = memoizeOne(
(extEntities: Record<string, ExtEntityRegistryEntry>) => (exposedEntities: Record<string, ExposeEntitySettings>) =>
Object.values(extEntities).filter( Object.values(exposedEntities).filter((expose) => expose.conversation)
(entity) => entity.options?.conversation?.should_expose .length
).length
); );
protected render() { protected render() {
@ -119,8 +121,8 @@ export class AssistPref extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.voice_assistants.assistants.pipeline.exposed_entities", "ui.panel.config.voice_assistants.assistants.pipeline.exposed_entities",
{ {
number: this.extEntities number: this.exposedEntities
? this._exposedEntities(this.extEntities) ? this._exposedEntitiesCount(this.exposedEntities)
: 0, : 0,
} }
)} )}

View File

@ -11,28 +11,30 @@ import "../../../components/ha-settings-row";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import type { HaSwitch } from "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
import { import {
ExposeEntitySettings,
getExposeNewEntities, getExposeNewEntities,
setExposeNewEntities, setExposeNewEntities,
} from "../../../data/voice"; } from "../../../data/expose";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
export class CloudAlexaPref extends LitElement { export class CloudAlexaPref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() private extEntities?: Record<string, ExtEntityRegistryEntry>; @property({ attribute: false }) public exposedEntities?: Record<
string,
ExposeEntitySettings
>;
@property() public cloudStatus?: CloudStatusLoggedIn; @property() public cloudStatus?: CloudStatusLoggedIn;
@state() private _exposeNew?: boolean; @state() private _exposeNew?: boolean;
private _exposedEntities = memoizeOne( private _exposedEntitiesCount = memoizeOne(
(extEntities: Record<string, ExtEntityRegistryEntry>) => (exposedEntities: Record<string, ExposeEntitySettings>) =>
Object.values(extEntities).filter( Object.values(exposedEntities).filter((expose) => expose["cloud.alexa"])
(entity) => entity.options?.["cloud.alexa"]?.should_expose .length
).length
); );
protected willUpdate() { protected willUpdate() {
@ -183,8 +185,8 @@ export class CloudAlexaPref extends LitElement {
: this.hass.localize( : this.hass.localize(
"ui.panel.config.cloud.account.alexa.exposed_entities", "ui.panel.config.cloud.account.alexa.exposed_entities",
{ {
number: this.extEntities number: this.exposedEntities
? this._exposedEntities(this.extEntities) ? this._exposedEntitiesCount(this.exposedEntities)
: 0, : 0,
} }
)} )}

View File

@ -4,6 +4,7 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { isEmptyFilter } from "../../../common/entity/entity_filter";
import "../../../components/ha-alert"; import "../../../components/ha-alert";
import "../../../components/ha-card"; import "../../../components/ha-card";
import "../../../components/ha-settings-row"; import "../../../components/ha-settings-row";
@ -11,20 +12,22 @@ import type { HaSwitch } from "../../../components/ha-switch";
import "../../../components/ha-textfield"; import "../../../components/ha-textfield";
import type { HaTextField } from "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { isEmptyFilter } from "../../../common/entity/entity_filter";
import { import {
ExposeEntitySettings,
getExposeNewEntities, getExposeNewEntities,
setExposeNewEntities, setExposeNewEntities,
} from "../../../data/voice"; } from "../../../data/expose";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; import { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url";
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
export class CloudGooglePref extends LitElement { export class CloudGooglePref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() private extEntities?: Record<string, ExtEntityRegistryEntry>; @property({ attribute: false }) public exposedEntities?: Record<
string,
ExposeEntitySettings
>;
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn; @property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
@ -40,10 +43,10 @@ export class CloudGooglePref extends LitElement {
} }
} }
private _exposedEntities = memoizeOne( private _exposedEntitiesCount = memoizeOne(
(extEntities: Record<string, ExtEntityRegistryEntry>) => (exposedEntities: Record<string, ExposeEntitySettings>) =>
Object.values(extEntities).filter( Object.values(exposedEntities).filter(
(entity) => entity.options?.["cloud.google_assistant"]?.should_expose (expose) => expose["cloud.google_assistant"]
).length ).length
); );
@ -239,8 +242,8 @@ export class CloudGooglePref extends LitElement {
: this.hass.localize( : this.hass.localize(
"ui.panel.config.cloud.account.google.exposed_entities", "ui.panel.config.cloud.account.google.exposed_entities",
{ {
number: this.extEntities number: this.exposedEntities
? this._exposedEntities(this.extEntities) ? this._exposedEntitiesCount(this.exposedEntities)
: 0, : 0,
} }
)} )}

View File

@ -1,18 +1,16 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-list"; import "@material/mwc-list";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-check-list-item"; import "../../../components/ha-check-list-item";
import "../../../components/search-input"; import "../../../components/search-input";
import { import { ExposeEntitySettings, voiceAssistants } from "../../../data/expose";
computeEntityRegistryName,
ExtEntityRegistryEntry,
} from "../../../data/entity_registry";
import { voiceAssistants } from "../../../data/voice";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import "./entity-voice-settings"; import "./entity-voice-settings";
@ -49,7 +47,7 @@ class DialogExposeEntity extends LitElement {
); );
const entities = this._filterEntities( const entities = this._filterEntities(
this._params.extendedEntities, this._params.exposedEntities,
this._filter this._filter
); );
@ -126,42 +124,40 @@ class DialogExposeEntity extends LitElement {
} }
private _filterEntities = memoizeOne( private _filterEntities = memoizeOne(
(RegEntries: Record<string, ExtEntityRegistryEntry>, filter?: string) => { (
exposedEntities: Record<string, ExposeEntitySettings>,
filter?: string
) => {
const lowerFilter = filter?.toLowerCase(); const lowerFilter = filter?.toLowerCase();
return Object.values(RegEntries).filter( return Object.values(this.hass.states).filter(
(entity) => (entity) =>
this._params!.filterAssistants.some( this._params!.filterAssistants.some(
(ass) => !entity.options?.[ass]?.should_expose (ass) => !exposedEntities[entity.entity_id]?.[ass]
) && ) &&
(!lowerFilter || (!lowerFilter ||
entity.entity_id.toLowerCase().includes(lowerFilter) || entity.entity_id.toLowerCase().includes(lowerFilter) ||
computeEntityRegistryName(this.hass!, entity) computeStateName(entity)?.toLowerCase().includes(lowerFilter))
?.toLowerCase()
.includes(lowerFilter))
); );
} }
); );
private _renderItem = (entity: ExtEntityRegistryEntry) => { private _renderItem = (entityState: HassEntity) => html`
const entityState = this.hass.states[entity.entity_id]; <ha-check-list-item
return html` graphic="icon"
<ha-check-list-item twoLine
graphic="icon" .value=${entityState.entity_id}
twoLine .selected=${this._selected.includes(entityState.entity_id)}
.value=${entity.entity_id} @request-selected=${this._handleSelected}
.selected=${this._selected.includes(entity.entity_id)} >
@request-selected=${this._handleSelected} <ha-state-icon
> title=${ifDefined(entityState?.state)}
<ha-state-icon slot="graphic"
title=${ifDefined(entityState?.state)} .state=${entityState}
slot="graphic" ></ha-state-icon>
.state=${entityState} ${computeStateName(entityState)}
></ha-state-icon> <span slot="secondary">${entityState.entity_id}</span>
${computeEntityRegistryName(this.hass!, entity)} </ha-check-list-item>
<span slot="secondary">${entity.entity_id}</span> `;
</ha-check-list-item>
`;
};
private _expose() { private _expose() {
this._params!.exposeEntities(this._selected); this._params!.exposeEntities(this._selected);

View File

@ -44,7 +44,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement {
@state() private _submitting = false; @state() private _submitting = false;
@state() private _supportedLanguages: string[] = []; @state() private _supportedLanguages?: string[];
public showDialog(params: VoiceAssistantPipelineDetailsDialogParams): void { public showDialog(params: VoiceAssistantPipelineDetailsDialogParams): void {
this._params = params; this._params = params;

View File

@ -1,38 +1,31 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { import { computeStateName } from "../../../common/entity/compute_state_name";
ExtEntityRegistryEntry, import { createCloseHeading } from "../../../components/ha-dialog";
computeEntityRegistryName,
getExtendedEntityRegistryEntry,
} from "../../../data/entity_registry";
import { haStyle, haStyleDialog } from "../../../resources/styles"; import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { VoiceSettingsDialogParams } from "./show-dialog-voice-settings";
import "./entity-voice-settings"; import "./entity-voice-settings";
import { createCloseHeading } from "../../../components/ha-dialog"; import { VoiceSettingsDialogParams } from "./show-dialog-voice-settings";
@customElement("dialog-voice-settings") @customElement("dialog-voice-settings")
class DialogVoiceSettings extends LitElement { class DialogVoiceSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@state() private _extEntityReg?: ExtEntityRegistryEntry; @state() private _params?: VoiceSettingsDialogParams;
public async showDialog(params: VoiceSettingsDialogParams): Promise<void> { public showDialog(params: VoiceSettingsDialogParams): void {
this._extEntityReg = await getExtendedEntityRegistryEntry( this._params = params;
this.hass,
params.entityId
);
} }
public closeDialog(): void { public closeDialog(): void {
this._extEntityReg = undefined; this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
protected render() { protected render() {
if (!this._extEntityReg) { if (!this._params) {
return nothing; return nothing;
} }
@ -43,15 +36,18 @@ class DialogVoiceSettings extends LitElement {
hideActions hideActions
.heading=${createCloseHeading( .heading=${createCloseHeading(
this.hass, this.hass,
computeEntityRegistryName(this.hass, this._extEntityReg) || computeStateName(this.hass.states[this._params.entityId]) ||
this.hass.localize("ui.panel.config.entities.picker.unnamed_entity") this.hass.localize("ui.panel.config.entities.picker.unnamed_entity")
)} )}
> >
<div> <div>
<entity-voice-settings <entity-voice-settings
.hass=${this.hass} .hass=${this.hass}
.entry=${this._extEntityReg} .entityId=${this._params.entityId}
.entry=${this._params.extEntityReg}
.exposed=${this._params.exposed}
@entity-entry-updated=${this._entityEntryUpdated} @entity-entry-updated=${this._entityEntryUpdated}
@exposed-entities-changed=${this._exposedEntitiesChanged}
></entity-voice-settings> ></entity-voice-settings>
</div> </div>
</ha-dialog> </ha-dialog>
@ -59,7 +55,11 @@ class DialogVoiceSettings extends LitElement {
} }
private _entityEntryUpdated(ev: CustomEvent) { private _entityEntryUpdated(ev: CustomEvent) {
this._extEntityReg = ev.detail; this._params!.extEntityReg = ev.detail;
}
private _exposedEntitiesChanged() {
this._params!.exposedEntitiesChanged?.();
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -36,18 +36,27 @@ import {
fetchCloudGoogleEntity, fetchCloudGoogleEntity,
GoogleEntity, GoogleEntity,
} from "../../../data/google_assistant"; } from "../../../data/google_assistant";
import { exposeEntities, voiceAssistants } from "../../../data/voice"; import {
exposeEntities,
ExposeEntitySettings,
voiceAssistants,
} from "../../../data/expose";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types"; import type { HomeAssistant } from "../../../types";
import { brandsUrl } from "../../../util/brands-url"; import { brandsUrl } from "../../../util/brands-url";
import { EntityRegistrySettings } from "../entities/entity-registry-settings"; import { EntityRegistrySettings } from "../entities/entity-registry-settings";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("entity-voice-settings") @customElement("entity-voice-settings")
export class EntityVoiceSettings extends SubscribeMixin(LitElement) { export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Object }) public entry!: ExtEntityRegistryEntry; @property() public entityId!: string;
@property({ attribute: false }) public exposed!: ExposeEntitySettings;
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
@state() private _cloudStatus?: CloudStatus; @state() private _cloudStatus?: CloudStatus;
@ -63,7 +72,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
if (!isComponentLoaded(this.hass, "cloud")) { if (!isComponentLoaded(this.hass, "cloud")) {
return; return;
} }
if (changedProps.has("entry") && this.entry) { if (changedProps.has("entityId") && this.entityId) {
this._fetchEntities(); this._fetchEntities();
} }
if (!this.hasUpdated) { if (!this.hasUpdated) {
@ -77,7 +86,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
try { try {
const googleEntity = await fetchCloudGoogleEntity( const googleEntity = await fetchCloudGoogleEntity(
this.hass, this.hass,
this.entry.entity_id this.entityId
); );
this._googleEntity = googleEntity; this._googleEntity = googleEntity;
this.requestUpdate("_googleEntity"); this.requestUpdate("_googleEntity");
@ -89,7 +98,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
} }
try { try {
await fetchCloudAlexaEntity(this.hass, this.entry.entity_id); await fetchCloudAlexaEntity(this.hass, this.entityId);
} catch (err: any) { } catch (err: any) {
if (err.code === "not_supported") { if (err.code === "not_supported") {
this._unsupported["cloud.alexa"] = true; this._unsupported["cloud.alexa"] = true;
@ -153,9 +162,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
uiAssistants.splice(uiAssistants.indexOf("cloud.alexa"), 1); uiAssistants.splice(uiAssistants.indexOf("cloud.alexa"), 1);
} }
const uiExposed = uiAssistants.some( const uiExposed = uiAssistants.some((key) => this.exposed[key]);
(key) => this.entry.options?.[key]?.should_expose
);
let manFilterFuncs: let manFilterFuncs:
| { | {
@ -171,10 +178,9 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
); );
} }
const manExposedAlexa = const manExposedAlexa = alexaManual && manFilterFuncs!.alexa(this.entityId);
alexaManual && manFilterFuncs!.alexa(this.entry.entity_id);
const manExposedGoogle = const manExposedGoogle =
googleManual && manFilterFuncs!.google(this.entry.entity_id); googleManual && manFilterFuncs!.google(this.entityId);
const anyExposed = uiExposed || manExposedAlexa || manExposedGoogle; const anyExposed = uiExposed || manExposedAlexa || manExposedGoogle;
@ -198,13 +204,14 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
? manExposedAlexa ? manExposedAlexa
: googleManual && key === "cloud.google_assistant" : googleManual && key === "cloud.google_assistant"
? manExposedGoogle ? manExposedGoogle
: this.entry.options?.[key]?.should_expose; : this.exposed[key];
const manualConfig = const manualConfig =
(alexaManual && key === "cloud.alexa") || (alexaManual && key === "cloud.alexa") ||
(googleManual && key === "cloud.google_assistant"); (googleManual && key === "cloud.google_assistant");
const support2fa = const support2fa =
this.entry &&
key === "cloud.google_assistant" && key === "cloud.google_assistant" &&
!googleManual && !googleManual &&
supported && supported &&
@ -249,7 +256,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
)} )}
> >
<ha-checkbox <ha-checkbox
.checked=${!this.entry.options?.[key]?.disable_2fa} .checked=${!this.entry!.options?.[key]?.disable_2fa}
@change=${this._2faChanged} @change=${this._2faChanged}
></ha-checkbox> ></ha-checkbox>
</ha-formfield> </ha-formfield>
@ -274,12 +281,26 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.dialogs.voice-settings.aliases_description")} ${this.hass.localize("ui.dialogs.voice-settings.aliases_description")}
</p> </p>
<ha-aliases-editor ${!this.entry
.hass=${this.hass} ? html`<ha-alert alert-type="warning">
.aliases=${this._aliases ?? this.entry.aliases} ${this.hass.localize(
@value-changed=${this._aliasesChanged} "ui.dialogs.voice-settings.aliases_no_unique_id",
@blur=${this._saveAliases} {
></ha-aliases-editor> faq_link: html`<a
href=${documentationUrl(this.hass, "/faq/unique_id")}
target="_blank"
rel="noreferrer"
>${this.hass.localize("ui.dialogs.entity_registry.faq")}</a
>`,
}
)}
</ha-alert>`
: html`<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases ?? this.entry.aliases}
@value-changed=${this._aliasesChanged}
@blur=${this._saveAliases}
></ha-aliases-editor>`}
`; `;
} }
@ -291,7 +312,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
try { try {
await updateCloudGoogleEntityConfig( await updateCloudGoogleEntityConfig(
this.hass, this.hass,
this.entry.entity_id, this.entityId,
!ev.target.checked !ev.target.checked
); );
} catch (_err) { } catch (_err) {
@ -303,15 +324,11 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
if (!this._aliases) { if (!this._aliases) {
return; return;
} }
const result = await updateEntityRegistryEntry( const result = await updateEntityRegistryEntry(this.hass, this.entityId, {
this.hass, aliases: this._aliases
this.entry.entity_id, .map((alias) => alias.trim())
{ .filter((alias) => alias),
aliases: this._aliases });
.map((alias) => alias.trim())
.filter((alias) => alias),
}
);
fireEvent(this, "entity-entry-updated", result.entity_entry); fireEvent(this, "entity-entry-updated", result.entity_entry);
} }
@ -319,14 +336,17 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
exposeEntities( exposeEntities(
this.hass, this.hass,
[ev.target.assistant], [ev.target.assistant],
[this.entry.entity_id], [this.entityId],
ev.target.checked ev.target.checked
); );
const entry = await getExtendedEntityRegistryEntry( if (this.entry) {
this.hass, const entry = await getExtendedEntityRegistryEntry(
this.entry.entity_id this.hass,
); this.entityId
fireEvent(this, "entity-entry-updated", entry); );
fireEvent(this, "entity-entry-updated", entry);
}
fireEvent(this, "exposed-entities-changed");
} }
private async _toggleAll(ev) { private async _toggleAll(ev) {
@ -336,17 +356,15 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
? ev.target.assistants.filter((key) => !this._unsupported[key]) ? ev.target.assistants.filter((key) => !this._unsupported[key])
: ev.target.assistants; : ev.target.assistants;
exposeEntities( exposeEntities(this.hass, assistants, [this.entityId], ev.target.checked);
this.hass, if (this.entry) {
assistants, const entry = await getExtendedEntityRegistryEntry(
[this.entry.entity_id], this.hass,
ev.target.checked this.entityId
); );
const entry = await getExtendedEntityRegistryEntry( fireEvent(this, "entity-entry-updated", entry);
this.hass, }
this.entry.entity_id fireEvent(this, "exposed-entities-changed");
);
fireEvent(this, "entity-entry-updated", entry);
} }
static get styles(): CSSResultGroup { static get styles(): CSSResultGroup {

View File

@ -3,7 +3,7 @@ import { mdiAlertCircle } from "@mdi/js";
import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { styleMap } from "lit/directives/style-map"; import { styleMap } from "lit/directives/style-map";
import { voiceAssistants } from "../../../../data/voice"; import { voiceAssistants } from "../../../../data/expose";
import { HomeAssistant } from "../../../../types"; import { HomeAssistant } from "../../../../types";
import { brandsUrl } from "../../../../util/brands-url"; import { brandsUrl } from "../../../../util/brands-url";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";

View File

@ -1,16 +1,13 @@
import { consume } from "@lit-labs/context";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import { css, html, LitElement, nothing, PropertyValues } from "lit"; import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../common/util/compute_rtl";
import { CloudStatus } from "../../../data/cloud"; import { CloudStatus } from "../../../data/cloud";
import { entitiesContext } from "../../../data/context"; import { ExposeEntitySettings } from "../../../data/expose";
import {
ExtEntityRegistryEntry, import "../../../layouts/hass-loading-screen";
getExtendedEntityRegistryEntries,
} from "../../../data/entity_registry";
import "../../../layouts/hass-tabs-subpage"; import "../../../layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import "./assist-pref"; import "./assist-pref";
@ -25,18 +22,17 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
@property({ attribute: false }) public cloudStatus?: CloudStatus; @property({ attribute: false }) public cloudStatus?: CloudStatus;
@property({ attribute: false }) public exposedEntities?: Record<
string,
ExposeEntitySettings
>;
@property() public isWide!: boolean; @property() public isWide!: boolean;
@property() public narrow!: boolean; @property() public narrow!: boolean;
@property() public route!: Route; @property() public route!: Route;
@state()
@consume({ context: entitiesContext, subscribe: true })
_entities!: HomeAssistant["entities"];
@state() private _extEntities?: Record<string, ExtEntityRegistryEntry>;
protected render() { protected render() {
if (!this.hass) { if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`; return html`<hass-loading-screen></hass-loading-screen>`;
@ -56,7 +52,7 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
<assist-pref <assist-pref
.hass=${this.hass} .hass=${this.hass}
.cloudStatus=${this.cloudStatus} .cloudStatus=${this.cloudStatus}
.extEntities=${this._extEntities} .exposedEntities=${this.exposedEntities}
></assist-pref> ></assist-pref>
` `
: nothing} : nothing}
@ -64,13 +60,13 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
? html` ? html`
<cloud-alexa-pref <cloud-alexa-pref
.hass=${this.hass} .hass=${this.hass}
.extEntities=${this._extEntities} .exposedEntities=${this.exposedEntities}
.cloudStatus=${this.cloudStatus} .cloudStatus=${this.cloudStatus}
dir=${computeRTLDirection(this.hass)} dir=${computeRTLDirection(this.hass)}
></cloud-alexa-pref> ></cloud-alexa-pref>
<cloud-google-pref <cloud-google-pref
.hass=${this.hass} .hass=${this.hass}
.extEntities=${this._extEntities} .exposedEntities=${this.exposedEntities}
.cloudStatus=${this.cloudStatus} .cloudStatus=${this.cloudStatus}
dir=${computeRTLDirection(this.hass)} dir=${computeRTLDirection(this.hass)}
></cloud-google-pref> ></cloud-google-pref>
@ -81,19 +77,6 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
`; `;
} }
public willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("_entities")) {
this._fetchExtendedEntities();
}
}
private async _fetchExtendedEntities() {
this._extEntities = await getExtendedEntityRegistryEntries(
this.hass,
Object.keys(this._entities)
);
}
static styles = css` static styles = css`
.content { .content {
padding: 28px 20px 0; padding: 28px 20px 0;

View File

@ -19,7 +19,8 @@ import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined"; import { ifDefined } from "lit/directives/if-defined";
import memoize from "memoize-one"; import memoize from "memoize-one";
import { HASSDomEvent } from "../../../common/dom/fire_event"; import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import { import {
EntityFilter, EntityFilter,
generateFilter, generateFilter,
@ -38,26 +39,28 @@ import { AlexaEntity, fetchCloudAlexaEntities } from "../../../data/alexa";
import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud"; import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
import { entitiesContext } from "../../../data/context"; import { entitiesContext } from "../../../data/context";
import { import {
computeEntityRegistryName,
EntityRegistryEntry,
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
getExtendedEntityRegistryEntries, getExtendedEntityRegistryEntries,
} from "../../../data/entity_registry"; } from "../../../data/entity_registry";
import {
exposeEntities,
ExposeEntitySettings,
voiceAssistants,
} from "../../../data/expose";
import { import {
fetchCloudGoogleEntities, fetchCloudGoogleEntities,
GoogleEntity, GoogleEntity,
} from "../../../data/google_assistant"; } from "../../../data/google_assistant";
import { exposeEntities, voiceAssistants } from "../../../data/voice";
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-loading-screen";
import "../../../layouts/hass-tabs-subpage-data-table"; import "../../../layouts/hass-tabs-subpage-data-table";
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
import { haStyle } from "../../../resources/styles"; import { haStyle } from "../../../resources/styles";
import { HomeAssistant, Route } from "../../../types"; import { HomeAssistant, Route } from "../../../types";
import "./expose/expose-assistant-icon";
import { voiceAssistantTabs } from "./ha-config-voice-assistants"; import { voiceAssistantTabs } from "./ha-config-voice-assistants";
import { showExposeEntityDialog } from "./show-dialog-expose-entity"; import { showExposeEntityDialog } from "./show-dialog-expose-entity";
import { showVoiceSettingsDialog } from "./show-dialog-voice-settings"; import { showVoiceSettingsDialog } from "./show-dialog-voice-settings";
import "./expose/expose-assistant-icon";
@customElement("ha-config-voice-assistants-expose") @customElement("ha-config-voice-assistants-expose")
export class VoiceAssistantsExpose extends LitElement { export class VoiceAssistantsExpose extends LitElement {
@ -71,6 +74,11 @@ export class VoiceAssistantsExpose extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public exposedEntities?: Record<
string,
ExposeEntitySettings
>;
@state() @state()
@consume({ context: entitiesContext, subscribe: true }) @consume({ context: entitiesContext, subscribe: true })
_entities!: HomeAssistant["entities"]; _entities!: HomeAssistant["entities"];
@ -256,8 +264,8 @@ export class VoiceAssistantsExpose extends LitElement {
private _filteredEntities = memoize( private _filteredEntities = memoize(
( (
entities: HomeAssistant["entities"], entities: Record<string, ExtEntityRegistryEntry>,
extEntities: Record<string, ExtEntityRegistryEntry> | undefined, exposedEntities: Record<string, ExposeEntitySettings>,
devices: HomeAssistant["devices"], devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"], areas: HomeAssistant["areas"],
cloudStatus: CloudStatus | undefined, cloudStatus: CloudStatus | undefined,
@ -296,12 +304,11 @@ export class VoiceAssistantsExpose extends LitElement {
const result: Record<string, DataTableRowData> = {}; const result: Record<string, DataTableRowData> = {};
let filteredEntities = Object.values(entities); let filteredEntities = Object.values(this.hass.states);
filteredEntities = filteredEntities.filter((entity) => filteredEntities = filteredEntities.filter((entity) =>
showAssistants.some( showAssistants.some(
(assis) => (assis) => exposedEntities?.[entity.entity_id]?.[assis]
extEntities?.[entity.entity_id].options?.[assis]?.should_expose
) )
); );
@ -317,37 +324,38 @@ export class VoiceAssistantsExpose extends LitElement {
filteredAssistants.some( filteredAssistants.some(
(assis) => (assis) =>
!(assis === "cloud.alexa" && alexaManual) && !(assis === "cloud.alexa" && alexaManual) &&
extEntities?.[entity.entity_id].options?.[assis]?.should_expose exposedEntities?.[entity.entity_id]?.[assis]
) )
); );
} }
}); });
for (const entry of filteredEntities) { for (const entityState of filteredEntities) {
const entity = this.hass.states[entry.entity_id]; const entry: ExtEntityRegistryEntry | undefined =
const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id; entities[entityState.entity_id];
const areaId =
entry?.area_id ?? entry?.device_id
? devices[entry.device_id!]?.area_id
: undefined;
const area = areaId ? areas[areaId] : undefined; const area = areaId ? areas[areaId] : undefined;
result[entry.entity_id] = { result[entityState.entity_id] = {
entity_id: entry.entity_id, entity_id: entityState.entity_id,
entity, entity: entityState,
name: name:
computeEntityRegistryName( computeStateName(entityState) ||
this.hass!,
entry as EntityRegistryEntry
) ||
this.hass.localize( this.hass.localize(
"ui.panel.config.entities.picker.unnamed_entity" "ui.panel.config.entities.picker.unnamed_entity"
), ),
area: area ? area.name : "—", area: area ? area.name : "—",
assistants: Object.keys( assistants: Object.keys(
extEntities![entry.entity_id].options! exposedEntities?.[entityState.entity_id]
).filter( ).filter(
(key) => (key) =>
showAssistants.includes(key) && showAssistants.includes(key) &&
extEntities![entry.entity_id].options![key]?.should_expose exposedEntities?.[entityState.entity_id]?.[key]
), ),
aliases: extEntities?.[entry.entity_id].aliases, aliases: entry?.aliases || [],
}; };
} }
@ -358,7 +366,7 @@ export class VoiceAssistantsExpose extends LitElement {
(this.cloudStatus as CloudStatusLoggedIn).google_entities, (this.cloudStatus as CloudStatusLoggedIn).google_entities,
(this.cloudStatus as CloudStatusLoggedIn).alexa_entities (this.cloudStatus as CloudStatusLoggedIn).alexa_entities
); );
Object.keys(entities).forEach((entityId) => { Object.keys(this.hass.states).forEach((entityId) => {
const assistants: string[] = []; const assistants: string[] = [];
if ( if (
alexaManual && alexaManual &&
@ -383,30 +391,33 @@ export class VoiceAssistantsExpose extends LitElement {
result[entityId].assistants.push(...assistants); result[entityId].assistants.push(...assistants);
result[entityId].manAssistants = assistants; result[entityId].manAssistants = assistants;
} else { } else {
const entry = entities[entityId]; const entityState = this.hass.states[entityId];
const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id; const entry: ExtEntityRegistryEntry | undefined =
entities[entityId];
const areaId =
entry?.area_id ?? entry?.device_id
? devices[entry.device_id!]?.area_id
: undefined;
const area = areaId ? areas[areaId] : undefined; const area = areaId ? areas[areaId] : undefined;
result[entityId] = { result[entityId] = {
entity_id: entry.entity_id, entity_id: entityState.entity_id,
entity: this.hass.states[entityId], entity: entityState,
name: computeEntityRegistryName( name: computeStateName(entityState),
this.hass!,
entry as EntityRegistryEntry
),
area: area ? area.name : "—", area: area ? area.name : "—",
assistants: [ assistants: [
...(extEntities ...(exposedEntities
? Object.keys(extEntities[entry.entity_id].options!).filter( ? Object.keys(
exposedEntities?.[entityState.entity_id]
).filter(
(key) => (key) =>
showAssistants.includes(key) && showAssistants.includes(key) &&
extEntities[entry.entity_id].options![key] exposedEntities?.[entityState.entity_id]?.[key]
?.should_expose
) )
: []), : []),
...assistants, ...assistants,
], ],
manAssistants: assistants, manAssistants: assistants,
aliases: extEntities?.[entityId].aliases, aliases: entry?.aliases || [],
}; };
} }
}); });
@ -468,14 +479,14 @@ export class VoiceAssistantsExpose extends LitElement {
} }
protected render() { protected render() {
if (!this.hass || this.hass.entities === undefined) { if (!this.hass || !this.exposedEntities || !this._extEntities) {
return html`<hass-loading-screen></hass-loading-screen>`; return html`<hass-loading-screen></hass-loading-screen>`;
} }
const activeFilters = this._activeFilters(this._searchParms); const activeFilters = this._activeFilters(this._searchParms);
const filteredEntities = this._filteredEntities( const filteredEntities = this._filteredEntities(
this._entities,
this._extEntities, this._extEntities,
this.exposedEntities,
this.hass.devices, this.hass.devices,
this.hass.areas, this.hass.areas,
this.cloudStatus, this.cloudStatus,
@ -621,9 +632,11 @@ export class VoiceAssistantsExpose extends LitElement {
: this._availableAssistants(this.cloudStatus); : this._availableAssistants(this.cloudStatus);
showExposeEntityDialog(this, { showExposeEntityDialog(this, {
filterAssistants: assistants, filterAssistants: assistants,
extendedEntities: this._extEntities!, exposedEntities: this.exposedEntities!,
exposeEntities: (entities) => { exposeEntities: (entities) => {
exposeEntities(this.hass, assistants, entities, true); exposeEntities(this.hass, assistants, entities, true).then(() =>
fireEvent(this, "exposed-entities-changed")
);
}, },
}); });
} }
@ -645,7 +658,9 @@ export class VoiceAssistantsExpose extends LitElement {
const assistants = this._searchParms.has("assistants") const assistants = this._searchParms.has("assistants")
? this._searchParms.get("assistants")!.split(",") ? this._searchParms.get("assistants")!.split(",")
: this._availableAssistants(this.cloudStatus); : this._availableAssistants(this.cloudStatus);
exposeEntities(this.hass, assistants, [entityId], false); exposeEntities(this.hass, assistants, [entityId], false).then(() =>
fireEvent(this, "exposed-entities-changed")
);
}; };
private _unexposeSelected() { private _unexposeSelected() {
@ -670,7 +685,12 @@ export class VoiceAssistantsExpose extends LitElement {
), ),
dismissText: this.hass.localize("ui.common.cancel"), dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => { confirm: () => {
exposeEntities(this.hass, assistants, this._selectedEntities, false); exposeEntities(
this.hass,
assistants,
this._selectedEntities,
false
).then(() => fireEvent(this, "exposed-entities-changed"));
this._clearSelection(); this._clearSelection();
}, },
}); });
@ -698,7 +718,12 @@ export class VoiceAssistantsExpose extends LitElement {
), ),
dismissText: this.hass.localize("ui.common.cancel"), dismissText: this.hass.localize("ui.common.cancel"),
confirm: () => { confirm: () => {
exposeEntities(this.hass, assistants, this._selectedEntities, true); exposeEntities(
this.hass,
assistants,
this._selectedEntities,
true
).then(() => fireEvent(this, "exposed-entities-changed"));
this._clearSelection(); this._clearSelection();
}, },
}); });
@ -710,7 +735,14 @@ export class VoiceAssistantsExpose extends LitElement {
private _openEditEntry(ev: CustomEvent): void { private _openEditEntry(ev: CustomEvent): void {
const entityId = (ev.detail as RowClickedEvent).id; const entityId = (ev.detail as RowClickedEvent).id;
showVoiceSettingsDialog(this, { entityId }); showVoiceSettingsDialog(this, {
entityId,
exposed: this.exposedEntities![entityId],
extEntityReg: this._extEntities?.[entityId],
exposedEntitiesChanged: () => {
fireEvent(this, "exposed-entities-changed");
},
});
} }
private _clearFilter() { private _clearFilter() {

View File

@ -1,11 +1,18 @@
import { consume } from "@lit-labs/context";
import { mdiDevices, mdiMicrophone } from "@mdi/js"; import { mdiDevices, mdiMicrophone } from "@mdi/js";
import { customElement, property } from "lit/decorators"; import { PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import { CloudStatus } from "../../../data/cloud";
import { entitiesContext } from "../../../data/context";
import {
ExposeEntitySettings,
listExposedEntities,
} from "../../../data/expose";
import { import {
HassRouterPage, HassRouterPage,
RouterOptions, RouterOptions,
} from "../../../layouts/hass-router-page"; } from "../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
import { CloudStatus } from "../../../data/cloud";
export const voiceAssistantTabs = [ export const voiceAssistantTabs = [
{ {
@ -30,6 +37,28 @@ class HaConfigVoiceAssistants extends HassRouterPage {
@property() public isWide!: boolean; @property() public isWide!: boolean;
@state()
@consume({ context: entitiesContext, subscribe: true })
_entities!: HomeAssistant["entities"];
@state() private _exposedEntities?: Record<string, ExposeEntitySettings>;
public connectedCallback(): void {
super.connectedCallback();
this.addEventListener(
"exposed-entities-changed",
this._fetchExposedEntities
);
}
public disconnectedCallback(): void {
super.connectedCallback();
this.removeEventListener(
"exposed-entities-changed",
this._fetchExposedEntities
);
}
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
defaultPage: "assistants", defaultPage: "assistants",
routes: { routes: {
@ -55,11 +84,30 @@ class HaConfigVoiceAssistants extends HassRouterPage {
pageEl.narrow = this.narrow; pageEl.narrow = this.narrow;
pageEl.isWide = this.isWide; pageEl.isWide = this.isWide;
pageEl.route = this.routeTail; pageEl.route = this.routeTail;
pageEl.exposedEntities = this._exposedEntities;
} }
public willUpdate(changedProperties: PropertyValues): void {
if (changedProperties.has("_entities")) {
this._fetchExposedEntities();
}
}
private _fetchExposedEntities = async () => {
this._exposedEntities = (
await listExposedEntities(this.hass)
).exposed_entities;
if (this.lastChild) {
(this.lastChild as any).exposedEntities = this._exposedEntities;
}
};
} }
declare global { declare global {
interface HTMLElementTagNameMap { interface HTMLElementTagNameMap {
"ha-config-voice-assistants": HaConfigVoiceAssistants; "ha-config-voice-assistants": HaConfigVoiceAssistants;
} }
interface HASSDomEvents {
"exposed-entities-changed": undefined;
}
} }

View File

@ -1,9 +1,9 @@
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; import { ExposeEntitySettings } from "../../../data/expose";
export interface ExposeEntityDialogParams { export interface ExposeEntityDialogParams {
filterAssistants: string[]; filterAssistants: string[];
extendedEntities: Record<string, ExtEntityRegistryEntry>; exposedEntities: Record<string, ExposeEntitySettings>;
exposeEntities: (entities: string[]) => void; exposeEntities: (entities: string[]) => void;
} }

View File

@ -1,7 +1,12 @@
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
import { ExposeEntitySettings } from "../../../data/expose";
export interface VoiceSettingsDialogParams { export interface VoiceSettingsDialogParams {
entityId: string; entityId: string;
exposed: ExposeEntitySettings;
extEntityReg?: ExtEntityRegistryEntry;
exposedEntitiesChanged?: () => void;
} }
export const loadVoiceSettingsDialog = () => import("./dialog-voice-settings"); export const loadVoiceSettingsDialog = () => import("./dialog-voice-settings");

View File

@ -1052,10 +1052,6 @@ class HUIRoot extends LitElement {
#view { #view {
position: relative; position: relative;
display: flex; display: flex;
background: var(
--lovelace-background,
var(--primary-background-color)
);
padding-top: calc(var(--header-height) + env(safe-area-inset-top)); padding-top: calc(var(--header-height) + env(safe-area-inset-top));
min-height: 100vh; min-height: 100vh;
box-sizing: border-box; box-sizing: border-box;
@ -1063,8 +1059,13 @@ class HUIRoot extends LitElement {
padding-right: env(safe-area-inset-right); padding-right: env(safe-area-inset-right);
padding-bottom: env(safe-area-inset-bottom); padding-bottom: env(safe-area-inset-bottom);
} }
hui-view, hui-view {
hui-unused-entities { background: var(
--lovelace-background,
var(--primary-background-color)
);
}
#view > * {
flex: 1 1 100%; flex: 1 1 100%;
max-width: 100%; max-width: 100%;
} }

View File

@ -838,7 +838,10 @@
"input_label": "Enter a request", "input_label": "Enter a request",
"send_text": "Send text", "send_text": "Send text",
"start_listening": "Start listening", "start_listening": "Start listening",
"manage_assistants": "Manage assistants" "manage_assistants": "Manage assistants",
"not_supported_microphone": "Microphone is not supported. You need to access Home Assistant from a secure URL (HTTPS) to use it.",
"not_supported_microphone_documentation": "Visit {documentation_link} to learn how to use a secure URL",
"not_supported_microphone_documentation_link": "the documentation"
}, },
"generic": { "generic": {
"cancel": "Cancel", "cancel": "Cancel",
@ -1082,6 +1085,7 @@
"expose_header": "Expose", "expose_header": "Expose",
"aliases_header": "Aliases", "aliases_header": "Aliases",
"aliases_description": "Aliases are supported by Assist and Google Assistant.", "aliases_description": "Aliases are supported by Assist and Google Assistant.",
"aliases_no_unique_id": "Aliases are not supported for entities without an unique id. See the {faq_link} for more detail.",
"ask_pin": "Ask for PIN", "ask_pin": "Ask for PIN",
"manual_config": "Managed in configuration.yaml", "manual_config": "Managed in configuration.yaml",
"unsupported": "Unsupported" "unsupported": "Unsupported"