diff --git a/hassio/src/dashboard/hassio-dashboard.ts b/hassio/src/dashboard/hassio-dashboard.ts index b0316ed0ee..f1bfb7a329 100644 --- a/hassio/src/dashboard/hassio-dashboard.ts +++ b/hassio/src/dashboard/hassio-dashboard.ts @@ -9,7 +9,6 @@ import { haStyle } from "../../../src/resources/styles"; import { HomeAssistant, Route } from "../../../src/types"; import { supervisorTabs } from "../hassio-tabs"; import "./hassio-addons"; -import "./hassio-update"; import "../../../src/layouts/hass-subpage"; @customElement("hassio-dashboard") @@ -22,6 +21,12 @@ class HassioDashboard extends LitElement { @property({ attribute: false }) public route!: Route; + firstUpdated() { + if (!atLeastVersion(this.hass.config.version, 2022, 5)) { + import("./hassio-update"); + } + } + protected render(): TemplateResult { if (atLeastVersion(this.hass.config.version, 2022, 5)) { return html` `; } diff --git a/hassio/src/entrypoint.ts b/hassio/src/entrypoint.ts index 09e73ecf4d..d844ef976e 100644 --- a/hassio/src/entrypoint.ts +++ b/hassio/src/entrypoint.ts @@ -2,6 +2,7 @@ import "../../src/resources/compatibility"; import { setCancelSyntheticClickEvents } from "@polymer/polymer/lib/utils/settings"; import "../../src/resources/roboto"; +import "../../src/resources/ha-style"; import "../../src/resources/safari-14-attachshadow-patch"; import "./hassio-main"; diff --git a/hassio/src/hassio-main.ts b/hassio/src/hassio-main.ts index 68fe3e7482..e9769e807c 100644 --- a/hassio/src/hassio-main.ts +++ b/hassio/src/hassio-main.ts @@ -9,7 +9,6 @@ import { navigate } from "../../src/common/navigate"; import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; import { Supervisor } from "../../src/data/supervisor/supervisor"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; -import "../../src/layouts/hass-loading-screen"; import { HomeAssistant } from "../../src/types"; import "./hassio-router"; import { SupervisorBaseElement } from "./supervisor-base-element"; diff --git a/hassio/src/hassio-panel-router.ts b/hassio/src/hassio-panel-router.ts index bceab08108..201f3a6c71 100644 --- a/hassio/src/hassio-panel-router.ts +++ b/hassio/src/hassio-panel-router.ts @@ -5,12 +5,8 @@ import { RouterOptions, } from "../../src/layouts/hass-router-page"; import { HomeAssistant, Route } from "../../src/types"; -import "./addon-store/hassio-addon-store"; // Don't codesplit it, that way the dashboard always loads fast. 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") class HassioPanelRouter extends HassRouterPage { @@ -31,12 +27,15 @@ class HassioPanelRouter extends HassRouterPage { }, store: { tag: "hassio-addon-store", + load: () => import("./addon-store/hassio-addon-store"), }, backups: { tag: "hassio-backups", + load: () => import("./backups/hassio-backups"), }, system: { tag: "hassio-system", + load: () => import("./system/hassio-system"), }, }, }; diff --git a/hassio/src/hassio-panel.ts b/hassio/src/hassio-panel.ts index 6c6dc54c7c..c2d3295759 100644 --- a/hassio/src/hassio-panel.ts +++ b/hassio/src/hassio-panel.ts @@ -4,6 +4,7 @@ import { Supervisor, supervisorCollection, } from "../../src/data/supervisor/supervisor"; +import "../../src/layouts/hass-loading-screen"; import { HomeAssistant, Route } from "../../src/types"; import "./hassio-panel-router"; diff --git a/hassio/src/hassio-router.ts b/hassio/src/hassio-router.ts index b40c075524..23a6e73a37 100644 --- a/hassio/src/hassio-router.ts +++ b/hassio/src/hassio-router.ts @@ -5,7 +5,6 @@ import { HassRouterPage, RouterOptions, } from "../../src/layouts/hass-router-page"; -import "../../src/resources/ha-style"; import { HomeAssistant } from "../../src/types"; // Don't codesplit it, that way the dashboard always loads fast. import "./hassio-panel"; diff --git a/hassio/src/update-available/update-available-card.ts b/hassio/src/update-available/update-available-card.ts index 84b01d1fdd..c9fcb74d5e 100644 --- a/hassio/src/update-available/update-available-card.ts +++ b/hassio/src/update-available/update-available-card.ts @@ -42,9 +42,6 @@ import { updateCore } from "../../../src/data/supervisor/core"; import { StoreAddon } from "../../../src/data/supervisor/store"; import { Supervisor } from "../../../src/data/supervisor/supervisor"; 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 { addonArchIsSupported, extractChangelog } from "../util/addon"; diff --git a/pyproject.toml b/pyproject.toml index 38974e1400..8966ef67eb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "home-assistant-frontend" -version = "20230501.0" +version = "20230502.0" license = {text = "Apache-2.0"} description = "The Home Assistant frontend" readme = "README.md" diff --git a/src/components/ha-aliases-editor.ts b/src/components/ha-aliases-editor.ts index ec79e59968..4f3d7aad5c 100644 --- a/src/components/ha-aliases-editor.ts +++ b/src/components/ha-aliases-editor.ts @@ -15,6 +15,8 @@ class AliasesEditor extends LitElement { @property() public aliases!: string[]; + @property({ type: Boolean }) public disabled = false; + protected render() { if (!this.aliases) { return nothing; @@ -25,6 +27,7 @@ class AliasesEditor extends LitElement { (alias, index) => html`
- + ${this.hass!.localize("ui.dialogs.aliases.add_alias")} diff --git a/src/components/ha-tts-picker.ts b/src/components/ha-tts-picker.ts index d9dfed597b..8d3fcb6b07 100644 --- a/src/components/ha-tts-picker.ts +++ b/src/components/ha-tts-picker.ts @@ -19,7 +19,10 @@ import type { HaSelect } from "./ha-select"; 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") export class HaTTSPicker extends LitElement { diff --git a/src/components/media-player/ha-browse-media-tts.ts b/src/components/media-player/ha-browse-media-tts.ts index 152da17ac6..1755b4e6fd 100644 --- a/src/components/media-player/ha-browse-media-tts.ts +++ b/src/components/media-player/ha-browse-media-tts.ts @@ -21,6 +21,7 @@ import { buttonLinkStyle } from "../../resources/styles"; import { HomeAssistant } from "../../types"; import "../ha-select"; import "../ha-textarea"; +import "../ha-language-picker"; export interface TtsMediaPickedEvent { item: MediaPlayerItem; @@ -103,21 +104,17 @@ class BrowseMediaTTS extends LitElement { return html`
- - ${languages.map( - ([key, label]) => - html`${label}` - )} - + hass.callWS({ type: "cloud/tts/info" }); export const getCloudTtsLanguages = (info?: CloudTTSInfo) => { - const languages: Array<[string, string]> = []; + const languages: string[] = []; if (!info) { return languages; @@ -23,25 +22,9 @@ export const getCloudTtsLanguages = (info?: CloudTTSInfo) => { continue; } seen.add(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]); + languages.push(lang); } - return languages.sort((a, b) => caseInsensitiveStringCompare(a[1], b[1])); + return languages; }; export const getCloudTtsSupportedGenders = ( diff --git a/src/data/voice.ts b/src/data/expose.ts similarity index 75% rename from src/data/voice.ts rename to src/data/expose.ts index c2bef3982b..d8cea10b34 100644 --- a/src/data/voice.ts +++ b/src/data/expose.ts @@ -12,6 +12,12 @@ export const voiceAssistants = { }, } as const; +export interface ExposeEntitySettings { + conversation?: boolean; + "cloud.alexa"?: boolean; + "cloud.google_assistant"?: boolean; +} + export const setExposeNewEntities = ( hass: HomeAssistant, assistant: string, @@ -41,3 +47,8 @@ export const exposeEntities = ( entity_ids, should_expose, }); + +export const listExposedEntities = (hass: HomeAssistant) => + hass.callWS<{ exposed_entities: Record }>({ + type: "homeassistant/expose_entity/list", + }); diff --git a/src/dialogs/more-info/components/voice/ha-more-info-view-voice-assistants.ts b/src/dialogs/more-info/components/voice/ha-more-info-view-voice-assistants.ts index f43b7fc14e..0fe5b790c1 100644 --- a/src/dialogs/more-info/components/voice/ha-more-info-view-voice-assistants.ts +++ b/src/dialogs/more-info/components/voice/ha-more-info-view-voice-assistants.ts @@ -1,6 +1,8 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; +import memoizeOne from "memoize-one"; import { ExtEntityRegistryEntry } from "../../../../data/entity_registry"; +import { ExposeEntitySettings, voiceAssistants } from "../../../../data/expose"; import "../../../../panels/config/voice-assistants/entity-voice-settings"; import { HomeAssistant } from "../../../../types"; @@ -12,13 +14,23 @@ class MoreInfoViewVoiceAssistants extends LitElement { @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() { if (!this.params) { return nothing; } return html``; } diff --git a/src/dialogs/more-info/ha-more-info-settings.ts b/src/dialogs/more-info/ha-more-info-settings.ts index a10545c651..b3a4c2eed1 100644 --- a/src/dialogs/more-info/ha-more-info-settings.ts +++ b/src/dialogs/more-info/ha-more-info-settings.ts @@ -5,11 +5,12 @@ import { CSSResultGroup, html, LitElement, - PropertyValues, nothing, + PropertyValues, } from "lit"; import { customElement, property, state } from "lit/decorators"; import { dynamicElement } from "../../common/dom/dynamic-element-directive"; +import "../../components/ha-alert"; import { EntityRegistryEntry, ExtEntityRegistryEntry, @@ -39,18 +40,17 @@ export class HaMoreInfoSettings extends LitElement { if (this.entry === null) { return html`
- ${this.hass.localize( - "ui.dialogs.entity_registry.no_unique_id", - "entity_id", - this.entityId, - "faq_link", - html`${this.hass.localize("ui.dialogs.entity_registry.faq")}` - )} + + ${this.hass.localize("ui.dialogs.entity_registry.no_unique_id", { + entity_id: this.entityId, + faq_link: html`${this.hass.localize("ui.dialogs.entity_registry.faq")}`, + })} +
`; } diff --git a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts index 87ea70b4e5..aabdb1e7c4 100644 --- a/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts +++ b/src/dialogs/voice-command-dialog/ha-voice-command-dialog.ts @@ -1,5 +1,6 @@ import "@material/mwc-button/mwc-button"; import { + mdiAlertCircle, mdiChevronDown, mdiClose, mdiHelpCircleOutline, @@ -14,6 +15,7 @@ import { LitElement, nothing, PropertyValues, + TemplateResult, } from "lit"; import { customElement, property, query, state } from "lit/decorators"; import { LocalStorage } from "../../common/decorators/local-storage"; @@ -42,7 +44,7 @@ import { showAlertDialog } from "../generic/show-dialog-box"; interface Message { who: string; - text?: string; + text?: string | TemplateResult; error?: boolean; } @@ -109,7 +111,10 @@ export class HaVoiceCommandDialog extends LitElement { if (!this._opened) { return nothing; } - const supportsSTT = this._pipeline?.stt_engine && AudioRecorder.isSupported; + + const supportsMicrophone = AudioRecorder.isSupported; + const supportsSTT = this._pipeline?.stt_engine; + return html` ${pipeline.name}${pipeline.id === this._preferredPipeline - ? html`` + ? html` + + ` : nothing} ` )} @@ -203,7 +210,7 @@ export class HaVoiceCommandDialog extends LitElement { iconTrailing > - ${this._showSendButton + ${this._showSendButton || !supportsSTT ? html` ` - : supportsSTT - ? html` + : html` ${this._audioRecorder?.active ? html`
@@ -224,18 +230,27 @@ export class HaVoiceCommandDialog extends LitElement {
` - : ""} - - - ` - : ""} + : nothing} + +
+ + + ${!supportsMicrophone + ? html` + + ` + : null} +
+ `}
${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) { this._startListening(); } else { @@ -390,6 +412,40 @@ export class HaVoiceCommandDialog extends LitElement { } } + private async _showNotSupportedMessage() { + this._addMessage({ + who: "hass", + text: html` +

+ ${this.hass.localize( + "ui.dialogs.voice_command.not_supported_microphone" + )} +

+

+ ${this.hass.localize( + "ui.dialogs.voice_command.not_supported_microphone_documentation", + { + documentation_link: html` + + ${this.hass.localize( + "ui.dialogs.voice_command.not_supported_microphone_documentation_link" + )} + + `, + } + )} +

+ `, + }); + } + private async _startListening() { this._audio?.pause(); if (!this._audioRecorder) { @@ -561,7 +617,8 @@ export class HaVoiceCommandDialog extends LitElement { return [ haStyleDialog, css` - ha-icon-button.listening-icon { + .listening-icon { + position: relative; color: var(--secondary-text-color); margin-right: -24px; margin-inline-end: -24px; @@ -569,10 +626,18 @@ export class HaVoiceCommandDialog extends LitElement { direction: var(--direction); } - ha-icon-button.listening-icon[active] { + .listening-icon[active] { color: var(--primary-color); } + .unsupported { + color: var(--error-color); + position: absolute; + --mdc-icon-size: 16px; + right: 5px; + top: 0px; + } + ha-dialog { --primary-action-button-flex: 1; --secondary-action-button-flex: 0; @@ -616,6 +681,19 @@ export class HaVoiceCommandDialog extends LitElement { ha-button-menu ha-button ha-svg-icon { height: 28px; 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 { text-decoration: none; @@ -648,6 +726,7 @@ export class HaVoiceCommandDialog extends LitElement { display: block; height: 400px; box-sizing: border-box; + position: relative; } @media all and (max-width: 450px), all and (max-height: 500px) { ha-dialog { @@ -655,6 +734,7 @@ export class HaVoiceCommandDialog extends LitElement { } .messages { height: 100%; + flex: 1; } } .messages-container { @@ -674,6 +754,12 @@ export class HaVoiceCommandDialog extends LitElement { padding: 8px; border-radius: 15px; } + .message p { + margin: 0; + } + .message p:not(:last-child) { + margin-bottom: 8px; + } .message.user { margin-left: 24px; diff --git a/src/layouts/supervisor-error-screen.ts b/src/layouts/supervisor-error-screen.ts index eabb00e439..6d415a7c43 100644 --- a/src/layouts/supervisor-error-screen.ts +++ b/src/layouts/supervisor-error-screen.ts @@ -11,7 +11,6 @@ import { customElement, property } from "lit/decorators"; import { atLeastVersion } from "../common/config/version"; import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; import "../components/ha-card"; -import "../resources/ha-style"; import { haStyle } from "../resources/styles"; import { HomeAssistant } from "../types"; import "./hass-subpage"; diff --git a/src/panels/config/areas/ha-config-areas-dashboard.ts b/src/panels/config/areas/ha-config-areas-dashboard.ts index d4e6c1703d..5b03220dec 100644 --- a/src/panels/config/areas/ha-config-areas-dashboard.ts +++ b/src/panels/config/areas/ha-config-areas-dashboard.ts @@ -21,7 +21,6 @@ import { subscribeEntityRegistry, } from "../../../data/entity_registry"; import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box"; -import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { HomeAssistant, Route } from "../../../types"; @@ -222,10 +221,6 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) { static get styles(): CSSResultGroup { return css` - hass-loading-screen { - --app-header-background-color: var(--sidebar-background-color); - --app-header-text-color: var(--sidebar-text-color); - } .container { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); diff --git a/src/panels/config/cloud/account/cloud-tts-pref.ts b/src/panels/config/cloud/account/cloud-tts-pref.ts index 43d421fda6..0245c3a134 100644 --- a/src/panels/config/cloud/account/cloud-tts-pref.ts +++ b/src/panels/config/cloud/account/cloud-tts-pref.ts @@ -8,6 +8,7 @@ import "../../../../components/ha-card"; import "../../../../components/ha-select"; import "../../../../components/ha-svg-icon"; import "../../../../components/ha-switch"; +import "../../../../components/ha-language-picker"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; import { CloudTTSInfo, @@ -54,34 +55,33 @@ export class CloudTTSPref extends LitElement { '"tts.cloud_say"' )}

+
+ + - - ${languages.map( - ([key, label]) => - html`${label}` - )} - - - - ${genders.map( - ([key, label]) => - html`${label}` - )} - + + ${genders.map( + ([key, label]) => + html`${label}` + )} + +
@@ -115,11 +115,11 @@ export class CloudTTSPref extends LitElement { } 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; } this.savingPreferences = true; - const language = ev.target.value; + const language = ev.detail.value; const curGender = this.cloudStatus!.prefs.tts_default_voice[1]; const genders = this.getSupportedGenders( @@ -185,6 +185,18 @@ export class CloudTTSPref extends LitElement { right: auto; left: 24px; } + .row { + display: flex; + } + .row > * { + flex: 1; + } + .row > *:first-child { + margin-right: 8px; + } + .row > *:last-child { + margin-left: 8px; + } .card-actions { display: flex; flex-direction: row-reverse; diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 4bc94fbd4c..afd74a41b1 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -62,7 +62,6 @@ import { } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-error-screen"; import "../../../layouts/hass-subpage"; -import "../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; diff --git a/src/panels/config/ha-panel-config.ts b/src/panels/config/ha-panel-config.ts index 57011cf0e4..3a3daa702d 100644 --- a/src/panels/config/ha-panel-config.ts +++ b/src/panels/config/ha-panel-config.ts @@ -33,7 +33,6 @@ import { customElement, property, state } from "lit/decorators"; import { isComponentLoaded } from "../../common/config/is_component_loaded"; import { listenMediaQuery } from "../../common/dom/media_query"; import { CloudStatus, fetchCloudStatus } from "../../data/cloud"; -import "../../layouts/hass-loading-screen"; import { HassRouterPage, RouterOptions } from "../../layouts/hass-router-page"; import { PageNavigation } from "../../layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../types"; diff --git a/src/panels/config/integrations/ha-integration-card.ts b/src/panels/config/integrations/ha-integration-card.ts index f9c56194a1..12a13a6cc5 100644 --- a/src/panels/config/integrations/ha-integration-card.ts +++ b/src/panels/config/integrations/ha-integration-card.ts @@ -174,9 +174,11 @@ export class HaIntegrationCard extends LitElement { ${this.items.map( (item) => html`${item.title || this.hass.localize( "ui.panel.config.integrations.config_entry.unnamed_entry" @@ -1026,6 +1028,9 @@ export class HaIntegrationCard extends LitElement { ha-list-item ha-svg-icon { color: var(--secondary-text-color); } + .config-entry { + height: 36px; + } ha-icon-next { width: 24px; } diff --git a/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts b/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts index 4c18a4e2b9..64882051b2 100644 --- a/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts +++ b/src/panels/config/integrations/integration-panels/zha/zha-add-group-page.ts @@ -13,7 +13,6 @@ import { ZHADeviceEndpoint, ZHAGroup, } from "../../../../../data/zha"; -import "../../../../../layouts/hass-error-screen"; import "../../../../../layouts/hass-subpage"; import type { PolymerChangedEvent } from "../../../../../polymer-types"; import type { HomeAssistant } from "../../../../../types"; diff --git a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts index a47c26bbe5..0de6edb5a4 100644 --- a/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts +++ b/src/panels/config/integrations/integration-panels/zwave_js/zwave_js-node-config.ts @@ -39,6 +39,8 @@ import { ZwaveJSNodeMetadata, ZWaveJSSetConfigParamResult, } from "../../../../../data/zwave_js"; +import "../../../../../layouts/hass-error-screen"; +import "../../../../../layouts/hass-loading-screen"; import "../../../../../layouts/hass-tabs-subpage"; import { SubscribeMixin } from "../../../../../mixins/subscribe-mixin"; import { haStyle } from "../../../../../resources/styles"; diff --git a/src/panels/config/logs/ha-config-logs.ts b/src/panels/config/logs/ha-config-logs.ts index 6788c9b639..6d176fca55 100644 --- a/src/panels/config/logs/ha-config-logs.ts +++ b/src/panels/config/logs/ha-config-logs.ts @@ -9,7 +9,6 @@ import "../../../components/search-input"; import { LogProvider } from "../../../data/error_log"; import { fetchHassioAddonsInfo } from "../../../data/hassio/addon"; import "../../../layouts/hass-subpage"; -import "../../../layouts/hass-tabs-subpage"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; import "./error-log-card"; diff --git a/src/panels/config/repairs/ha-config-repairs.ts b/src/panels/config/repairs/ha-config-repairs.ts index 7265f2f837..1643de6b06 100644 --- a/src/panels/config/repairs/ha-config-repairs.ts +++ b/src/panels/config/repairs/ha-config-repairs.ts @@ -9,7 +9,6 @@ import "../../../components/ha-list-item"; import "../../../components/ha-svg-icon"; import { domainToName } from "../../../data/integration"; import type { RepairsIssue } from "../../../data/repairs"; -import "../../../layouts/hass-subpage"; import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { showRepairsFlowDialog } from "./show-dialog-repair-flow"; diff --git a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts index 19d78a6527..856af04c39 100644 --- a/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts +++ b/src/panels/config/voice-assistants/assist-pipeline-detail/assist-pipeline-detail-config.ts @@ -1,9 +1,10 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; import { customElement, property } from "lit/decorators"; 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 { HomeAssistant } from "../../../../types"; +import "../../../../components/ha-form/ha-form"; @customElement("assist-pipeline-detail-config") export class AssistPipelineDetailConfig extends LitElement { @@ -33,26 +34,28 @@ export class AssistPipelineDetailConfig extends LitElement { text: {}, }, }, - { - name: "language", - required: true, - selector: { - language: { - languages: supportedLanguages ?? [], - }, - }, - }, + supportedLanguages + ? { + name: "language", + required: true, + selector: { + language: { + languages: supportedLanguages, + }, + }, + } + : { name: "", type: "constant" }, ] as const, }, ] as const ); - private _computeLabel = ( - schema: SchemaUnion> - ): string => - this.hass.localize( - `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` - ); + private _computeLabel = (schema): string => + schema.name + ? this.hass.localize( + `ui.panel.config.voice_assistants.assistants.pipeline.detail.form.${schema.name}` as LocalizeKeys + ) + : ""; protected render() { return html` diff --git a/src/panels/config/voice-assistants/assist-pref.ts b/src/panels/config/voice-assistants/assist-pref.ts index 80034e3bd1..f16d13d11d 100644 --- a/src/panels/config/voice-assistants/assist-pref.ts +++ b/src/panels/config/voice-assistants/assist-pref.ts @@ -20,7 +20,7 @@ import { updateAssistPipeline, } from "../../../data/assist_pipeline"; 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 type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; @@ -29,7 +29,10 @@ import { showVoiceAssistantPipelineDetailDialog } from "./show-dialog-voice-assi export class AssistPref extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() private extEntities?: Record; + @property({ attribute: false }) public exposedEntities?: Record< + string, + ExposeEntitySettings + >; @state() private _pipelines: AssistPipeline[] = []; @@ -46,11 +49,10 @@ export class AssistPref extends LitElement { }); } - private _exposedEntities = memoizeOne( - (extEntities: Record) => - Object.values(extEntities).filter( - (entity) => entity.options?.conversation?.should_expose - ).length + private _exposedEntitiesCount = memoizeOne( + (exposedEntities: Record) => + Object.values(exposedEntities).filter((expose) => expose.conversation) + .length ); protected render() { @@ -119,8 +121,8 @@ export class AssistPref extends LitElement { ${this.hass.localize( "ui.panel.config.voice_assistants.assistants.pipeline.exposed_entities", { - number: this.extEntities - ? this._exposedEntities(this.extEntities) + number: this.exposedEntities + ? this._exposedEntitiesCount(this.exposedEntities) : 0, } )} diff --git a/src/panels/config/voice-assistants/cloud-alexa-pref.ts b/src/panels/config/voice-assistants/cloud-alexa-pref.ts index b4106304bf..a071eba07f 100644 --- a/src/panels/config/voice-assistants/cloud-alexa-pref.ts +++ b/src/panels/config/voice-assistants/cloud-alexa-pref.ts @@ -11,28 +11,30 @@ import "../../../components/ha-settings-row"; import "../../../components/ha-switch"; import type { HaSwitch } from "../../../components/ha-switch"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud"; -import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; import { + ExposeEntitySettings, getExposeNewEntities, setExposeNewEntities, -} from "../../../data/voice"; +} from "../../../data/expose"; import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; export class CloudAlexaPref extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() private extEntities?: Record; + @property({ attribute: false }) public exposedEntities?: Record< + string, + ExposeEntitySettings + >; @property() public cloudStatus?: CloudStatusLoggedIn; @state() private _exposeNew?: boolean; - private _exposedEntities = memoizeOne( - (extEntities: Record) => - Object.values(extEntities).filter( - (entity) => entity.options?.["cloud.alexa"]?.should_expose - ).length + private _exposedEntitiesCount = memoizeOne( + (exposedEntities: Record) => + Object.values(exposedEntities).filter((expose) => expose["cloud.alexa"]) + .length ); protected willUpdate() { @@ -183,8 +185,8 @@ export class CloudAlexaPref extends LitElement { : this.hass.localize( "ui.panel.config.cloud.account.alexa.exposed_entities", { - number: this.extEntities - ? this._exposedEntities(this.extEntities) + number: this.exposedEntities + ? this._exposedEntitiesCount(this.exposedEntities) : 0, } )} diff --git a/src/panels/config/voice-assistants/cloud-google-pref.ts b/src/panels/config/voice-assistants/cloud-google-pref.ts index 3c28d1b636..409319ffc9 100644 --- a/src/panels/config/voice-assistants/cloud-google-pref.ts +++ b/src/panels/config/voice-assistants/cloud-google-pref.ts @@ -4,6 +4,7 @@ import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { property, state } from "lit/decorators"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; +import { isEmptyFilter } from "../../../common/entity/entity_filter"; import "../../../components/ha-alert"; import "../../../components/ha-card"; import "../../../components/ha-settings-row"; @@ -11,20 +12,22 @@ import type { HaSwitch } from "../../../components/ha-switch"; import "../../../components/ha-textfield"; import type { HaTextField } from "../../../components/ha-textfield"; 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 { + ExposeEntitySettings, getExposeNewEntities, setExposeNewEntities, -} from "../../../data/voice"; -import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; +} from "../../../data/expose"; +import { HomeAssistant } from "../../../types"; +import { brandsUrl } from "../../../util/brands-url"; +import { showSaveSuccessToast } from "../../../util/toast-saved-success"; export class CloudGooglePref extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @property() private extEntities?: Record; + @property({ attribute: false }) public exposedEntities?: Record< + string, + ExposeEntitySettings + >; @property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn; @@ -40,10 +43,10 @@ export class CloudGooglePref extends LitElement { } } - private _exposedEntities = memoizeOne( - (extEntities: Record) => - Object.values(extEntities).filter( - (entity) => entity.options?.["cloud.google_assistant"]?.should_expose + private _exposedEntitiesCount = memoizeOne( + (exposedEntities: Record) => + Object.values(exposedEntities).filter( + (expose) => expose["cloud.google_assistant"] ).length ); @@ -239,8 +242,8 @@ export class CloudGooglePref extends LitElement { : this.hass.localize( "ui.panel.config.cloud.account.google.exposed_entities", { - number: this.extEntities - ? this._exposedEntities(this.extEntities) + number: this.exposedEntities + ? this._exposedEntitiesCount(this.exposedEntities) : 0, } )} diff --git a/src/panels/config/voice-assistants/dialog-expose-entity.ts b/src/panels/config/voice-assistants/dialog-expose-entity.ts index 6e6da8088e..2f41470b49 100644 --- a/src/panels/config/voice-assistants/dialog-expose-entity.ts +++ b/src/panels/config/voice-assistants/dialog-expose-entity.ts @@ -1,18 +1,16 @@ import "@material/mwc-button"; import "@material/mwc-list"; import { mdiClose } from "@mdi/js"; +import { HassEntity } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property, state } from "lit/decorators"; import { ifDefined } from "lit/directives/if-defined"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; +import { computeStateName } from "../../../common/entity/compute_state_name"; import "../../../components/ha-check-list-item"; import "../../../components/search-input"; -import { - computeEntityRegistryName, - ExtEntityRegistryEntry, -} from "../../../data/entity_registry"; -import { voiceAssistants } from "../../../data/voice"; +import { ExposeEntitySettings, voiceAssistants } from "../../../data/expose"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; import "./entity-voice-settings"; @@ -49,7 +47,7 @@ class DialogExposeEntity extends LitElement { ); const entities = this._filterEntities( - this._params.extendedEntities, + this._params.exposedEntities, this._filter ); @@ -126,42 +124,40 @@ class DialogExposeEntity extends LitElement { } private _filterEntities = memoizeOne( - (RegEntries: Record, filter?: string) => { + ( + exposedEntities: Record, + filter?: string + ) => { const lowerFilter = filter?.toLowerCase(); - return Object.values(RegEntries).filter( + return Object.values(this.hass.states).filter( (entity) => this._params!.filterAssistants.some( - (ass) => !entity.options?.[ass]?.should_expose + (ass) => !exposedEntities[entity.entity_id]?.[ass] ) && (!lowerFilter || entity.entity_id.toLowerCase().includes(lowerFilter) || - computeEntityRegistryName(this.hass!, entity) - ?.toLowerCase() - .includes(lowerFilter)) + computeStateName(entity)?.toLowerCase().includes(lowerFilter)) ); } ); - private _renderItem = (entity: ExtEntityRegistryEntry) => { - const entityState = this.hass.states[entity.entity_id]; - return html` - - - ${computeEntityRegistryName(this.hass!, entity)} - ${entity.entity_id} - - `; - }; + private _renderItem = (entityState: HassEntity) => html` + + + ${computeStateName(entityState)} + ${entityState.entity_id} + + `; private _expose() { this._params!.exposeEntities(this._selected); diff --git a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts index aa221e8b66..6048cfc0ac 100644 --- a/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts +++ b/src/panels/config/voice-assistants/dialog-voice-assistant-pipeline-detail.ts @@ -44,7 +44,7 @@ export class DialogVoiceAssistantPipelineDetail extends LitElement { @state() private _submitting = false; - @state() private _supportedLanguages: string[] = []; + @state() private _supportedLanguages?: string[]; public showDialog(params: VoiceAssistantPipelineDetailsDialogParams): void { this._params = params; diff --git a/src/panels/config/voice-assistants/dialog-voice-settings.ts b/src/panels/config/voice-assistants/dialog-voice-settings.ts index 74ae26d75b..1e3aac8da0 100644 --- a/src/panels/config/voice-assistants/dialog-voice-settings.ts +++ b/src/panels/config/voice-assistants/dialog-voice-settings.ts @@ -1,38 +1,31 @@ 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 { fireEvent } from "../../../common/dom/fire_event"; -import { - ExtEntityRegistryEntry, - computeEntityRegistryName, - getExtendedEntityRegistryEntry, -} from "../../../data/entity_registry"; +import { computeStateName } from "../../../common/entity/compute_state_name"; +import { createCloseHeading } from "../../../components/ha-dialog"; import { haStyle, haStyleDialog } from "../../../resources/styles"; import { HomeAssistant } from "../../../types"; -import { VoiceSettingsDialogParams } from "./show-dialog-voice-settings"; import "./entity-voice-settings"; -import { createCloseHeading } from "../../../components/ha-dialog"; +import { VoiceSettingsDialogParams } from "./show-dialog-voice-settings"; @customElement("dialog-voice-settings") class DialogVoiceSettings extends LitElement { @property({ attribute: false }) public hass!: HomeAssistant; - @state() private _extEntityReg?: ExtEntityRegistryEntry; + @state() private _params?: VoiceSettingsDialogParams; - public async showDialog(params: VoiceSettingsDialogParams): Promise { - this._extEntityReg = await getExtendedEntityRegistryEntry( - this.hass, - params.entityId - ); + public showDialog(params: VoiceSettingsDialogParams): void { + this._params = params; } public closeDialog(): void { - this._extEntityReg = undefined; + this._params = undefined; fireEvent(this, "dialog-closed", { dialog: this.localName }); } protected render() { - if (!this._extEntityReg) { + if (!this._params) { return nothing; } @@ -43,15 +36,18 @@ class DialogVoiceSettings extends LitElement { hideActions .heading=${createCloseHeading( this.hass, - computeEntityRegistryName(this.hass, this._extEntityReg) || + computeStateName(this.hass.states[this._params.entityId]) || this.hass.localize("ui.panel.config.entities.picker.unnamed_entity") )} >
@@ -59,7 +55,11 @@ class DialogVoiceSettings extends LitElement { } private _entityEntryUpdated(ev: CustomEvent) { - this._extEntityReg = ev.detail; + this._params!.extEntityReg = ev.detail; + } + + private _exposedEntitiesChanged() { + this._params!.exposedEntitiesChanged?.(); } static get styles(): CSSResultGroup { diff --git a/src/panels/config/voice-assistants/entity-voice-settings.ts b/src/panels/config/voice-assistants/entity-voice-settings.ts index b985447260..9c92bfebd0 100644 --- a/src/panels/config/voice-assistants/entity-voice-settings.ts +++ b/src/panels/config/voice-assistants/entity-voice-settings.ts @@ -36,18 +36,27 @@ import { fetchCloudGoogleEntity, GoogleEntity, } from "../../../data/google_assistant"; -import { exposeEntities, voiceAssistants } from "../../../data/voice"; +import { + exposeEntities, + ExposeEntitySettings, + voiceAssistants, +} from "../../../data/expose"; import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; import { haStyle } from "../../../resources/styles"; import type { HomeAssistant } from "../../../types"; import { brandsUrl } from "../../../util/brands-url"; import { EntityRegistrySettings } from "../entities/entity-registry-settings"; +import { documentationUrl } from "../../../util/documentation-url"; @customElement("entity-voice-settings") export class EntityVoiceSettings extends SubscribeMixin(LitElement) { @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; @@ -63,7 +72,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { if (!isComponentLoaded(this.hass, "cloud")) { return; } - if (changedProps.has("entry") && this.entry) { + if (changedProps.has("entityId") && this.entityId) { this._fetchEntities(); } if (!this.hasUpdated) { @@ -77,7 +86,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { try { const googleEntity = await fetchCloudGoogleEntity( this.hass, - this.entry.entity_id + this.entityId ); this._googleEntity = googleEntity; this.requestUpdate("_googleEntity"); @@ -89,7 +98,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { } try { - await fetchCloudAlexaEntity(this.hass, this.entry.entity_id); + await fetchCloudAlexaEntity(this.hass, this.entityId); } catch (err: any) { if (err.code === "not_supported") { this._unsupported["cloud.alexa"] = true; @@ -153,9 +162,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { uiAssistants.splice(uiAssistants.indexOf("cloud.alexa"), 1); } - const uiExposed = uiAssistants.some( - (key) => this.entry.options?.[key]?.should_expose - ); + const uiExposed = uiAssistants.some((key) => this.exposed[key]); let manFilterFuncs: | { @@ -171,10 +178,9 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { ); } - const manExposedAlexa = - alexaManual && manFilterFuncs!.alexa(this.entry.entity_id); + const manExposedAlexa = alexaManual && manFilterFuncs!.alexa(this.entityId); const manExposedGoogle = - googleManual && manFilterFuncs!.google(this.entry.entity_id); + googleManual && manFilterFuncs!.google(this.entityId); const anyExposed = uiExposed || manExposedAlexa || manExposedGoogle; @@ -198,13 +204,14 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { ? manExposedAlexa : googleManual && key === "cloud.google_assistant" ? manExposedGoogle - : this.entry.options?.[key]?.should_expose; + : this.exposed[key]; const manualConfig = (alexaManual && key === "cloud.alexa") || (googleManual && key === "cloud.google_assistant"); const support2fa = + this.entry && key === "cloud.google_assistant" && !googleManual && supported && @@ -249,7 +256,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { )} > @@ -274,12 +281,26 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { ${this.hass.localize("ui.dialogs.voice-settings.aliases_description")}

- + ${!this.entry + ? html` + ${this.hass.localize( + "ui.dialogs.voice-settings.aliases_no_unique_id", + { + faq_link: html`${this.hass.localize("ui.dialogs.entity_registry.faq")}`, + } + )} + ` + : html``} `; } @@ -291,7 +312,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { try { await updateCloudGoogleEntityConfig( this.hass, - this.entry.entity_id, + this.entityId, !ev.target.checked ); } catch (_err) { @@ -303,15 +324,11 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { if (!this._aliases) { return; } - const result = await updateEntityRegistryEntry( - this.hass, - this.entry.entity_id, - { - aliases: this._aliases - .map((alias) => alias.trim()) - .filter((alias) => alias), - } - ); + const result = await updateEntityRegistryEntry(this.hass, this.entityId, { + aliases: this._aliases + .map((alias) => alias.trim()) + .filter((alias) => alias), + }); fireEvent(this, "entity-entry-updated", result.entity_entry); } @@ -319,14 +336,17 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) { exposeEntities( this.hass, [ev.target.assistant], - [this.entry.entity_id], + [this.entityId], ev.target.checked ); - const entry = await getExtendedEntityRegistryEntry( - this.hass, - this.entry.entity_id - ); - fireEvent(this, "entity-entry-updated", entry); + if (this.entry) { + const entry = await getExtendedEntityRegistryEntry( + this.hass, + this.entityId + ); + fireEvent(this, "entity-entry-updated", entry); + } + fireEvent(this, "exposed-entities-changed"); } 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; - exposeEntities( - this.hass, - assistants, - [this.entry.entity_id], - ev.target.checked - ); - const entry = await getExtendedEntityRegistryEntry( - this.hass, - this.entry.entity_id - ); - fireEvent(this, "entity-entry-updated", entry); + exposeEntities(this.hass, assistants, [this.entityId], ev.target.checked); + if (this.entry) { + const entry = await getExtendedEntityRegistryEntry( + this.hass, + this.entityId + ); + fireEvent(this, "entity-entry-updated", entry); + } + fireEvent(this, "exposed-entities-changed"); } static get styles(): CSSResultGroup { diff --git a/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts b/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts index ed4daf7086..5fe28881aa 100644 --- a/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts +++ b/src/panels/config/voice-assistants/expose/expose-assistant-icon.ts @@ -3,7 +3,7 @@ import { mdiAlertCircle } from "@mdi/js"; import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; import { customElement, property } from "lit/decorators"; import { styleMap } from "lit/directives/style-map"; -import { voiceAssistants } from "../../../../data/voice"; +import { voiceAssistants } from "../../../../data/expose"; import { HomeAssistant } from "../../../../types"; import { brandsUrl } from "../../../../util/brands-url"; import "../../../../components/ha-svg-icon"; diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts index 137f57a76a..f1cfc8b24e 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-assistants.ts @@ -1,16 +1,13 @@ -import { consume } from "@lit-labs/context"; import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item-body"; -import { css, html, LitElement, nothing, PropertyValues } from "lit"; -import { customElement, property, state } from "lit/decorators"; +import { css, html, LitElement, nothing } from "lit"; +import { customElement, property } from "lit/decorators"; import { isComponentLoaded } from "../../../common/config/is_component_loaded"; import { computeRTLDirection } from "../../../common/util/compute_rtl"; import { CloudStatus } from "../../../data/cloud"; -import { entitiesContext } from "../../../data/context"; -import { - ExtEntityRegistryEntry, - getExtendedEntityRegistryEntries, -} from "../../../data/entity_registry"; +import { ExposeEntitySettings } from "../../../data/expose"; + +import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage"; import { HomeAssistant, Route } from "../../../types"; import "./assist-pref"; @@ -25,18 +22,17 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement { @property({ attribute: false }) public cloudStatus?: CloudStatus; + @property({ attribute: false }) public exposedEntities?: Record< + string, + ExposeEntitySettings + >; + @property() public isWide!: boolean; @property() public narrow!: boolean; @property() public route!: Route; - @state() - @consume({ context: entitiesContext, subscribe: true }) - _entities!: HomeAssistant["entities"]; - - @state() private _extEntities?: Record; - protected render() { if (!this.hass) { return html``; @@ -56,7 +52,7 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement { ` : nothing} @@ -64,13 +60,13 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement { ? html` @@ -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` .content { padding: 28px 20px 0; diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts index 707e1af95f..23f337086c 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants-expose.ts @@ -19,7 +19,8 @@ import { customElement, property, query, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { ifDefined } from "lit/directives/if-defined"; 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 { EntityFilter, generateFilter, @@ -38,26 +39,28 @@ import { AlexaEntity, fetchCloudAlexaEntities } from "../../../data/alexa"; import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud"; import { entitiesContext } from "../../../data/context"; import { - computeEntityRegistryName, - EntityRegistryEntry, ExtEntityRegistryEntry, getExtendedEntityRegistryEntries, } from "../../../data/entity_registry"; +import { + exposeEntities, + ExposeEntitySettings, + voiceAssistants, +} from "../../../data/expose"; import { fetchCloudGoogleEntities, GoogleEntity, } from "../../../data/google_assistant"; -import { exposeEntities, voiceAssistants } from "../../../data/voice"; import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box"; import "../../../layouts/hass-loading-screen"; import "../../../layouts/hass-tabs-subpage-data-table"; import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; +import "./expose/expose-assistant-icon"; import { voiceAssistantTabs } from "./ha-config-voice-assistants"; import { showExposeEntityDialog } from "./show-dialog-expose-entity"; import { showVoiceSettingsDialog } from "./show-dialog-voice-settings"; -import "./expose/expose-assistant-icon"; @customElement("ha-config-voice-assistants-expose") export class VoiceAssistantsExpose extends LitElement { @@ -71,6 +74,11 @@ export class VoiceAssistantsExpose extends LitElement { @property({ attribute: false }) public route!: Route; + @property({ attribute: false }) public exposedEntities?: Record< + string, + ExposeEntitySettings + >; + @state() @consume({ context: entitiesContext, subscribe: true }) _entities!: HomeAssistant["entities"]; @@ -256,8 +264,8 @@ export class VoiceAssistantsExpose extends LitElement { private _filteredEntities = memoize( ( - entities: HomeAssistant["entities"], - extEntities: Record | undefined, + entities: Record, + exposedEntities: Record, devices: HomeAssistant["devices"], areas: HomeAssistant["areas"], cloudStatus: CloudStatus | undefined, @@ -296,12 +304,11 @@ export class VoiceAssistantsExpose extends LitElement { const result: Record = {}; - let filteredEntities = Object.values(entities); + let filteredEntities = Object.values(this.hass.states); filteredEntities = filteredEntities.filter((entity) => showAssistants.some( - (assis) => - extEntities?.[entity.entity_id].options?.[assis]?.should_expose + (assis) => exposedEntities?.[entity.entity_id]?.[assis] ) ); @@ -317,37 +324,38 @@ export class VoiceAssistantsExpose extends LitElement { filteredAssistants.some( (assis) => !(assis === "cloud.alexa" && alexaManual) && - extEntities?.[entity.entity_id].options?.[assis]?.should_expose + exposedEntities?.[entity.entity_id]?.[assis] ) ); } }); - for (const entry of filteredEntities) { - const entity = this.hass.states[entry.entity_id]; - const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id; + for (const entityState of filteredEntities) { + const entry: ExtEntityRegistryEntry | undefined = + 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; - result[entry.entity_id] = { - entity_id: entry.entity_id, - entity, + result[entityState.entity_id] = { + entity_id: entityState.entity_id, + entity: entityState, name: - computeEntityRegistryName( - this.hass!, - entry as EntityRegistryEntry - ) || + computeStateName(entityState) || this.hass.localize( "ui.panel.config.entities.picker.unnamed_entity" ), area: area ? area.name : "—", assistants: Object.keys( - extEntities![entry.entity_id].options! + exposedEntities?.[entityState.entity_id] ).filter( (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).alexa_entities ); - Object.keys(entities).forEach((entityId) => { + Object.keys(this.hass.states).forEach((entityId) => { const assistants: string[] = []; if ( alexaManual && @@ -383,30 +391,33 @@ export class VoiceAssistantsExpose extends LitElement { result[entityId].assistants.push(...assistants); result[entityId].manAssistants = assistants; } else { - const entry = entities[entityId]; - const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id; + const entityState = this.hass.states[entityId]; + 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; result[entityId] = { - entity_id: entry.entity_id, - entity: this.hass.states[entityId], - name: computeEntityRegistryName( - this.hass!, - entry as EntityRegistryEntry - ), + entity_id: entityState.entity_id, + entity: entityState, + name: computeStateName(entityState), area: area ? area.name : "—", assistants: [ - ...(extEntities - ? Object.keys(extEntities[entry.entity_id].options!).filter( + ...(exposedEntities + ? Object.keys( + exposedEntities?.[entityState.entity_id] + ).filter( (key) => showAssistants.includes(key) && - extEntities[entry.entity_id].options![key] - ?.should_expose + exposedEntities?.[entityState.entity_id]?.[key] ) : []), ...assistants, ], manAssistants: assistants, - aliases: extEntities?.[entityId].aliases, + aliases: entry?.aliases || [], }; } }); @@ -468,14 +479,14 @@ export class VoiceAssistantsExpose extends LitElement { } protected render() { - if (!this.hass || this.hass.entities === undefined) { + if (!this.hass || !this.exposedEntities || !this._extEntities) { return html``; } const activeFilters = this._activeFilters(this._searchParms); const filteredEntities = this._filteredEntities( - this._entities, this._extEntities, + this.exposedEntities, this.hass.devices, this.hass.areas, this.cloudStatus, @@ -621,9 +632,11 @@ export class VoiceAssistantsExpose extends LitElement { : this._availableAssistants(this.cloudStatus); showExposeEntityDialog(this, { filterAssistants: assistants, - extendedEntities: this._extEntities!, + exposedEntities: this.exposedEntities!, 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") ? this._searchParms.get("assistants")!.split(",") : this._availableAssistants(this.cloudStatus); - exposeEntities(this.hass, assistants, [entityId], false); + exposeEntities(this.hass, assistants, [entityId], false).then(() => + fireEvent(this, "exposed-entities-changed") + ); }; private _unexposeSelected() { @@ -670,7 +685,12 @@ export class VoiceAssistantsExpose extends LitElement { ), dismissText: this.hass.localize("ui.common.cancel"), confirm: () => { - exposeEntities(this.hass, assistants, this._selectedEntities, false); + exposeEntities( + this.hass, + assistants, + this._selectedEntities, + false + ).then(() => fireEvent(this, "exposed-entities-changed")); this._clearSelection(); }, }); @@ -698,7 +718,12 @@ export class VoiceAssistantsExpose extends LitElement { ), dismissText: this.hass.localize("ui.common.cancel"), confirm: () => { - exposeEntities(this.hass, assistants, this._selectedEntities, true); + exposeEntities( + this.hass, + assistants, + this._selectedEntities, + true + ).then(() => fireEvent(this, "exposed-entities-changed")); this._clearSelection(); }, }); @@ -710,7 +735,14 @@ export class VoiceAssistantsExpose extends LitElement { private _openEditEntry(ev: CustomEvent): void { 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() { diff --git a/src/panels/config/voice-assistants/ha-config-voice-assistants.ts b/src/panels/config/voice-assistants/ha-config-voice-assistants.ts index fe35b6dd78..2ea1fc334c 100644 --- a/src/panels/config/voice-assistants/ha-config-voice-assistants.ts +++ b/src/panels/config/voice-assistants/ha-config-voice-assistants.ts @@ -1,11 +1,18 @@ +import { consume } from "@lit-labs/context"; 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 { HassRouterPage, RouterOptions, } from "../../../layouts/hass-router-page"; import { HomeAssistant } from "../../../types"; -import { CloudStatus } from "../../../data/cloud"; export const voiceAssistantTabs = [ { @@ -30,6 +37,28 @@ class HaConfigVoiceAssistants extends HassRouterPage { @property() public isWide!: boolean; + @state() + @consume({ context: entitiesContext, subscribe: true }) + _entities!: HomeAssistant["entities"]; + + @state() private _exposedEntities?: Record; + + 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 = { defaultPage: "assistants", routes: { @@ -55,11 +84,30 @@ class HaConfigVoiceAssistants extends HassRouterPage { pageEl.narrow = this.narrow; pageEl.isWide = this.isWide; 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 { interface HTMLElementTagNameMap { "ha-config-voice-assistants": HaConfigVoiceAssistants; } + interface HASSDomEvents { + "exposed-entities-changed": undefined; + } } diff --git a/src/panels/config/voice-assistants/show-dialog-expose-entity.ts b/src/panels/config/voice-assistants/show-dialog-expose-entity.ts index acb406e711..4a9a992363 100644 --- a/src/panels/config/voice-assistants/show-dialog-expose-entity.ts +++ b/src/panels/config/voice-assistants/show-dialog-expose-entity.ts @@ -1,9 +1,9 @@ import { fireEvent } from "../../../common/dom/fire_event"; -import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; +import { ExposeEntitySettings } from "../../../data/expose"; export interface ExposeEntityDialogParams { filterAssistants: string[]; - extendedEntities: Record; + exposedEntities: Record; exposeEntities: (entities: string[]) => void; } diff --git a/src/panels/config/voice-assistants/show-dialog-voice-settings.ts b/src/panels/config/voice-assistants/show-dialog-voice-settings.ts index 1acc94bb10..7a03951272 100644 --- a/src/panels/config/voice-assistants/show-dialog-voice-settings.ts +++ b/src/panels/config/voice-assistants/show-dialog-voice-settings.ts @@ -1,7 +1,12 @@ import { fireEvent } from "../../../common/dom/fire_event"; +import { ExtEntityRegistryEntry } from "../../../data/entity_registry"; +import { ExposeEntitySettings } from "../../../data/expose"; export interface VoiceSettingsDialogParams { entityId: string; + exposed: ExposeEntitySettings; + extEntityReg?: ExtEntityRegistryEntry; + exposedEntitiesChanged?: () => void; } export const loadVoiceSettingsDialog = () => import("./dialog-voice-settings"); diff --git a/src/panels/lovelace/hui-root.ts b/src/panels/lovelace/hui-root.ts index 02b18b6122..288646a280 100644 --- a/src/panels/lovelace/hui-root.ts +++ b/src/panels/lovelace/hui-root.ts @@ -1052,10 +1052,6 @@ class HUIRoot extends LitElement { #view { position: relative; display: flex; - background: var( - --lovelace-background, - var(--primary-background-color) - ); padding-top: calc(var(--header-height) + env(safe-area-inset-top)); min-height: 100vh; box-sizing: border-box; @@ -1063,8 +1059,13 @@ class HUIRoot extends LitElement { padding-right: env(safe-area-inset-right); padding-bottom: env(safe-area-inset-bottom); } - hui-view, - hui-unused-entities { + hui-view { + background: var( + --lovelace-background, + var(--primary-background-color) + ); + } + #view > * { flex: 1 1 100%; max-width: 100%; } diff --git a/src/translations/en.json b/src/translations/en.json index fa194a5049..6bdbb08f21 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -838,7 +838,10 @@ "input_label": "Enter a request", "send_text": "Send text", "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": { "cancel": "Cancel", @@ -1082,6 +1085,7 @@ "expose_header": "Expose", "aliases_header": "Aliases", "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", "manual_config": "Managed in configuration.yaml", "unsupported": "Unsupported"