450 lines
14 KiB
TypeScript
450 lines
14 KiB
TypeScript
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 type {
|
|
EntityDomainFilter,
|
|
EntityDomainFilterFunc,
|
|
} from "../../../common/entity/entity_domain_filter";
|
|
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-formfield";
|
|
import "../../../components/ha-settings-row";
|
|
import "../../../components/ha-switch";
|
|
import { fetchCloudAlexaEntity } from "../../../data/alexa";
|
|
import type { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
|
|
import {
|
|
fetchCloudStatus,
|
|
updateCloudGoogleEntityConfig,
|
|
} from "../../../data/cloud";
|
|
import type { ExtEntityRegistryEntry } from "../../../data/entity_registry";
|
|
import {
|
|
getExtendedEntityRegistryEntry,
|
|
updateEntityRegistryEntry,
|
|
} from "../../../data/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 { brandsUrl } from "../../../util/brands-url";
|
|
import { documentationUrl } from "../../../util/documentation-url";
|
|
import type { EntityRegistrySettings } from "../entities/entity-registry-settings";
|
|
|
|
@customElement("entity-voice-settings")
|
|
export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
|
|
@property({ attribute: false }) public entityId!: string;
|
|
|
|
@property({ attribute: false }) public exposed!: ExposeEntitySettings;
|
|
|
|
@property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
|
|
|
|
@state() private _cloudStatus?: CloudStatus;
|
|
|
|
@state() private _aliases?: string[];
|
|
|
|
@state() private _googleEntity?: GoogleEntity;
|
|
|
|
@state() private _unsupported: Partial<
|
|
Record<"cloud.google_assistant" | "cloud.alexa" | "conversation", boolean>
|
|
> = {};
|
|
|
|
protected willUpdate(changedProps: PropertyValues<this>) {
|
|
if (!isComponentLoaded(this.hass, "cloud")) {
|
|
return;
|
|
}
|
|
if (changedProps.has("entityId") && this.entityId) {
|
|
this._fetchEntities();
|
|
}
|
|
if (!this.hasUpdated) {
|
|
fetchCloudStatus(this.hass).then((status) => {
|
|
this._cloudStatus = status;
|
|
});
|
|
}
|
|
}
|
|
|
|
private async _fetchEntities() {
|
|
try {
|
|
const 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");
|
|
}
|
|
}
|
|
|
|
try {
|
|
await fetchCloudAlexaEntity(this.hass, this.entityId);
|
|
} catch (err: any) {
|
|
if (err.code === "not_supported") {
|
|
this._unsupported["cloud.alexa"] = true;
|
|
this.requestUpdate("_unsupported");
|
|
}
|
|
}
|
|
}
|
|
|
|
private _getEntityFilterFuncs = memoizeOne(
|
|
(googleFilter: EntityDomainFilter, alexaFilter: EntityDomainFilter) => ({
|
|
google: generateEntityDomainFilter(
|
|
googleFilter.include_domains,
|
|
googleFilter.include_entities,
|
|
googleFilter.exclude_domains,
|
|
googleFilter.exclude_entities
|
|
),
|
|
alexa: generateEntityDomainFilter(
|
|
alexaFilter.include_domains,
|
|
alexaFilter.include_entities,
|
|
alexaFilter.exclude_domains,
|
|
alexaFilter.exclude_entities
|
|
),
|
|
})
|
|
);
|
|
|
|
protected render() {
|
|
const googleEnabled =
|
|
this._cloudStatus?.logged_in === true &&
|
|
this._cloudStatus.prefs.google_enabled === true;
|
|
|
|
const alexaEnabled =
|
|
this._cloudStatus?.logged_in === true &&
|
|
this._cloudStatus.prefs.alexa_enabled === true;
|
|
|
|
const showAssistants = [...Object.keys(voiceAssistants)];
|
|
const uiAssistants = [...showAssistants];
|
|
|
|
const alexaManual =
|
|
alexaEnabled &&
|
|
!isEmptyEntityDomainFilter(
|
|
(this._cloudStatus as CloudStatusLoggedIn).alexa_entities
|
|
);
|
|
const googleManual =
|
|
googleEnabled &&
|
|
!isEmptyEntityDomainFilter(
|
|
(this._cloudStatus as CloudStatusLoggedIn).google_entities
|
|
);
|
|
|
|
if (!googleEnabled) {
|
|
showAssistants.splice(
|
|
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;
|
|
alexa: EntityDomainFilterFunc;
|
|
}
|
|
| undefined;
|
|
|
|
if (alexaManual || googleManual) {
|
|
manFilterFuncs = this._getEntityFilterFuncs(
|
|
(this._cloudStatus as CloudStatusLoggedIn).google_entities,
|
|
(this._cloudStatus as CloudStatusLoggedIn).alexa_entities
|
|
);
|
|
}
|
|
|
|
const manExposedAlexa = alexaManual && manFilterFuncs!.alexa(this.entityId);
|
|
const manExposedGoogle =
|
|
googleManual && manFilterFuncs!.google(this.entityId);
|
|
|
|
const anyExposed = uiExposed || manExposedAlexa || manExposedGoogle;
|
|
|
|
return html`
|
|
<ha-settings-row>
|
|
<h3 slot="heading">
|
|
${this.hass.localize("ui.dialogs.voice-settings.expose_header")}
|
|
</h3>
|
|
<ha-switch
|
|
@change=${this._toggleAll}
|
|
.assistants=${uiAssistants}
|
|
.checked=${anyExposed}
|
|
></ha-switch>
|
|
</ha-settings-row>
|
|
${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 manualConfig =
|
|
(alexaManual && key === "cloud.alexa") ||
|
|
(googleManual && key === "cloud.google_assistant");
|
|
|
|
const support2fa =
|
|
key === "cloud.google_assistant" &&
|
|
!googleManual &&
|
|
supported &&
|
|
this._googleEntity?.might_2fa;
|
|
|
|
return html`
|
|
<ha-settings-row .threeLine=${!supported && manualConfig}>
|
|
<img
|
|
alt=""
|
|
src=${brandsUrl({
|
|
domain: voiceAssistants[key].domain,
|
|
type: "icon",
|
|
darkOptimized: this.hass.themes?.darkMode,
|
|
})}
|
|
crossorigin="anonymous"
|
|
referrerpolicy="no-referrer"
|
|
slot="prefix"
|
|
/>
|
|
<span slot="heading">${voiceAssistants[key].name}</span>
|
|
${!supported
|
|
? html`<div slot="description" class="unsupported">
|
|
<ha-svg-icon .path=${mdiAlertCircle}></ha-svg-icon>
|
|
${this.hass.localize(
|
|
"ui.dialogs.voice-settings.unsupported"
|
|
)}
|
|
</div>`
|
|
: nothing}
|
|
${manualConfig
|
|
? html`
|
|
<div slot="description">
|
|
${this.hass.localize(
|
|
"ui.dialogs.voice-settings.manual_config"
|
|
)}
|
|
</div>
|
|
`
|
|
: nothing}
|
|
${support2fa
|
|
? html`
|
|
<ha-formfield
|
|
slot="description"
|
|
.label=${this.hass.localize(
|
|
"ui.dialogs.voice-settings.ask_pin"
|
|
)}
|
|
>
|
|
<ha-checkbox
|
|
.checked=${!this._googleEntity!.disable_2fa}
|
|
@change=${this._2faChanged}
|
|
></ha-checkbox>
|
|
</ha-formfield>
|
|
`
|
|
: nothing}
|
|
<ha-switch
|
|
.assistant=${key}
|
|
@change=${this._toggleAssistant}
|
|
.disabled=${manualConfig || (!exposed && !supported)}
|
|
.checked=${exposed}
|
|
></ha-switch>
|
|
</ha-settings-row>
|
|
`;
|
|
})
|
|
: 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-aliases-editor
|
|
.hass=${this.hass}
|
|
.aliases=${this._aliases ?? this.entry.aliases}
|
|
@value-changed=${this._aliasesChanged}
|
|
@blur=${this._saveAliases}
|
|
></ha-aliases-editor>`}
|
|
`;
|
|
}
|
|
|
|
private _aliasesChanged(ev) {
|
|
const currentLength =
|
|
this._aliases?.length ?? this.entry?.aliases?.length ?? 0;
|
|
|
|
this._aliases = ev.detail.value;
|
|
|
|
// if an entry was deleted, then save changes
|
|
if (currentLength > ev.detail.value.length) {
|
|
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 result = await updateEntityRegistryEntry(this.hass, this.entityId, {
|
|
aliases: this._aliases
|
|
.map((alias) => alias.trim())
|
|
.filter((alias) => alias),
|
|
});
|
|
fireEvent(this, "entity-entry-updated", result.entity_entry);
|
|
}
|
|
|
|
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");
|
|
}
|
|
|
|
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,
|
|
this.entityId
|
|
);
|
|
fireEvent(this, "entity-entry-updated", entry);
|
|
}
|
|
fireEvent(this, "exposed-entities-changed");
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return [
|
|
haStyle,
|
|
css`
|
|
:host {
|
|
display: block;
|
|
margin: 32px;
|
|
margin-top: 0;
|
|
--settings-row-prefix-display: contents;
|
|
--settings-row-content-display: contents;
|
|
}
|
|
ha-settings-row {
|
|
padding: 0;
|
|
}
|
|
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;
|
|
}
|
|
ha-formfield {
|
|
margin-left: -8px;
|
|
margin-inline-start: -8px;
|
|
margin-inline-end: initial;
|
|
}
|
|
ha-checkbox {
|
|
--mdc-checkbox-state-layer-size: 40px;
|
|
}
|
|
.unsupported {
|
|
display: flex;
|
|
align-items: center;
|
|
}
|
|
.unsupported ha-svg-icon {
|
|
color: var(--error-color);
|
|
--mdc-icon-size: 16px;
|
|
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;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"entity-registry-settings": EntityRegistrySettings;
|
|
}
|
|
interface HASSDomEvents {
|
|
"entity-entry-updated": ExtEntityRegistryEntry;
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"entity-voice-settings": EntityVoiceSettings;
|
|
}
|
|
}
|