mirror of
https://github.com/home-assistant/frontend.git
synced 2026-06-24 01:01:32 +00:00
Compare commits
3 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b24eb2c916 | |||
| 071f8e96ae | |||
| 734fed21a8 |
+5
-1
@@ -6,11 +6,15 @@ 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<AlexaEntity>({
|
||||
hass.callWS<AlexaEntityConfig>({
|
||||
type: "cloud/alexa/entities/get",
|
||||
entity_id,
|
||||
});
|
||||
|
||||
+13
-2
@@ -172,12 +172,23 @@ export const removeCloudData = (hass: HomeAssistant) =>
|
||||
export const updateCloudGoogleEntityConfig = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
disable_2fa: boolean
|
||||
values: { disable_2fa?: boolean; name?: string | null; aliases?: string[] }
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "cloud/google_assistant/entities/update",
|
||||
entity_id,
|
||||
disable_2fa,
|
||||
...values,
|
||||
});
|
||||
|
||||
export const updateCloudAlexaEntityConfig = (
|
||||
hass: HomeAssistant,
|
||||
entity_id: string,
|
||||
name: string | null
|
||||
) =>
|
||||
hass.callWS({
|
||||
type: "cloud/alexa/entities/update",
|
||||
entity_id,
|
||||
name,
|
||||
});
|
||||
|
||||
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
|
||||
|
||||
@@ -5,6 +5,8 @@ export interface GoogleEntity {
|
||||
traits: string[];
|
||||
might_2fa: boolean;
|
||||
disable_2fa?: boolean;
|
||||
name?: string | null;
|
||||
aliases?: string[] | null;
|
||||
}
|
||||
|
||||
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
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,6 +7,7 @@ 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 {
|
||||
@@ -33,9 +34,19 @@ 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`
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
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)}
|
||||
.scope=${"entity"}
|
||||
name-detail="none"
|
||||
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)}
|
||||
.scope=${"area"}
|
||||
name-detail="device"
|
||||
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)}
|
||||
.scope=${"device"}
|
||||
name-detail="entity"
|
||||
virtualize
|
||||
narrow
|
||||
no-icon
|
||||
|
||||
@@ -1011,13 +1011,9 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
)}</span
|
||||
>
|
||||
<span slot="secondary">
|
||||
${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"
|
||||
)}
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.entity_registry.editor.voice_assistants_description"
|
||||
)}
|
||||
</span>
|
||||
<ha-icon-next slot="meta"></ha-icon-next>
|
||||
</ha-list-item>
|
||||
|
||||
@@ -0,0 +1,95 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
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 { mdiTuneVertical } from "@mdi/js";
|
||||
import { mdiChevronLeft, mdiTuneVertical } from "@mdi/js";
|
||||
import type { CSSResultGroup } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
@@ -6,10 +6,17 @@ 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 } from "../../../resources/styles";
|
||||
import {
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
} 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")
|
||||
@@ -20,8 +27,14 @@ 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;
|
||||
}
|
||||
|
||||
@@ -31,6 +44,8 @@ class DialogVoiceSettings extends LitElement {
|
||||
|
||||
private _dialogClosed(): void {
|
||||
this._params = undefined;
|
||||
this._exposed = undefined;
|
||||
this._editingAssistant = undefined;
|
||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||
}
|
||||
|
||||
@@ -41,14 +56,23 @@ 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 =
|
||||
computeStateName(this.hass.states[this._params.entityId]) ||
|
||||
this.hass.localize("ui.panel.config.entities.picker.unnamed_entity");
|
||||
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");
|
||||
|
||||
return html`
|
||||
<ha-dialog
|
||||
@@ -56,28 +80,58 @@ class DialogVoiceSettings extends LitElement {
|
||||
header-title=${title}
|
||||
@closed=${this._dialogClosed}
|
||||
>
|
||||
<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>
|
||||
${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-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() {
|
||||
@@ -88,6 +142,7 @@ class DialogVoiceSettings extends LitElement {
|
||||
return [
|
||||
haStyle,
|
||||
haStyleDialog,
|
||||
haStyleDialogFixedTop,
|
||||
css`
|
||||
ha-dialog {
|
||||
--dialog-content-padding: 0;
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { mdiAlertCircle } from "@mdi/js";
|
||||
import { mdiAlertCircle, mdiCog } 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,
|
||||
@@ -14,35 +13,24 @@ import {
|
||||
generateEntityDomainFilter,
|
||||
isEmptyEntityDomainFilter,
|
||||
} from "../../../common/entity/entity_domain_filter";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-aliases-editor";
|
||||
import "../../../components/ha-checkbox";
|
||||
import "../../../components/ha-icon-button";
|
||||
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,
|
||||
updateCloudGoogleEntityConfig,
|
||||
} from "../../../data/cloud";
|
||||
import { fetchCloudStatus } from "../../../data/cloud";
|
||||
import type { ExtEntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
getExtendedEntityRegistryEntry,
|
||||
updateEntityRegistryEntry,
|
||||
} from "../../../data/entity/entity_registry";
|
||||
import { getExtendedEntityRegistryEntry } 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 SubscribeMixin(LitElement) {
|
||||
export class EntityVoiceSettings extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public entityId!: string;
|
||||
@@ -53,8 +41,6 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
@state() private _cloudStatus?: CloudStatus;
|
||||
|
||||
@state() private _aliases?: (string | null)[];
|
||||
|
||||
@state() private _googleEntity?: GoogleEntity;
|
||||
|
||||
@state() private _unsupported: Partial<
|
||||
@@ -77,16 +63,16 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
private async _fetchEntities() {
|
||||
try {
|
||||
const googleEntity = await fetchCloudGoogleEntity(
|
||||
this._googleEntity = await fetchCloudGoogleEntity(
|
||||
this.hass,
|
||||
this.entityId
|
||||
);
|
||||
this._googleEntity = googleEntity;
|
||||
this.requestUpdate("_googleEntity");
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_supported") {
|
||||
this._unsupported["cloud.google_assistant"] = true;
|
||||
this.requestUpdate("_unsupported");
|
||||
this._unsupported = {
|
||||
...this._unsupported,
|
||||
"cloud.google_assistant": true,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -94,8 +80,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
await fetchCloudAlexaEntity(this.hass, this.entityId);
|
||||
} catch (err: any) {
|
||||
if (err.code === "not_supported") {
|
||||
this._unsupported["cloud.alexa"] = true;
|
||||
this.requestUpdate("_unsupported");
|
||||
this._unsupported = { ...this._unsupported, "cloud.alexa": true };
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -127,7 +112,6 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
this._cloudStatus.prefs.alexa_enabled === true;
|
||||
|
||||
const showAssistants = [...Object.keys(voiceAssistants)];
|
||||
const uiAssistants = [...showAssistants];
|
||||
|
||||
const alexaManual =
|
||||
alexaEnabled &&
|
||||
@@ -145,20 +129,12 @@ export class EntityVoiceSettings extends SubscribeMixin(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;
|
||||
@@ -177,216 +153,97 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
const manExposedGoogle =
|
||||
googleManual && manFilterFuncs!.google(this.entityId);
|
||||
|
||||
const anyExposed = uiExposed || manExposedAlexa || manExposedGoogle;
|
||||
|
||||
return html`
|
||||
<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];
|
||||
${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 support2fa =
|
||||
key === "cloud.google_assistant" &&
|
||||
!googleManual &&
|
||||
supported &&
|
||||
this._googleEntity?.might_2fa;
|
||||
const hasSettings = supported && !manualConfig;
|
||||
|
||||
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>
|
||||
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">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice-settings.unsupported"
|
||||
"ui.dialogs.voice-settings.manual_config"
|
||||
)}
|
||||
</div>
|
||||
`
|
||||
: aliasCount
|
||||
? html`<div slot="supporting-text">
|
||||
${this.hass.localize(
|
||||
"ui.dialogs.voice-settings.aliases_count",
|
||||
{ count: aliasCount }
|
||||
)}
|
||||
</div>`
|
||||
: 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>
|
||||
<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}
|
||||
<ha-switch
|
||||
slot="end"
|
||||
.checked=${(this._aliases ?? this.entry.aliases).includes(null)}
|
||||
@change=${this._toggleEntityNameAlias}
|
||||
.assistant=${key}
|
||||
@change=${this._toggleAssistant}
|
||||
.disabled=${manualConfig || (!exposed && !supported)}
|
||||
.checked=${exposed}
|
||||
></ha-switch>
|
||||
</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>
|
||||
`}
|
||||
</div>
|
||||
</ha-md-list-item>
|
||||
`;
|
||||
})}
|
||||
`;
|
||||
}
|
||||
|
||||
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 _editAssistant(ev) {
|
||||
fireEvent(this, "edit-assistant", { assistant: ev.target.assistant });
|
||||
}
|
||||
|
||||
private async _toggleAssistant(ev) {
|
||||
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");
|
||||
}
|
||||
ev.stopPropagation();
|
||||
const assistant: string = ev.target.assistant;
|
||||
const checked: boolean = ev.target.checked;
|
||||
|
||||
private async _toggleAll(ev) {
|
||||
const expose = ev.target.checked;
|
||||
exposeEntities(this.hass, [assistant], [this.entityId], checked);
|
||||
fireEvent(this, "exposed-changed", {
|
||||
value: { ...this.exposed, [assistant]: 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,
|
||||
@@ -403,7 +260,7 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
css`
|
||||
:host {
|
||||
display: block;
|
||||
margin: 32px;
|
||||
margin: var(--ha-space-8);
|
||||
margin-top: 0;
|
||||
}
|
||||
ha-md-list-item {
|
||||
@@ -411,19 +268,10 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
--md-list-item-trailing-space: 0;
|
||||
--md-item-overflow: visible;
|
||||
}
|
||||
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;
|
||||
.trailing {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: var(--ha-space-2);
|
||||
}
|
||||
.unsupported {
|
||||
display: flex;
|
||||
@@ -432,21 +280,10 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
.unsupported ha-svg-icon {
|
||||
color: var(--error-color);
|
||||
--mdc-icon-size: 16px;
|
||||
margin-right: 4px;
|
||||
margin-inline-end: 4px;
|
||||
margin-right: var(--ha-space-1);
|
||||
margin-inline-end: var(--ha-space-1);
|
||||
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;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
@@ -454,15 +291,11 @@ export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"entity-registry-settings": EntityRegistrySettings;
|
||||
"entity-voice-settings": EntityVoiceSettings;
|
||||
}
|
||||
interface HASSDomEvents {
|
||||
"entity-entry-updated": ExtEntityRegistryEntry;
|
||||
}
|
||||
}
|
||||
|
||||
declare global {
|
||||
interface HTMLElementTagNameMap {
|
||||
"entity-voice-settings": EntityVoiceSettings;
|
||||
"edit-assistant": { assistant: string };
|
||||
"exposed-changed": { value: ExposeEntitySettings };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,176 @@
|
||||
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,6 +104,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
string[] | undefined
|
||||
>;
|
||||
|
||||
@state() private _googleAliases?: Record<string, string[]>;
|
||||
|
||||
@storage({
|
||||
key: "voice-expose-table-sort",
|
||||
state: false,
|
||||
@@ -158,7 +160,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
| undefined,
|
||||
_language: string,
|
||||
localize: LocalizeFunc,
|
||||
entitiesToCheck?: any[]
|
||||
entitiesToCheck?: any[],
|
||||
googleAliases?: Record<string, string[]>
|
||||
): DataTableColumnContainer => ({
|
||||
icon: {
|
||||
title: "",
|
||||
@@ -199,9 +202,15 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
sortable: true,
|
||||
filterable: true,
|
||||
template: (entry) => {
|
||||
const aliases = entry.aliases.filter(
|
||||
const registryAliases = entry.aliases.filter(
|
||||
(a: string | null) => a !== null
|
||||
);
|
||||
const aliases = [
|
||||
...new Set([
|
||||
...registryAliases,
|
||||
...(googleAliases?.[entry.entity_id] ?? []),
|
||||
]),
|
||||
];
|
||||
return aliases.length === 0
|
||||
? "-"
|
||||
: aliases.length === 1
|
||||
@@ -457,6 +466,14 @@ 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 {
|
||||
@@ -503,7 +520,8 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
this._supportedEntities,
|
||||
this.hass.language,
|
||||
this.hass.localize,
|
||||
filteredEntities
|
||||
filteredEntities,
|
||||
this._googleAliases
|
||||
)}
|
||||
.data=${filteredEntities}
|
||||
.searchLabel=${this.hass.localize(
|
||||
@@ -708,6 +726,9 @@ export class VoiceAssistantsExpose extends LitElement {
|
||||
exposedEntitiesChanged: () => {
|
||||
fireEvent(this, "exposed-entities-changed");
|
||||
},
|
||||
entityEntryUpdated: (entry) => {
|
||||
this._extEntities = { ...this._extEntities, [entityId]: entry };
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ export interface VoiceSettingsDialogParams {
|
||||
exposed: ExposeEntitySettings;
|
||||
extEntityReg?: ExtEntityRegistryEntry;
|
||||
exposedEntitiesChanged?: () => void;
|
||||
entityEntryUpdated?: (entry: ExtEntityRegistryEntry) => void;
|
||||
}
|
||||
|
||||
export const loadVoiceSettingsDialog = () => import("./dialog-voice-settings");
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -29,7 +29,7 @@ import type {
|
||||
LogbookCauseType,
|
||||
LogbookGlyph,
|
||||
LogbookItem,
|
||||
LogbookScope,
|
||||
LogbookNameDetail,
|
||||
LogbookValue,
|
||||
} from "./logbook-entry-model";
|
||||
import {
|
||||
@@ -63,7 +63,8 @@ class HaLogbookEntry extends LitElement {
|
||||
|
||||
@property({ type: Boolean, attribute: false }) public graphColor = false;
|
||||
|
||||
@property({ attribute: false }) public scope?: LogbookScope;
|
||||
@property({ type: String, attribute: "name-detail" })
|
||||
public nameDetail?: LogbookNameDetail;
|
||||
|
||||
@property({ type: Boolean, attribute: false }) public firstOfDay = false;
|
||||
|
||||
@@ -83,7 +84,7 @@ class HaLogbookEntry extends LitElement {
|
||||
const seenEntityIds: string[] = [];
|
||||
|
||||
const item = computeLogbookItem(this.hass, entry, {
|
||||
scope: this.scope,
|
||||
nameDetail: this.nameDetail,
|
||||
userIdToName: this.userIdToName,
|
||||
});
|
||||
|
||||
@@ -98,7 +99,7 @@ class HaLogbookEntry extends LitElement {
|
||||
? `/config/${traceContext.domain}/trace/${traceContext.item_id}?run_id=${traceContext.run_id}`
|
||||
: undefined;
|
||||
|
||||
const hideName = this.scope === "entity";
|
||||
const hideName = this.nameDetail === "none";
|
||||
const layout: EntryLayout =
|
||||
!this.narrow && !this.noIcon ? "timeline" : hideName ? "inline" : "list";
|
||||
const node = layout === "timeline" ? "icon" : "dot";
|
||||
@@ -224,7 +225,7 @@ class HaLogbookEntry extends LitElement {
|
||||
}
|
||||
|
||||
private _renderTimeline(ctx: LogbookRenderItem) {
|
||||
const hideName = this.scope === "entity";
|
||||
const hideName = this.nameDetail === "none";
|
||||
const rtl = computeRTL(
|
||||
this.hass.language,
|
||||
this.hass.translationMetadata.translations
|
||||
|
||||
@@ -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 { LogbookScope } from "./logbook-entry-model";
|
||||
import type { LogbookNameDetail } from "./logbook-entry-model";
|
||||
import { sameDay } from "./logbook-entry-model";
|
||||
|
||||
declare global {
|
||||
@@ -47,7 +47,8 @@ class HaLogbookRenderer extends LitElement {
|
||||
@property({ type: Boolean, attribute: "show-cause" }) public showCause =
|
||||
false;
|
||||
|
||||
@property({ attribute: false }) public scope?: LogbookScope;
|
||||
@property({ type: String, attribute: "name-detail" })
|
||||
public nameDetail?: LogbookNameDetail;
|
||||
|
||||
// @ts-ignore
|
||||
@restoreScroll(".container") private _savedScrollPos?: number;
|
||||
@@ -137,7 +138,7 @@ class HaLogbookRenderer extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.noIcon=${this.noIcon}
|
||||
.graphColor=${this.graphColor}
|
||||
.scope=${this.scope}
|
||||
.nameDetail=${this.nameDetail}
|
||||
.firstOfDay=${firstOfDay}
|
||||
.lastOfDay=${lastOfDay}
|
||||
.showRelative=${this._showRelative}
|
||||
|
||||
@@ -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 { LogbookScope } from "./logbook-entry-model";
|
||||
import type { LogbookNameDetail } from "./logbook-entry-model";
|
||||
|
||||
interface LogbookTimePeriod {
|
||||
now: Date;
|
||||
@@ -67,9 +67,10 @@ export class HaLogbook extends LitElement {
|
||||
@property({ type: Boolean, attribute: "show-cause" }) public showCause =
|
||||
false;
|
||||
|
||||
// Surface scope: removes the context (and, for "entity", the subject name)
|
||||
// the surface already implies.
|
||||
@property({ attribute: false }) public scope?: LogbookScope;
|
||||
// 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;
|
||||
|
||||
@property({ attribute: "show-more-link", type: Boolean })
|
||||
public showMoreLink = true;
|
||||
@@ -130,7 +131,7 @@ export class HaLogbook extends LitElement {
|
||||
.noIcon=${this.noIcon}
|
||||
.graphColor=${this.graphColor}
|
||||
.showCause=${this.showCause}
|
||||
.scope=${this.scope}
|
||||
.nameDetail=${this.nameDetail}
|
||||
.entries=${this._logbookEntries}
|
||||
.traceContexts=${this._traceContexts}
|
||||
.userIdToName=${this._userIdToName}
|
||||
|
||||
@@ -35,8 +35,10 @@ export const classifyLogbookEntry = (
|
||||
return "integration";
|
||||
};
|
||||
|
||||
// A device lives in exactly one area, so `device` (and `entity`) imply it too.
|
||||
export type LogbookScope = "entity" | "device" | "area";
|
||||
// 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";
|
||||
|
||||
export interface EntityDisplay {
|
||||
primary?: string;
|
||||
@@ -46,7 +48,7 @@ export interface EntityDisplay {
|
||||
export const entityDisplay = (
|
||||
hass: HomeAssistant,
|
||||
entityId: string,
|
||||
scope?: LogbookScope
|
||||
nameDetail?: LogbookNameDetail
|
||||
): EntityDisplay => {
|
||||
const stateObj = hass.states[entityId] as HassEntity | undefined;
|
||||
if (!stateObj) {
|
||||
@@ -69,14 +71,15 @@ export const entityDisplay = (
|
||||
const deviceQualifier = entityName ? deviceName : undefined;
|
||||
|
||||
let parts: (string | undefined)[];
|
||||
switch (scope) {
|
||||
switch (nameDetail) {
|
||||
case "none":
|
||||
case "entity":
|
||||
case "device":
|
||||
parts = [];
|
||||
break;
|
||||
case "area":
|
||||
case "device":
|
||||
parts = [deviceQualifier];
|
||||
break;
|
||||
case "area":
|
||||
default:
|
||||
parts = [areaName, deviceQualifier];
|
||||
}
|
||||
@@ -307,7 +310,7 @@ export interface LogbookItem {
|
||||
}
|
||||
|
||||
export interface BuildLogbookItemOptions {
|
||||
scope?: LogbookScope;
|
||||
nameDetail?: LogbookNameDetail;
|
||||
userIdToName?: Record<string, string>;
|
||||
}
|
||||
|
||||
@@ -328,7 +331,7 @@ export const computeLogbookItem = (
|
||||
: undefined;
|
||||
|
||||
const display = entry.entity_id
|
||||
? entityDisplay(hass, entry.entity_id, opts.scope)
|
||||
? entityDisplay(hass, entry.entity_id, opts.nameDetail)
|
||||
: undefined;
|
||||
|
||||
return {
|
||||
|
||||
@@ -9,6 +9,7 @@ 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";
|
||||
@@ -17,6 +18,7 @@ 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";
|
||||
@@ -191,6 +193,60 @@ 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")) {
|
||||
@@ -256,6 +312,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard {
|
||||
.time=${this._time}
|
||||
.entityIds=${this._getEntityIds()}
|
||||
.stateFilter=${this._stateFilter}
|
||||
.nameDetail=${this._getNameDetail()}
|
||||
narrow
|
||||
no-icon
|
||||
virtualize
|
||||
|
||||
@@ -25,6 +25,7 @@ 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 {
|
||||
@@ -387,6 +388,7 @@ export interface LogbookCardConfig extends LovelaceCardConfig {
|
||||
hours_to_show?: number;
|
||||
theme?: string;
|
||||
state_filter?: string[];
|
||||
name_detail?: "auto" | LogbookNameDetail;
|
||||
}
|
||||
|
||||
export interface MapEntityConfig extends EntityConfig {
|
||||
|
||||
@@ -6,6 +6,7 @@ import {
|
||||
array,
|
||||
assert,
|
||||
assign,
|
||||
enums,
|
||||
number,
|
||||
object,
|
||||
optional,
|
||||
@@ -26,6 +27,8 @@ 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({
|
||||
@@ -35,32 +38,10 @@ 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
|
||||
@@ -70,6 +51,45 @@ 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;
|
||||
@@ -106,7 +126,7 @@ export class HuiLogbookCardEditor
|
||||
this.hass.devices,
|
||||
this.hass.areas
|
||||
)}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${this._schema(this.hass.localize)}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
@@ -129,6 +149,7 @@ export class HuiLogbookCardEditor
|
||||
areas: HomeAssistant["areas"]
|
||||
) => ({
|
||||
...config,
|
||||
name_detail: config.name_detail ?? "auto",
|
||||
context_entities: resolveEntityIDs(
|
||||
this.hass!,
|
||||
target,
|
||||
@@ -153,7 +174,9 @@ export class HuiLogbookCardEditor
|
||||
fireEvent(this, "config-changed", { config: newConfig });
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => {
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) => {
|
||||
switch (schema.name) {
|
||||
case "theme":
|
||||
return `${this.hass!.localize(
|
||||
@@ -165,6 +188,10 @@ 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}`
|
||||
|
||||
@@ -1962,7 +1962,7 @@
|
||||
"stream_orientation_8": "Rotate right"
|
||||
},
|
||||
"voice_assistants": "[%key:ui::panel::config::dashboard::voice_assistants::main%]",
|
||||
"no_aliases": "Configure aliases and expose settings for voice assistants"
|
||||
"voice_assistants_description": "Configure aliases and expose settings for voice assistants"
|
||||
}
|
||||
},
|
||||
"recreate_entity_ids": {
|
||||
@@ -1999,9 +1999,11 @@
|
||||
"required_error_msg": "[%key:ui::panel::config::zone::detail::required_error_msg%]"
|
||||
},
|
||||
"voice-settings": {
|
||||
"expose_header": "Expose",
|
||||
"aliases_header": "Aliases",
|
||||
"aliases_description": "Aliases are alternative names to call your entity. Only supported by Assist and Google Assistant.",
|
||||
"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}",
|
||||
"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",
|
||||
@@ -9460,7 +9462,15 @@
|
||||
"logbook": {
|
||||
"name": "Activity",
|
||||
"description": "This card shows a list of events for entities.",
|
||||
"state_filter": "State filter"
|
||||
"state_filter": "State filter",
|
||||
"name_detail": "Name detail",
|
||||
"name_detail_options": {
|
||||
"auto": "Automatic",
|
||||
"none": "None",
|
||||
"entity": "Entity",
|
||||
"device": "Device ▸ Entity",
|
||||
"area": "Area ▸ Device ▸ Entity"
|
||||
}
|
||||
},
|
||||
"history-graph": {
|
||||
"name": "History graph",
|
||||
|
||||
@@ -97,29 +97,36 @@ describe("entityDisplay", () => {
|
||||
areas: { area_1: mockArea({ area_id: "area_1", name: "Allée" }) },
|
||||
});
|
||||
|
||||
it("shows 'Area ▸ Device' with no scope", () => {
|
||||
it("shows 'Area ▸ Device' with no name detail (defaults to full)", () => {
|
||||
expect(entityDisplay(hass, "sensor.allee_battery")).toEqual({
|
||||
primary: "Battery state",
|
||||
secondary: "Allée ▸ Caméra Allée",
|
||||
});
|
||||
});
|
||||
|
||||
it("shows device only in an area-scoped logbook", () => {
|
||||
it("shows 'Area ▸ Device' for the 'area' name detail", () => {
|
||||
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 in a device-scoped logbook", () => {
|
||||
expect(entityDisplay(hass, "sensor.allee_battery", "device")).toEqual({
|
||||
it("shows no context for the 'entity' name detail", () => {
|
||||
expect(entityDisplay(hass, "sensor.allee_battery", "entity")).toEqual({
|
||||
primary: "Battery state",
|
||||
secondary: undefined,
|
||||
});
|
||||
});
|
||||
|
||||
it("shows no context in an entity-scoped logbook", () => {
|
||||
expect(entityDisplay(hass, "sensor.allee_battery", "entity")).toEqual({
|
||||
it("shows no context for the 'none' name detail", () => {
|
||||
expect(entityDisplay(hass, "sensor.allee_battery", "none")).toEqual({
|
||||
primary: "Battery state",
|
||||
secondary: undefined,
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user