Update expose for entities not in the entity registry (#16377)

This commit is contained in:
Bram Kragten 2023-05-02 21:57:51 +02:00 committed by GitHub
parent 6c0011fb45
commit e766c277f5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
17 changed files with 340 additions and 224 deletions

View File

@ -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`
<div class="layout horizontal center-center row">
<ha-textfield
.disabled=${this.disabled}
dialogInitialFocus=${index}
.index=${index}
class="flex-auto"
@ -37,6 +40,7 @@ class AliasesEditor extends LitElement {
@keydown=${this._keyDownAlias}
></ha-textfield>
<ha-icon-button
.disabled=${this.disabled}
.index=${index}
slot="navigationIcon"
label=${this.hass!.localize("ui.dialogs.aliases.remove_alias", {
@ -49,7 +53,7 @@ class AliasesEditor extends LitElement {
`
)}
<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")}
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
</mwc-button>

View File

@ -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<string, ExposeEntitySettings> }>({
type: "homeassistant/expose_entity/list",
});

View File

@ -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`<entity-voice-settings
.hass=${this.hass}
.entityId=${this.entry.entity_id}
.entry=${this.entry}
.exposed=${this._calculateExposed(this.entry)}
></entity-voice-settings>`;
}

View File

@ -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`
<div class="content">
${this.hass.localize(
"ui.dialogs.entity_registry.no_unique_id",
"entity_id",
this.entityId,
"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 alert-type="warning">
${this.hass.localize("ui.dialogs.entity_registry.no_unique_id", {
entity_id: this.entityId,
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>
</div>
`;
}

View File

@ -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<string, ExtEntityRegistryEntry>;
@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<string, ExtEntityRegistryEntry>) =>
Object.values(extEntities).filter(
(entity) => entity.options?.conversation?.should_expose
).length
private _exposedEntitiesCount = memoizeOne(
(exposedEntities: Record<string, ExposeEntitySettings>) =>
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,
}
)}

View File

@ -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<string, ExtEntityRegistryEntry>;
@property({ attribute: false }) public exposedEntities?: Record<
string,
ExposeEntitySettings
>;
@property() public cloudStatus?: CloudStatusLoggedIn;
@state() private _exposeNew?: boolean;
private _exposedEntities = memoizeOne(
(extEntities: Record<string, ExtEntityRegistryEntry>) =>
Object.values(extEntities).filter(
(entity) => entity.options?.["cloud.alexa"]?.should_expose
).length
private _exposedEntitiesCount = memoizeOne(
(exposedEntities: Record<string, ExposeEntitySettings>) =>
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,
}
)}

View File

@ -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<string, ExtEntityRegistryEntry>;
@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<string, ExtEntityRegistryEntry>) =>
Object.values(extEntities).filter(
(entity) => entity.options?.["cloud.google_assistant"]?.should_expose
private _exposedEntitiesCount = memoizeOne(
(exposedEntities: Record<string, ExposeEntitySettings>) =>
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,
}
)}

View File

@ -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<string, ExtEntityRegistryEntry>, filter?: string) => {
(
exposedEntities: Record<string, ExposeEntitySettings>,
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`
<ha-check-list-item
graphic="icon"
twoLine
.value=${entity.entity_id}
.selected=${this._selected.includes(entity.entity_id)}
@request-selected=${this._handleSelected}
>
<ha-state-icon
title=${ifDefined(entityState?.state)}
slot="graphic"
.state=${entityState}
></ha-state-icon>
${computeEntityRegistryName(this.hass!, entity)}
<span slot="secondary">${entity.entity_id}</span>
</ha-check-list-item>
`;
};
private _renderItem = (entityState: HassEntity) => html`
<ha-check-list-item
graphic="icon"
twoLine
.value=${entityState.entity_id}
.selected=${this._selected.includes(entityState.entity_id)}
@request-selected=${this._handleSelected}
>
<ha-state-icon
title=${ifDefined(entityState?.state)}
slot="graphic"
.state=${entityState}
></ha-state-icon>
${computeStateName(entityState)}
<span slot="secondary">${entityState.entity_id}</span>
</ha-check-list-item>
`;
private _expose() {
this._params!.exposeEntities(this._selected);

View File

@ -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<void> {
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")
)}
>
<div>
<entity-voice-settings
.hass=${this.hass}
.entry=${this._extEntityReg}
.entityId=${this._params.entityId}
.entry=${this._params.extEntityReg}
.exposed=${this._params.exposed}
@entity-entry-updated=${this._entityEntryUpdated}
@exposed-entities-changed=${this._exposedEntitiesChanged}
></entity-voice-settings>
</div>
</ha-dialog>
@ -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 {

View File

@ -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) {
)}
>
<ha-checkbox
.checked=${!this.entry.options?.[key]?.disable_2fa}
.checked=${!this.entry!.options?.[key]?.disable_2fa}
@change=${this._2faChanged}
></ha-checkbox>
</ha-formfield>
@ -274,12 +281,26 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
${this.hass.localize("ui.dialogs.voice-settings.aliases_description")}
</p>
<ha-aliases-editor
.hass=${this.hass}
.aliases=${this._aliases ?? this.entry.aliases}
@value-changed=${this._aliasesChanged}
@blur=${this._saveAliases}
></ha-aliases-editor>
${!this.entry
? html`<ha-alert alert-type="warning">
${this.hass.localize(
"ui.dialogs.voice-settings.aliases_no_unique_id",
{
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 {
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 {

View File

@ -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";

View File

@ -1,16 +1,12 @@
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";
@ -26,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<string, ExtEntityRegistryEntry>;
protected render() {
if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
@ -57,7 +52,7 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
<assist-pref
.hass=${this.hass}
.cloudStatus=${this.cloudStatus}
.extEntities=${this._extEntities}
.exposedEntities=${this.exposedEntities}
></assist-pref>
`
: nothing}
@ -65,13 +60,13 @@ export class HaConfigVoiceAssistantsAssistants extends LitElement {
? html`
<cloud-alexa-pref
.hass=${this.hass}
.extEntities=${this._extEntities}
.exposedEntities=${this.exposedEntities}
.cloudStatus=${this.cloudStatus}
dir=${computeRTLDirection(this.hass)}
></cloud-alexa-pref>
<cloud-google-pref
.hass=${this.hass}
.extEntities=${this._extEntities}
.exposedEntities=${this.exposedEntities}
.cloudStatus=${this.cloudStatus}
dir=${computeRTLDirection(this.hass)}
></cloud-google-pref>
@ -82,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;

View File

@ -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<string, ExtEntityRegistryEntry> | undefined,
entities: Record<string, ExtEntityRegistryEntry>,
exposedEntities: Record<string, ExposeEntitySettings>,
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
cloudStatus: CloudStatus | undefined,
@ -296,12 +304,11 @@ export class VoiceAssistantsExpose extends LitElement {
const result: Record<string, DataTableRowData> = {};
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`<hass-loading-screen></hass-loading-screen>`;
}
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() {

View File

@ -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<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 = {
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;
}
}

View File

@ -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<string, ExtEntityRegistryEntry>;
exposedEntities: Record<string, ExposeEntitySettings>;
exposeEntities: (entities: string[]) => void;
}

View File

@ -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");

View File

@ -1085,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"