Compare commits

..

2 Commits

Author SHA1 Message Date
Aidan Timson 8cb6c0ccf9 Use automation descriptions instead of target ui 2026-06-23 12:32:13 +01:00
Aidan Timson 8006eff03c Add row targets UI to entity state and numeric state in card condition editor 2026-06-23 12:20:28 +01:00
28 changed files with 468 additions and 915 deletions
+1 -5
View File
@@ -6,15 +6,11 @@ export interface AlexaEntity {
interfaces: string[];
}
export interface AlexaEntityConfig {
name?: string | null;
}
export const fetchCloudAlexaEntities = (hass: HomeAssistant) =>
hass.callWS<AlexaEntity[]>({ type: "cloud/alexa/entities" });
export const fetchCloudAlexaEntity = (hass: HomeAssistant, entity_id: string) =>
hass.callWS<AlexaEntityConfig>({
hass.callWS<AlexaEntity>({
type: "cloud/alexa/entities/get",
entity_id,
});
+2 -13
View File
@@ -172,23 +172,12 @@ export const removeCloudData = (hass: HomeAssistant) =>
export const updateCloudGoogleEntityConfig = (
hass: HomeAssistant,
entity_id: string,
values: { disable_2fa?: boolean; name?: string | null; aliases?: string[] }
disable_2fa: boolean
) =>
hass.callWS({
type: "cloud/google_assistant/entities/update",
entity_id,
...values,
});
export const updateCloudAlexaEntityConfig = (
hass: HomeAssistant,
entity_id: string,
name: string | null
) =>
hass.callWS({
type: "cloud/alexa/entities/update",
entity_id,
name,
disable_2fa,
});
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
-2
View File
@@ -5,8 +5,6 @@ export interface GoogleEntity {
traits: string[];
might_2fa: boolean;
disable_2fa?: boolean;
name?: string | null;
aliases?: string[] | null;
}
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
@@ -1,33 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { ExtEntityRegistryEntry } from "../../../../data/entity/entity_registry";
import "../../../../panels/config/voice-assistants/voice-assistant-settings";
import type { HomeAssistant } from "../../../../types";
@customElement("ha-more-info-view-voice-assistant-settings")
class MoreInfoViewVoiceAssistantSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
@property({ attribute: false }) public params?: { assistant: string };
protected render() {
if (!this.params || !this.entry) {
return nothing;
}
return html`<voice-assistant-settings
.hass=${this.hass}
.entityId=${this.entry.entity_id}
.assistant=${this.params.assistant}
.entry=${this.entry}
></voice-assistant-settings>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-more-info-view-voice-assistant-settings": MoreInfoViewVoiceAssistantSettings;
}
}
@@ -7,7 +7,6 @@ import type { ExposeEntitySettings } from "../../../../data/expose";
import { voiceAssistants } from "../../../../data/expose";
import "../../../../panels/config/voice-assistants/entity-voice-settings";
import type { HomeAssistant } from "../../../../types";
import { showVoiceAssistantSettingsView } from "./show-view-voice-assistant-settings";
@customElement("ha-more-info-view-voice-assistants")
class MoreInfoViewVoiceAssistants extends LitElement {
@@ -34,19 +33,9 @@ class MoreInfoViewVoiceAssistants extends LitElement {
.entityId=${this.entry.entity_id}
.entry=${this.entry}
.exposed=${this._calculateExposed(this.entry)}
@edit-assistant=${this._editAssistant}
></entity-voice-settings>`;
}
private _editAssistant(ev: CustomEvent) {
const assistant = ev.detail.assistant;
showVoiceAssistantSettingsView(
this,
voiceAssistants[assistant].name,
assistant
);
}
static get styles(): CSSResultGroup {
return [
css`
@@ -1,17 +0,0 @@
import { fireEvent } from "../../../../common/dom/fire_event";
export const loadVoiceAssistantSettingsView = () =>
import("./ha-more-info-view-voice-assistant-settings");
export const showVoiceAssistantSettingsView = (
element: HTMLElement,
title: string,
assistant: string
): void => {
fireEvent(element, "show-child-view", {
viewTag: "ha-more-info-view-voice-assistant-settings",
viewImport: loadVoiceAssistantSettingsView,
viewTitle: title,
viewParams: { assistant },
});
};
@@ -42,7 +42,7 @@ export class MoreInfoLogbook extends LitElement {
.hass=${this.hass}
.time=${this._time}
.entityIds=${this._entityIdAsList(this.entityId)}
name-detail="none"
.scope=${"entity"}
narrow
no-icon
graph-color
@@ -619,7 +619,7 @@ class HaConfigAreaPage extends SubscribeMixin(LitElement) {
.time=${this._logbookTime}
.entityIds=${this._allEntities(memberships)}
.deviceIds=${this._allDeviceIds(memberships.devices)}
name-detail="device"
.scope=${"area"}
virtualize
narrow
no-icon
@@ -921,7 +921,7 @@ export class HaConfigDevicePage extends LitElement {
.time=${this._logbookTime}
.entityIds=${this._entityIds(entities)}
.deviceIds=${this._deviceIdInList(this.deviceId)}
name-detail="entity"
.scope=${"device"}
virtualize
narrow
no-icon
@@ -1011,9 +1011,13 @@ export class EntityRegistrySettingsEditor extends LitElement {
)}</span
>
<span slot="secondary">
${this.hass.localize(
"ui.dialogs.entity_registry.editor.voice_assistants_description"
)}
${this.entry.aliases.filter((a) => a !== null).length
? this.entry.aliases
.filter((a): a is string => a !== null)
.join(", ")
: this.hass.localize(
"ui.dialogs.entity_registry.editor.no_aliases"
)}
</span>
<ha-icon-next slot="meta"></ha-icon-next>
</ha-list-item>
@@ -1,95 +0,0 @@
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/input/ha-input";
import type { AlexaEntityConfig } from "../../../data/alexa";
import { fetchCloudAlexaEntity } from "../../../data/alexa";
import { updateCloudAlexaEntityConfig } from "../../../data/cloud";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@customElement("alexa-entity-voice-settings")
export class AlexaEntityVoiceSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@state() private _entity?: AlexaEntityConfig;
protected willUpdate(changedProps: PropertyValues<this>) {
if (changedProps.has("entityId") && this.entityId) {
this._fetchEntity();
}
}
private async _fetchEntity() {
try {
this._entity = await fetchCloudAlexaEntity(this.hass, this.entityId);
} catch (_err) {
this._entity = undefined;
}
}
protected render() {
if (!this._entity) {
return nothing;
}
const defaultName = this.hass.states[this.entityId]
? computeStateName(this.hass.states[this.entityId])
: this.entityId;
return html`
<ha-input
.label=${this.hass.localize("ui.dialogs.voice-settings.name")}
.hint=${this.hass.localize(
"ui.dialogs.voice-settings.name_description"
)}
with-clear
.value=${this._entity.name ?? ""}
.placeholder=${defaultName}
@change=${this._nameChanged}
></ha-input>
`;
}
private async _nameChanged(ev) {
if (!this._entity) {
return;
}
const value = ev.target.value?.trim() || null;
if ((this._entity.name ?? null) === value) {
return;
}
const previous = this._entity.name ?? null;
this._entity = { ...this._entity, name: value };
try {
await updateCloudAlexaEntityConfig(this.hass, this.entityId, value);
} catch (_err) {
this._entity = { ...this._entity, name: previous };
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
margin: 0 var(--ha-space-8) var(--ha-space-8);
}
ha-input {
display: block;
width: 100%;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"alexa-entity-voice-settings": AlexaEntityVoiceSettings;
}
}
@@ -1,146 +0,0 @@
import type { CSSResultGroup } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-alert";
import "../../../components/ha-aliases-editor";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
import { updateEntityRegistryEntry } from "../../../data/entity/entity_registry";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
@customElement("assist-entity-voice-settings")
export class AssistEntityVoiceSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
@state() private _aliases?: (string | null)[];
protected render() {
if (!this.entry) {
return 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>`;
}
return html`
<ha-md-list-item>
<span slot="headline">
${this.hass.states[this.entityId]
? computeStateName(this.hass.states[this.entityId])
: this.entityId}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.dialogs.voice-settings.entity_name_alias_description"
)}
</span>
<ha-switch
slot="end"
.checked=${(this._aliases ?? this.entry.aliases).includes(null)}
@change=${this._toggleEntityNameAlias}
></ha-switch>
</ha-md-list-item>
<h4 class="header">
${this.hass.localize("ui.dialogs.voice-settings.aliases")}
</h4>
<ha-aliases-editor
.aliases=${(this._aliases ?? this.entry.aliases).filter(
(a): a is string => a !== null
)}
sortable
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
`;
}
private async _toggleEntityNameAlias(ev) {
const previous = this._aliases;
const enabled = ev.target.checked;
const currentAliases = this._aliases ?? this.entry?.aliases ?? [];
if (enabled) {
this._aliases = [null, ...currentAliases.filter((a) => a !== null)];
} else {
this._aliases = currentAliases.filter((a): a is string => a !== null);
}
await this._saveAliases(previous);
}
private _aliasesChanged(ev) {
const previous = this._aliases;
const currentAliases = this._aliases ?? this.entry?.aliases ?? [];
const hasNull = currentAliases.includes(null);
const nullAliases: (string | null)[] = hasNull ? [null] : [];
const newStringAliases: string[] = ev.detail.value;
this._aliases = [...nullAliases, ...newStringAliases];
this._saveAliases(previous);
}
private async _saveAliases(previous?: (string | null)[]) {
if (!this._aliases) {
return;
}
const hasNull = this._aliases.includes(null);
const nullAliases: null[] = hasNull ? [null] : [];
const stringAliases = this._aliases
.filter((a): a is string => a !== null)
.map((alias) => alias.trim())
.filter((alias) => alias);
try {
const result = await updateEntityRegistryEntry(this.hass, this.entityId, {
aliases: [...nullAliases, ...stringAliases],
});
fireEvent(this, "entity-entry-updated", result.entity_entry);
} catch (_err) {
this._aliases = previous;
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
margin: 0 var(--ha-space-8) var(--ha-space-8);
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
ha-aliases-editor {
display: block;
}
.header {
margin-top: var(--ha-space-2);
margin-bottom: var(--ha-space-1);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"assist-entity-voice-settings": AssistEntityVoiceSettings;
}
interface HASSDomEvents {
"entity-entry-updated": ExtEntityRegistryEntry;
}
}
@@ -1,4 +1,4 @@
import { mdiChevronLeft, mdiTuneVertical } from "@mdi/js";
import { mdiTuneVertical } from "@mdi/js";
import type { CSSResultGroup } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -6,17 +6,10 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-icon-button";
import "../../../components/ha-dialog";
import type { ExposeEntitySettings } from "../../../data/expose";
import { voiceAssistants } from "../../../data/expose";
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
import {
haStyle,
haStyleDialog,
haStyleDialogFixedTop,
} from "../../../resources/styles";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import "./entity-voice-settings";
import "./voice-assistant-settings";
import type { VoiceSettingsDialogParams } from "./show-dialog-voice-settings";
@customElement("dialog-voice-settings")
@@ -27,14 +20,8 @@ class DialogVoiceSettings extends LitElement {
@state() private _open = false;
@state() private _editingAssistant?: string;
@state() private _exposed?: ExposeEntitySettings;
public showDialog(params: VoiceSettingsDialogParams): void {
this._params = params;
this._exposed = params.exposed;
this._editingAssistant = undefined;
this._open = true;
}
@@ -44,8 +31,6 @@ class DialogVoiceSettings extends LitElement {
private _dialogClosed(): void {
this._params = undefined;
this._exposed = undefined;
this._editingAssistant = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -56,23 +41,14 @@ class DialogVoiceSettings extends LitElement {
this.closeDialog();
}
private _editAssistant(ev: CustomEvent): void {
this._editingAssistant = ev.detail.assistant;
}
private _backToList(): void {
this._editingAssistant = undefined;
}
protected render() {
if (!this._params) {
return nothing;
}
const title = this._editingAssistant
? voiceAssistants[this._editingAssistant].name
: computeStateName(this.hass.states[this._params.entityId]) ||
this.hass.localize("ui.panel.config.entities.picker.unnamed_entity");
const title =
computeStateName(this.hass.states[this._params.entityId]) ||
this.hass.localize("ui.panel.config.entities.picker.unnamed_entity");
return html`
<ha-dialog
@@ -80,58 +56,28 @@ class DialogVoiceSettings extends LitElement {
header-title=${title}
@closed=${this._dialogClosed}
>
${this._editingAssistant
? html`<ha-icon-button
slot="headerNavigationIcon"
.label=${this.hass.localize("ui.common.back")}
.path=${mdiChevronLeft}
@click=${this._backToList}
></ha-icon-button>`
: html`<ha-icon-button
slot="headerActionItems"
.label=${this.hass.localize(
"ui.dialogs.voice-settings.view_entity"
)}
.path=${mdiTuneVertical}
@click=${this._viewMoreInfo}
></ha-icon-button>`}
<div>${this._renderContent()}</div>
<ha-icon-button
slot="headerActionItems"
.label=${this.hass.localize("ui.dialogs.voice-settings.view_entity")}
.path=${mdiTuneVertical}
@click=${this._viewMoreInfo}
></ha-icon-button>
<div>
<entity-voice-settings
.hass=${this.hass}
.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>
`;
}
private _renderContent() {
const entityId = this._params!.entityId;
if (this._editingAssistant) {
return html`<voice-assistant-settings
.hass=${this.hass}
.entityId=${entityId}
.assistant=${this._editingAssistant}
.entry=${this._params!.extEntityReg}
@entity-entry-updated=${this._entityEntryUpdated}
></voice-assistant-settings>`;
}
return html`<entity-voice-settings
.hass=${this.hass}
.entityId=${entityId}
.entry=${this._params!.extEntityReg}
.exposed=${this._exposed!}
@edit-assistant=${this._editAssistant}
@exposed-changed=${this._exposedChanged}
@entity-entry-updated=${this._entityEntryUpdated}
@exposed-entities-changed=${this._exposedEntitiesChanged}
></entity-voice-settings>`;
}
private _exposedChanged(ev: CustomEvent): void {
this._exposed = ev.detail.value;
}
private _entityEntryUpdated(ev: CustomEvent) {
this._params!.extEntityReg = ev.detail;
this._params!.entityEntryUpdated?.(ev.detail);
}
private _exposedEntitiesChanged() {
@@ -142,7 +88,6 @@ class DialogVoiceSettings extends LitElement {
return [
haStyle,
haStyleDialog,
haStyleDialogFixedTop,
css`
ha-dialog {
--dialog-content-padding: 0;
@@ -1,10 +1,11 @@
import { mdiAlertCircle, mdiCog } from "@mdi/js";
import { mdiAlertCircle } from "@mdi/js";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
import { computeStateName } from "../../../common/entity/compute_state_name";
import type {
EntityDomainFilter,
EntityDomainFilterFunc,
@@ -13,24 +14,35 @@ import {
generateEntityDomainFilter,
isEmptyEntityDomainFilter,
} from "../../../common/entity/entity_domain_filter";
import "../../../components/ha-icon-button";
import "../../../components/ha-alert";
import "../../../components/ha-aliases-editor";
import "../../../components/ha-checkbox";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import "../../../components/voice-assistant-brand-icon";
import { fetchCloudAlexaEntity } from "../../../data/alexa";
import type { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
import { fetchCloudStatus } from "../../../data/cloud";
import {
fetchCloudStatus,
updateCloudGoogleEntityConfig,
} from "../../../data/cloud";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
import { getExtendedEntityRegistryEntry } from "../../../data/entity/entity_registry";
import {
getExtendedEntityRegistryEntry,
updateEntityRegistryEntry,
} from "../../../data/entity/entity_registry";
import type { ExposeEntitySettings } from "../../../data/expose";
import { exposeEntities, voiceAssistants } from "../../../data/expose";
import type { GoogleEntity } from "../../../data/google_assistant";
import { fetchCloudGoogleEntity } from "../../../data/google_assistant";
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
import { documentationUrl } from "../../../util/documentation-url";
import type { EntityRegistrySettings } from "../entities/entity-registry-settings";
@customElement("entity-voice-settings")
export class EntityVoiceSettings extends LitElement {
export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@@ -41,6 +53,8 @@ export class EntityVoiceSettings extends LitElement {
@state() private _cloudStatus?: CloudStatus;
@state() private _aliases?: (string | null)[];
@state() private _googleEntity?: GoogleEntity;
@state() private _unsupported: Partial<
@@ -63,16 +77,16 @@ export class EntityVoiceSettings extends LitElement {
private async _fetchEntities() {
try {
this._googleEntity = await fetchCloudGoogleEntity(
const googleEntity = await fetchCloudGoogleEntity(
this.hass,
this.entityId
);
this._googleEntity = googleEntity;
this.requestUpdate("_googleEntity");
} catch (err: any) {
if (err.code === "not_supported") {
this._unsupported = {
...this._unsupported,
"cloud.google_assistant": true,
};
this._unsupported["cloud.google_assistant"] = true;
this.requestUpdate("_unsupported");
}
}
@@ -80,7 +94,8 @@ export class EntityVoiceSettings extends LitElement {
await fetchCloudAlexaEntity(this.hass, this.entityId);
} catch (err: any) {
if (err.code === "not_supported") {
this._unsupported = { ...this._unsupported, "cloud.alexa": true };
this._unsupported["cloud.alexa"] = true;
this.requestUpdate("_unsupported");
}
}
}
@@ -112,6 +127,7 @@ export class EntityVoiceSettings extends LitElement {
this._cloudStatus.prefs.alexa_enabled === true;
const showAssistants = [...Object.keys(voiceAssistants)];
const uiAssistants = [...showAssistants];
const alexaManual =
alexaEnabled &&
@@ -129,12 +145,20 @@ export class EntityVoiceSettings extends LitElement {
showAssistants.indexOf("cloud.google_assistant"),
1
);
uiAssistants.splice(showAssistants.indexOf("cloud.google_assistant"), 1);
} else if (googleManual) {
uiAssistants.splice(uiAssistants.indexOf("cloud.google_assistant"), 1);
}
if (!alexaEnabled) {
showAssistants.splice(showAssistants.indexOf("cloud.alexa"), 1);
uiAssistants.splice(uiAssistants.indexOf("cloud.alexa"), 1);
} else if (alexaManual) {
uiAssistants.splice(uiAssistants.indexOf("cloud.alexa"), 1);
}
const uiExposed = uiAssistants.some((key) => this.exposed[key]);
let manFilterFuncs:
| {
google: EntityDomainFilterFunc;
@@ -153,97 +177,216 @@ export class EntityVoiceSettings extends LitElement {
const manExposedGoogle =
googleManual && manFilterFuncs!.google(this.entityId);
const anyExposed = uiExposed || manExposedAlexa || manExposedGoogle;
return html`
${showAssistants.map((key) => {
const supported = !this._unsupported[key];
<ha-md-list-item>
<h3 slot="headline">
${this.hass.localize("ui.dialogs.voice-settings.expose_header")}
</h3>
<ha-switch
slot="end"
@change=${this._toggleAll}
.assistants=${uiAssistants}
.checked=${anyExposed}
></ha-switch>
</ha-md-list-item>
${anyExposed
? showAssistants.map((key) => {
const supported = !this._unsupported[key];
const exposed =
alexaManual && key === "cloud.alexa"
? manExposedAlexa
: googleManual && key === "cloud.google_assistant"
? manExposedGoogle
: this.exposed[key];
const exposed =
alexaManual && key === "cloud.alexa"
? manExposedAlexa
: googleManual && key === "cloud.google_assistant"
? manExposedGoogle
: this.exposed[key];
const manualConfig =
(alexaManual && key === "cloud.alexa") ||
(googleManual && key === "cloud.google_assistant");
const manualConfig =
(alexaManual && key === "cloud.alexa") ||
(googleManual && key === "cloud.google_assistant");
const hasSettings = supported && !manualConfig;
const support2fa =
key === "cloud.google_assistant" &&
!googleManual &&
supported &&
this._googleEntity?.might_2fa;
const aliasCount =
key === "conversation"
? this.entry
? this.entry.aliases.filter(Boolean).length
: undefined
: key === "cloud.google_assistant"
? (this._googleEntity?.aliases?.filter(Boolean).length ?? 0)
: undefined;
return html`
<ha-md-list-item>
<voice-assistant-brand-icon slot="start" .voiceAssistantId=${key}>
</voice-assistant-brand-icon>
<span slot="headline">${voiceAssistants[key].name}</span>
${!supported
? html`<div slot="supporting-text" class="unsupported">
<ha-svg-icon .path=${mdiAlertCircle}></ha-svg-icon>
${this.hass.localize("ui.dialogs.voice-settings.unsupported")}
</div>`
: manualConfig
? html`
<div slot="supporting-text">
return html`
<ha-md-list-item>
<voice-assistant-brand-icon
slot="start"
.voiceAssistantId=${key}
>
</voice-assistant-brand-icon>
<span slot="headline">${voiceAssistants[key].name}</span>
${!supported
? html`<div slot="supporting-text" class="unsupported">
<ha-svg-icon .path=${mdiAlertCircle}></ha-svg-icon>
${this.hass.localize(
"ui.dialogs.voice-settings.manual_config"
)}
</div>
`
: aliasCount
? html`<div slot="supporting-text">
${this.hass.localize(
"ui.dialogs.voice-settings.aliases_count",
{ count: aliasCount }
"ui.dialogs.voice-settings.unsupported"
)}
</div>`
: nothing}
<div slot="end" class="trailing">
${hasSettings
? html`<ha-icon-button
.path=${mdiCog}
.label=${this.hass.localize(
"ui.dialogs.voice-settings.edit_settings",
{ assistant: voiceAssistants[key].name }
)}
.assistant=${key}
@click=${this._editAssistant}
></ha-icon-button>`
: nothing}
${manualConfig
? html`
<div slot="supporting-text">
${this.hass.localize(
"ui.dialogs.voice-settings.manual_config"
)}
</div>
`
: nothing}
${support2fa
? html`
<ha-checkbox
slot="supporting-text"
.checked=${!this._googleEntity!.disable_2fa}
@change=${this._2faChanged}
>
${this.hass.localize(
"ui.dialogs.voice-settings.ask_pin"
)}
</ha-checkbox>
`
: nothing}
<ha-switch
slot="end"
.assistant=${key}
@change=${this._toggleAssistant}
.disabled=${manualConfig || (!exposed && !supported)}
.checked=${exposed}
></ha-switch>
</ha-md-list-item>
`;
})
: nothing}
<h3 class="header">
${this.hass.localize("ui.dialogs.voice-settings.aliases_header")}
</h3>
<p class="description">
${this.hass.localize("ui.dialogs.voice-settings.aliases_description")}
</p>
${!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-md-list-item>
<span slot="headline">
${this.hass.states[this.entityId]
? computeStateName(this.hass.states[this.entityId])
: this.entityId}
</span>
<span slot="supporting-text">
${this.hass.localize(
"ui.dialogs.voice-settings.entity_name_alias_description"
)}
</span>
<ha-switch
.assistant=${key}
@change=${this._toggleAssistant}
.disabled=${manualConfig || (!exposed && !supported)}
.checked=${exposed}
slot="end"
.checked=${(this._aliases ?? this.entry.aliases).includes(null)}
@change=${this._toggleEntityNameAlias}
></ha-switch>
</div>
</ha-md-list-item>
`;
})}
</ha-md-list-item>
<ha-aliases-editor
.aliases=${(this._aliases ?? this.entry.aliases).filter(
(a): a is string => a !== null
)}
sortable
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
`}
`;
}
private _editAssistant(ev) {
fireEvent(this, "edit-assistant", { assistant: ev.target.assistant });
private async _toggleEntityNameAlias(ev) {
const enabled = ev.target.checked;
const currentAliases = this._aliases ?? this.entry?.aliases ?? [];
if (enabled) {
this._aliases = [null, ...currentAliases.filter((a) => a !== null)];
} else {
this._aliases = currentAliases.filter((a): a is string => a !== null);
}
await this._saveAliases();
}
private _aliasesChanged(ev) {
const currentAliases = this._aliases ?? this.entry?.aliases ?? [];
const hasNull = currentAliases.includes(null);
const nullAliases: (string | null)[] = hasNull ? [null] : [];
const newStringAliases: string[] = ev.detail.value;
this._aliases = [...nullAliases, ...newStringAliases];
this._saveAliases();
}
private async _2faChanged(ev) {
try {
await updateCloudGoogleEntityConfig(
this.hass,
this.entityId,
!ev.target.checked
);
} catch (_err) {
ev.target.checked = !ev.target.checked;
}
}
private async _saveAliases() {
if (!this._aliases) {
return;
}
const hasNull = this._aliases.includes(null);
const nullAliases: null[] = hasNull ? [null] : [];
const stringAliases = this._aliases
.filter((a): a is string => a !== null)
.map((alias) => alias.trim())
.filter((alias) => alias);
const result = await updateEntityRegistryEntry(this.hass, this.entityId, {
aliases: [...nullAliases, ...stringAliases],
});
fireEvent(this, "entity-entry-updated", result.entity_entry);
}
private async _toggleAssistant(ev) {
ev.stopPropagation();
const assistant: string = ev.target.assistant;
const checked: boolean = ev.target.checked;
exposeEntities(
this.hass,
[ev.target.assistant],
[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");
}
exposeEntities(this.hass, [assistant], [this.entityId], checked);
fireEvent(this, "exposed-changed", {
value: { ...this.exposed, [assistant]: checked },
});
private async _toggleAll(ev) {
const expose = ev.target.checked;
const assistants = expose
? ev.target.assistants.filter((key) => !this._unsupported[key])
: ev.target.assistants;
exposeEntities(this.hass, assistants, [this.entityId], ev.target.checked);
if (this.entry) {
const entry = await getExtendedEntityRegistryEntry(
this.hass,
@@ -260,7 +403,7 @@ export class EntityVoiceSettings extends LitElement {
css`
:host {
display: block;
margin: var(--ha-space-8);
margin: 32px;
margin-top: 0;
}
ha-md-list-item {
@@ -268,10 +411,19 @@ export class EntityVoiceSettings extends LitElement {
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
.trailing {
display: flex;
align-items: center;
gap: var(--ha-space-2);
img {
height: 32px;
width: 32px;
margin-right: 16px;
margin-inline-end: 16px;
margin-inline-start: initial;
}
ha-aliases-editor {
display: block;
}
ha-alert {
display: block;
margin-top: 16px;
}
.unsupported {
display: flex;
@@ -280,10 +432,21 @@ export class EntityVoiceSettings extends LitElement {
.unsupported ha-svg-icon {
color: var(--error-color);
--mdc-icon-size: 16px;
margin-right: var(--ha-space-1);
margin-inline-end: var(--ha-space-1);
margin-right: 4px;
margin-inline-end: 4px;
margin-inline-start: initial;
}
.header {
margin-top: 8px;
margin-bottom: 4px;
}
.description {
color: var(--secondary-text-color);
font-size: var(--ha-font-size-m);
line-height: var(--ha-line-height-condensed);
margin-top: 0;
margin-bottom: 16px;
}
`,
];
}
@@ -291,11 +454,15 @@ export class EntityVoiceSettings extends LitElement {
declare global {
interface HTMLElementTagNameMap {
"entity-voice-settings": EntityVoiceSettings;
"entity-registry-settings": EntityRegistrySettings;
}
interface HASSDomEvents {
"entity-entry-updated": ExtEntityRegistryEntry;
"edit-assistant": { assistant: string };
"exposed-changed": { value: ExposeEntitySettings };
}
}
declare global {
interface HTMLElementTagNameMap {
"entity-voice-settings": EntityVoiceSettings;
}
}
@@ -1,176 +0,0 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import type { CSSResultGroup, PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/ha-aliases-editor";
import "../../../components/ha-md-list-item";
import "../../../components/ha-switch";
import "../../../components/input/ha-input";
import { updateCloudGoogleEntityConfig } from "../../../data/cloud";
import type { GoogleEntity } from "../../../data/google_assistant";
import { fetchCloudGoogleEntity } from "../../../data/google_assistant";
import { haStyle } from "../../../resources/styles";
import type { HomeAssistant } from "../../../types";
@customElement("google-entity-voice-settings")
export class GoogleEntityVoiceSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@state() private _entity?: GoogleEntity;
protected willUpdate(changedProps: PropertyValues<this>) {
if (changedProps.has("entityId") && this.entityId) {
this._fetchEntity();
}
}
private async _fetchEntity() {
try {
const entity = await fetchCloudGoogleEntity(this.hass, this.entityId);
if (entity.aliases) {
entity.aliases = entity.aliases.filter(Boolean);
}
this._entity = entity;
} catch (_err) {
this._entity = undefined;
}
}
protected render() {
if (!this._entity) {
return nothing;
}
const defaultName = this.hass.states[this.entityId]
? computeStateName(this.hass.states[this.entityId])
: this.entityId;
return html`
<ha-input
.label=${this.hass.localize("ui.dialogs.voice-settings.name")}
.hint=${this.hass.localize(
"ui.dialogs.voice-settings.name_description"
)}
with-clear
.value=${this._entity.name ?? ""}
.placeholder=${defaultName}
@change=${this._nameChanged}
></ha-input>
<h4 class="header">
${this.hass.localize("ui.dialogs.voice-settings.aliases")}
</h4>
<ha-aliases-editor
.aliases=${this._entity.aliases ?? []}
@value-changed=${this._aliasesChanged}
></ha-aliases-editor>
${this._entity.might_2fa
? html`
<wa-divider></wa-divider>
<ha-md-list-item>
<span slot="headline">
${this.hass.localize("ui.dialogs.voice-settings.ask_pin")}
</span>
<ha-switch
slot="end"
.checked=${!this._entity.disable_2fa}
@change=${this._2faChanged}
></ha-switch>
</ha-md-list-item>
`
: nothing}
`;
}
private async _nameChanged(ev) {
if (!this._entity) {
return;
}
const value = ev.target.value?.trim() || null;
if ((this._entity.name ?? null) === value) {
return;
}
const previous = this._entity.name ?? null;
this._entity = { ...this._entity, name: value };
try {
await updateCloudGoogleEntityConfig(this.hass, this.entityId, {
name: value,
});
} catch (_err) {
this._entity = { ...this._entity, name: previous };
}
}
private async _aliasesChanged(ev) {
if (!this._entity) {
return;
}
const aliases = ev.detail.value as string[];
const previous = this._entity.aliases ?? null;
this._entity = { ...this._entity, aliases };
const stringAliases = aliases
.map((alias) => alias.trim())
.filter((alias) => alias);
try {
await updateCloudGoogleEntityConfig(this.hass, this.entityId, {
aliases: stringAliases,
});
} catch (_err) {
this._entity = { ...this._entity, aliases: previous };
}
}
private async _2faChanged(ev) {
if (!this._entity) {
return;
}
const disable_2fa = !ev.target.checked;
this._entity = { ...this._entity, disable_2fa };
try {
await updateCloudGoogleEntityConfig(this.hass, this.entityId, {
disable_2fa,
});
} catch (_err) {
this._entity = { ...this._entity, disable_2fa: !disable_2fa };
}
}
static get styles(): CSSResultGroup {
return [
haStyle,
css`
:host {
display: block;
margin: 0 var(--ha-space-8) var(--ha-space-8);
}
ha-input {
display: block;
width: 100%;
}
ha-aliases-editor {
display: block;
}
.header {
margin-top: var(--ha-space-2);
margin-bottom: var(--ha-space-1);
}
ha-md-list-item {
--md-list-item-leading-space: 0;
--md-list-item-trailing-space: 0;
--md-item-overflow: visible;
}
wa-divider {
margin: var(--ha-space-2) 0;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"google-entity-voice-settings": GoogleEntityVoiceSettings;
}
}
@@ -104,8 +104,6 @@ export class VoiceAssistantsExpose extends LitElement {
string[] | undefined
>;
@state() private _googleAliases?: Record<string, string[]>;
@storage({
key: "voice-expose-table-sort",
state: false,
@@ -160,8 +158,7 @@ export class VoiceAssistantsExpose extends LitElement {
| undefined,
_language: string,
localize: LocalizeFunc,
entitiesToCheck?: any[],
googleAliases?: Record<string, string[]>
entitiesToCheck?: any[]
): DataTableColumnContainer => ({
icon: {
title: "",
@@ -202,15 +199,9 @@ export class VoiceAssistantsExpose extends LitElement {
sortable: true,
filterable: true,
template: (entry) => {
const registryAliases = entry.aliases.filter(
const aliases = entry.aliases.filter(
(a: string | null) => a !== null
);
const aliases = [
...new Set([
...registryAliases,
...(googleAliases?.[entry.entity_id] ?? []),
]),
];
return aliases.length === 0
? "-"
: aliases.length === 1
@@ -466,14 +457,6 @@ export class VoiceAssistantsExpose extends LitElement {
// TODO add supported entity for assist
conversation: undefined,
};
this._googleAliases = googleEntities
? Object.fromEntries(
googleEntities.map((entity) => [
entity.entity_id,
(entity.aliases ?? []).filter(Boolean),
])
)
: undefined;
}
public willUpdate(changedProperties: PropertyValues): void {
@@ -520,8 +503,7 @@ export class VoiceAssistantsExpose extends LitElement {
this._supportedEntities,
this.hass.language,
this.hass.localize,
filteredEntities,
this._googleAliases
filteredEntities
)}
.data=${filteredEntities}
.searchLabel=${this.hass.localize(
@@ -726,9 +708,6 @@ export class VoiceAssistantsExpose extends LitElement {
exposedEntitiesChanged: () => {
fireEvent(this, "exposed-entities-changed");
},
entityEntryUpdated: (entry) => {
this._extEntities = { ...this._extEntities, [entityId]: entry };
},
});
}
@@ -7,7 +7,6 @@ export interface VoiceSettingsDialogParams {
exposed: ExposeEntitySettings;
extEntityReg?: ExtEntityRegistryEntry;
exposedEntitiesChanged?: () => void;
entityEntryUpdated?: (entry: ExtEntityRegistryEntry) => void;
}
export const loadVoiceSettingsDialog = () => import("./dialog-voice-settings");
@@ -1,47 +0,0 @@
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../types";
import "./alexa-entity-voice-settings";
import "./assist-entity-voice-settings";
import "./google-entity-voice-settings";
@customElement("voice-assistant-settings")
export class VoiceAssistantSettings extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public entityId!: string;
@property({ attribute: false }) public assistant!: string;
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
protected render() {
switch (this.assistant) {
case "cloud.google_assistant":
return html`<google-entity-voice-settings
.hass=${this.hass}
.entityId=${this.entityId}
></google-entity-voice-settings>`;
case "cloud.alexa":
return html`<alexa-entity-voice-settings
.hass=${this.hass}
.entityId=${this.entityId}
></alexa-entity-voice-settings>`;
case "conversation":
return html`<assist-entity-voice-settings
.hass=${this.hass}
.entityId=${this.entityId}
.entry=${this.entry}
></assist-entity-voice-settings>`;
default:
return nothing;
}
}
}
declare global {
interface HTMLElementTagNameMap {
"voice-assistant-settings": VoiceAssistantSettings;
}
}
+5 -6
View File
@@ -29,7 +29,7 @@ import type {
LogbookCauseType,
LogbookGlyph,
LogbookItem,
LogbookNameDetail,
LogbookScope,
LogbookValue,
} from "./logbook-entry-model";
import {
@@ -63,8 +63,7 @@ class HaLogbookEntry extends LitElement {
@property({ type: Boolean, attribute: false }) public graphColor = false;
@property({ type: String, attribute: "name-detail" })
public nameDetail?: LogbookNameDetail;
@property({ attribute: false }) public scope?: LogbookScope;
@property({ type: Boolean, attribute: false }) public firstOfDay = false;
@@ -84,7 +83,7 @@ class HaLogbookEntry extends LitElement {
const seenEntityIds: string[] = [];
const item = computeLogbookItem(this.hass, entry, {
nameDetail: this.nameDetail,
scope: this.scope,
userIdToName: this.userIdToName,
});
@@ -99,7 +98,7 @@ class HaLogbookEntry extends LitElement {
? `/config/${traceContext.domain}/trace/${traceContext.item_id}?run_id=${traceContext.run_id}`
: undefined;
const hideName = this.nameDetail === "none";
const hideName = this.scope === "entity";
const layout: EntryLayout =
!this.narrow && !this.noIcon ? "timeline" : hideName ? "inline" : "list";
const node = layout === "timeline" ? "icon" : "dot";
@@ -225,7 +224,7 @@ class HaLogbookEntry extends LitElement {
}
private _renderTimeline(ctx: LogbookRenderItem) {
const hideName = this.nameDetail === "none";
const hideName = this.scope === "entity";
const rtl = computeRTL(
this.hass.language,
this.hass.translationMetadata.translations
+3 -4
View File
@@ -13,7 +13,7 @@ import { haStyle, haStyleScrollbar } from "../../resources/styles";
import { loadVirtualizer } from "../../resources/virtualizer";
import type { HomeAssistant } from "../../types";
import "./ha-logbook-entry";
import type { LogbookNameDetail } from "./logbook-entry-model";
import type { LogbookScope } from "./logbook-entry-model";
import { sameDay } from "./logbook-entry-model";
declare global {
@@ -47,8 +47,7 @@ class HaLogbookRenderer extends LitElement {
@property({ type: Boolean, attribute: "show-cause" }) public showCause =
false;
@property({ type: String, attribute: "name-detail" })
public nameDetail?: LogbookNameDetail;
@property({ attribute: false }) public scope?: LogbookScope;
// @ts-ignore
@restoreScroll(".container") private _savedScrollPos?: number;
@@ -138,7 +137,7 @@ class HaLogbookRenderer extends LitElement {
.narrow=${this.narrow}
.noIcon=${this.noIcon}
.graphColor=${this.graphColor}
.nameDetail=${this.nameDetail}
.scope=${this.scope}
.firstOfDay=${firstOfDay}
.lastOfDay=${lastOfDay}
.showRelative=${this._showRelative}
+5 -6
View File
@@ -13,7 +13,7 @@ import { loadTraceContexts } from "../../data/trace";
import { fetchUsers } from "../../data/user";
import type { HomeAssistant } from "../../types";
import "./ha-logbook-renderer";
import type { LogbookNameDetail } from "./logbook-entry-model";
import type { LogbookScope } from "./logbook-entry-model";
interface LogbookTimePeriod {
now: Date;
@@ -67,10 +67,9 @@ export class HaLogbook extends LitElement {
@property({ type: Boolean, attribute: "show-cause" }) public showCause =
false;
// How much naming detail an entity row shows; `none` also hides the name when
// the surface already implies the subject.
@property({ type: String, attribute: "name-detail" })
public nameDetail?: LogbookNameDetail;
// Surface scope: removes the context (and, for "entity", the subject name)
// the surface already implies.
@property({ attribute: false }) public scope?: LogbookScope;
@property({ attribute: "show-more-link", type: Boolean })
public showMoreLink = true;
@@ -131,7 +130,7 @@ export class HaLogbook extends LitElement {
.noIcon=${this.noIcon}
.graphColor=${this.graphColor}
.showCause=${this.showCause}
.nameDetail=${this.nameDetail}
.scope=${this.scope}
.entries=${this._logbookEntries}
.traceContexts=${this._traceContexts}
.userIdToName=${this._userIdToName}
+8 -11
View File
@@ -35,10 +35,8 @@ export const classifyLogbookEntry = (
return "integration";
};
// How much naming detail an entity row shows, from least to most. The value is
// the broadest part shown: `none` (name hidden), `entity`, `device` (device ▸
// entity), `area` (area ▸ device ▸ entity).
export type LogbookNameDetail = "none" | "entity" | "device" | "area";
// A device lives in exactly one area, so `device` (and `entity`) imply it too.
export type LogbookScope = "entity" | "device" | "area";
export interface EntityDisplay {
primary?: string;
@@ -48,7 +46,7 @@ export interface EntityDisplay {
export const entityDisplay = (
hass: HomeAssistant,
entityId: string,
nameDetail?: LogbookNameDetail
scope?: LogbookScope
): EntityDisplay => {
const stateObj = hass.states[entityId] as HassEntity | undefined;
if (!stateObj) {
@@ -71,15 +69,14 @@ export const entityDisplay = (
const deviceQualifier = entityName ? deviceName : undefined;
let parts: (string | undefined)[];
switch (nameDetail) {
case "none":
switch (scope) {
case "entity":
case "device":
parts = [];
break;
case "device":
case "area":
parts = [deviceQualifier];
break;
case "area":
default:
parts = [areaName, deviceQualifier];
}
@@ -310,7 +307,7 @@ export interface LogbookItem {
}
export interface BuildLogbookItemOptions {
nameDetail?: LogbookNameDetail;
scope?: LogbookScope;
userIdToName?: Record<string, string>;
}
@@ -331,7 +328,7 @@ export const computeLogbookItem = (
: undefined;
const display = entry.entity_id
? entityDisplay(hass, entry.entity_id, opts.nameDetail)
? entityDisplay(hass, entry.entity_id, opts.scope)
: undefined;
return {
@@ -9,7 +9,6 @@ import memoizeOne from "memoize-one";
import { ensureArray } from "../../../common/array/ensure-array";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { getEntityEntryContext } from "../../../common/entity/context/get_entity_context";
import { navigate } from "../../../common/navigate";
import { createSearchParam } from "../../../common/url/search-params";
import "../../../components/ha-card";
@@ -18,7 +17,6 @@ import { resolveEntityIDs } from "../../../data/selector";
import type { HomeAssistant } from "../../../types";
import "../../logbook/ha-logbook";
import type { HaLogbook } from "../../logbook/ha-logbook";
import type { LogbookNameDetail } from "../../logbook/logbook-entry-model";
import { findEntities } from "../common/find-entities";
import { processConfigEntities } from "../common/process-config-entities";
import "../components/hui-warning";
@@ -193,60 +191,6 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
resolveEntityIDs(this.hass, targetPickerValue, entities, devices, areas)
);
private _getNameDetail(): LogbookNameDetail | undefined {
const nameDetail = this._config?.name_detail ?? "auto";
if (nameDetail !== "auto") {
return nameDetail;
}
const entityIds = this._getEntityIds();
if (!entityIds) {
return undefined;
}
return this._getAutoNameDetail(
entityIds,
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
}
// Pick the least detail the targeted entities need to stay unambiguous: a
// single entity needs no name, a shared device needs only the entity name, a
// shared area needs the device, otherwise show the full context.
private _getAutoNameDetail = memoizeOne(
(
entityIds: string[],
entities: HomeAssistant["entities"],
devices: HomeAssistant["devices"],
areas: HomeAssistant["areas"],
floors: HomeAssistant["floors"]
): LogbookNameDetail => {
if (entityIds.length <= 1) {
return "none";
}
const deviceIds = new Set<string | undefined>();
const areaIds = new Set<string | undefined>();
for (const entityId of entityIds) {
const entry = entities[entityId];
const { device, area } = entry
? getEntityEntryContext(entry, entities, devices, areas, floors)
: { device: null, area: null };
deviceIds.add(device?.id);
areaIds.add(area?.area_id);
}
// An entity without a device or area counts as its own group: it does not
// share the context, so it must not collapse the level.
if (deviceIds.size === 1 && !deviceIds.has(undefined)) {
return "entity";
}
if (areaIds.size === 1 && !areaIds.has(undefined)) {
return "device";
}
return "area";
}
);
protected update(changedProperties: PropertyValues<this>) {
super.update(changedProperties);
if (changedProperties.has("layout")) {
@@ -312,7 +256,6 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
.time=${this._time}
.entityIds=${this._getEntityIds()}
.stateFilter=${this._stateFilter}
.nameDetail=${this._getNameDetail()}
narrow
no-icon
virtualize
-2
View File
@@ -25,7 +25,6 @@ import type {
import type { LegacyStateFilter } from "../common/evaluate-filter";
import type { Condition, LegacyCondition } from "../common/validate-condition";
import type { HuiImage } from "../components/hui-image";
import type { LogbookNameDetail } from "../../logbook/logbook-entry-model";
import type { TimestampRenderingFormat } from "../components/types";
import type { LovelaceElementConfig } from "../elements/types";
import type {
@@ -388,7 +387,6 @@ export interface LogbookCardConfig extends LovelaceCardConfig {
hours_to_show?: number;
theme?: string;
state_filter?: string[];
name_detail?: "auto" | LogbookNameDetail;
}
export interface MapEntityConfig extends EntityConfig {
@@ -13,11 +13,15 @@ import deepClone from "deep-clone-simple";
import type { PropertyValues } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { ensureArray } from "../../../../common/array/ensure-array";
import { ConditionListenersController } from "../../../../common/controllers/condition-listeners-controller";
import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { formatListWithOrs } from "../../../../common/string/format-list";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/automation/ha-automation-row-event-chip";
import "../../../../components/automation/ha-automation-row-live-test";
@@ -41,7 +45,9 @@ import type {
Condition,
LegacyCondition,
NotCondition,
NumericStateCondition,
OrCondition,
StateCondition,
} from "../../common/validate-condition";
import {
checkConditionsMet,
@@ -219,6 +225,84 @@ export class HaCardConditionEditor extends LitElement {
};
}
private _describeCondition(
condition: Condition,
entityId?: string
): string | undefined {
const stateObj = entityId ? this.hass.states[entityId] : undefined;
const entity = stateObj ? computeStateName(stateObj) : entityId;
if (!entity) {
return undefined;
}
if (condition.condition === "state") {
const value = condition.state ?? condition.state_not;
const values = ensureArray(value ?? []).filter((v) => v !== "");
if (!values.length) {
return undefined;
}
const attribute =
condition.attribute && stateObj
? computeAttributeNameDisplay(
this.hass.localize,
stateObj,
this.hass.entities,
condition.attribute
)
: condition.attribute;
const states = formatListWithOrs(
this.hass.locale,
values.map((v) =>
stateObj
? condition.attribute
? this.hass
.formatEntityAttributeValue(stateObj, condition.attribute, v)
.toString()
: this.hass.formatEntityState(stateObj, v)
: v
)
);
const invert = condition.state_not !== undefined;
const variant = invert ? "is_not" : "is";
return this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.condition.state.description.${
attribute ? `${variant}_attribute` : variant
}`,
{ entity, state: states, attribute }
);
}
if (condition.condition === "numeric_state") {
const { above, below } = condition;
if (above === undefined && below === undefined) {
return undefined;
}
const attribute =
condition.attribute && stateObj
? computeAttributeNameDisplay(
this.hass.localize,
stateObj,
this.hass.entities,
condition.attribute
)
: condition.attribute;
const variant =
above !== undefined && below !== undefined
? "above_below"
: above !== undefined
? "above"
: "below";
return this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.condition.numeric_state.description.${
attribute ? `${variant}_attribute` : variant
}`,
{ entity, above, below, attribute }
);
}
return undefined;
}
protected render() {
const condition = this._condition;
@@ -228,6 +312,16 @@ export class HaCardConditionEditor extends LitElement {
isNoEntityCondition(condition.condition, this._noEntity) ||
containsNoEntityCondition(condition, this._noEntity);
const contextEntityId =
condition.condition === "state" || condition.condition === "numeric_state"
? (condition as StateCondition | NumericStateCondition).entity ||
(this._entityContext?.mode === "current"
? this._entityContext.entityId
: undefined)
: undefined;
const description = this._describeCondition(condition, contextEntityId);
return html`
<div class="container">
<ha-expansion-panel left-chevron>
@@ -254,9 +348,11 @@ export class HaCardConditionEditor extends LitElement {
>`
: nothing}
<h3 slot="header">
${this.hass.localize(
${description ||
this.hass.localize(
`ui.panel.lovelace.editor.condition-editor.condition.${condition.condition}.label`
) || condition.condition}
) ||
condition.condition}
</h3>
<ha-automation-row-event-chip
.show=${this._testingResult !== undefined}
@@ -6,7 +6,6 @@ import {
array,
assert,
assign,
enums,
number,
object,
optional,
@@ -27,8 +26,6 @@ import type { LogbookCardConfig } from "../../cards/types";
import type { LovelaceCardEditor } from "../../types";
import { baseLovelaceCardConfig } from "../structs/base-card-struct";
const NAME_DETAILS = ["auto", "none", "entity", "device", "area"] as const;
const cardConfigStruct = assign(
baseLovelaceCardConfig,
object({
@@ -38,10 +35,32 @@ const cardConfigStruct = assign(
theme: optional(string()),
target: optional(targetStruct),
state_filter: optional(array(string())),
name_detail: optional(enums(NAME_DETAILS)),
})
);
const SCHEMA = [
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "theme", selector: { theme: {} } },
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { mode: "box", min: 1 } },
},
],
},
{
name: "state_filter",
context: {
filter_entity: "context_entities",
},
selector: { state: { multiple: true } },
},
] as const;
@customElement("hui-logbook-card-editor")
export class HuiLogbookCardEditor
extends LitElement
@@ -51,45 +70,6 @@ export class HuiLogbookCardEditor
@state() private _config?: LogbookCardConfig;
private _schema = memoizeOne(
(localize: HomeAssistant["localize"]) =>
[
{ name: "title", selector: { text: {} } },
{
name: "",
type: "grid",
schema: [
{ name: "theme", selector: { theme: {} } },
{
name: "hours_to_show",
default: DEFAULT_HOURS_TO_SHOW,
selector: { number: { mode: "box", min: 1 } },
},
],
},
{
name: "name_detail",
required: true,
selector: {
select: {
mode: "dropdown",
options: NAME_DETAILS.map((value) => ({
value,
label: localize(
`ui.panel.lovelace.editor.card.logbook.name_detail_options.${value}`
),
})),
},
},
},
{
name: "state_filter",
context: { filter_entity: "context_entities" },
selector: { state: { multiple: true } },
},
] as const
);
public setConfig(config: LogbookCardConfig): void {
assert(config, cardConfigStruct);
this._config = config;
@@ -126,7 +106,7 @@ export class HuiLogbookCardEditor
this.hass.devices,
this.hass.areas
)}
.schema=${this._schema(this.hass.localize)}
.schema=${SCHEMA}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
@@ -149,7 +129,6 @@ export class HuiLogbookCardEditor
areas: HomeAssistant["areas"]
) => ({
...config,
name_detail: config.name_detail ?? "auto",
context_entities: resolveEntityIDs(
this.hass!,
target,
@@ -174,9 +153,7 @@ export class HuiLogbookCardEditor
fireEvent(this, "config-changed", { config: newConfig });
}
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
) => {
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
switch (schema.name) {
case "theme":
return `${this.hass!.localize(
@@ -188,10 +165,6 @@ export class HuiLogbookCardEditor
return this.hass!.localize(
"ui.panel.lovelace.editor.card.logbook.state_filter"
);
case "name_detail":
return this.hass!.localize(
"ui.panel.lovelace.editor.card.logbook.name_detail"
);
default:
return this.hass!.localize(
`ui.panel.lovelace.editor.card.generic.${schema.name}`
+21 -17
View File
@@ -1962,7 +1962,7 @@
"stream_orientation_8": "Rotate right"
},
"voice_assistants": "[%key:ui::panel::config::dashboard::voice_assistants::main%]",
"voice_assistants_description": "Configure aliases and expose settings for voice assistants"
"no_aliases": "Configure aliases and expose settings for voice assistants"
}
},
"recreate_entity_ids": {
@@ -1999,11 +1999,9 @@
"required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]"
},
"voice-settings": {
"edit_settings": "Edit {assistant} settings",
"name": "Name",
"name_description": "Leave empty to use the entity's default name.",
"aliases": "Aliases",
"aliases_count": "{count} {count, plural,\n one {alias}\n other {aliases}\n}",
"expose_header": "Expose",
"aliases_header": "Aliases",
"aliases_description": "Aliases are alternative names to call your entity. Only supported by Assist and Google Assistant.",
"aliases_no_unique_id": "Aliases are not supported for entities without a unique ID. See the {faq_link} for more detail.",
"entity_name_alias_description": "Default name. Disable it if you want your voice assistants to ignore it and just use aliases.",
"ask_pin": "Ask for PIN",
@@ -9272,7 +9270,15 @@
"label": "Entity numeric state",
"attribute": "[%key:ui::panel::lovelace::editor::condition-editor::condition::state::attribute%]",
"above": "Above",
"below": "Below"
"below": "Below",
"description": {
"above": "{entity} is above {above}",
"below": "{entity} is below {below}",
"above_below": "{entity} is above {above} and below {below}",
"above_attribute": "{entity} {attribute} is above {above}",
"below_attribute": "{entity} {attribute} is below {below}",
"above_below_attribute": "{entity} {attribute} is above {above} and below {below}"
}
},
"screen": {
"label": "Screen",
@@ -9290,7 +9296,13 @@
"attribute": "Attribute (optional)",
"current_entity": "Current entity",
"state_equal": "State is equal to",
"state_not_equal": "State is not equal to"
"state_not_equal": "State is not equal to",
"description": {
"is": "{entity} is {state}",
"is_not": "{entity} is not {state}",
"is_attribute": "{entity} {attribute} is {state}",
"is_not_attribute": "{entity} {attribute} is not {state}"
}
},
"time": {
"label": "Time",
@@ -9462,15 +9474,7 @@
"logbook": {
"name": "Activity",
"description": "This card shows a list of events for entities.",
"state_filter": "State filter",
"name_detail": "Name detail",
"name_detail_options": {
"auto": "Automatic",
"none": "None",
"entity": "Entity",
"device": "Device ▸ Entity",
"area": "Area ▸ Device ▸ Entity"
}
"state_filter": "State filter"
},
"history-graph": {
"name": "History graph",
@@ -97,36 +97,29 @@ describe("entityDisplay", () => {
areas: { area_1: mockArea({ area_id: "area_1", name: "Allée" }) },
});
it("shows 'Area ▸ Device' with no name detail (defaults to full)", () => {
it("shows 'Area ▸ Device' with no scope", () => {
expect(entityDisplay(hass, "sensor.allee_battery")).toEqual({
primary: "Battery state",
secondary: "Allée ▸ Caméra Allée",
});
});
it("shows 'Area ▸ Device' for the 'area' name detail", () => {
it("shows device only in an area-scoped logbook", () => {
expect(entityDisplay(hass, "sensor.allee_battery", "area")).toEqual({
primary: "Battery state",
secondary: "Allée ▸ Caméra Allée",
});
});
it("shows the device only for the 'device' name detail", () => {
expect(entityDisplay(hass, "sensor.allee_battery", "device")).toEqual({
primary: "Battery state",
secondary: "Caméra Allée",
});
});
it("shows no context for the 'entity' name detail", () => {
expect(entityDisplay(hass, "sensor.allee_battery", "entity")).toEqual({
it("shows no context in a device-scoped logbook", () => {
expect(entityDisplay(hass, "sensor.allee_battery", "device")).toEqual({
primary: "Battery state",
secondary: undefined,
});
});
it("shows no context for the 'none' name detail", () => {
expect(entityDisplay(hass, "sensor.allee_battery", "none")).toEqual({
it("shows no context in an entity-scoped logbook", () => {
expect(entityDisplay(hass, "sensor.allee_battery", "entity")).toEqual({
primary: "Battery state",
secondary: undefined,
});