mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-21 16:26:43 +00:00
Add initial expose UI (#16138)
Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
parent
a5edb4caaf
commit
442f73b8c5
@ -216,7 +216,7 @@ export class HassioAddonStore extends LitElement {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _filterChanged(e) {
|
private _filterChanged(e) {
|
||||||
this._filter = e.detail.value;
|
this._filter = e.detail.value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -73,7 +73,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData {
|
|||||||
main?: boolean;
|
main?: boolean;
|
||||||
title: TemplateResult | string;
|
title: TemplateResult | string;
|
||||||
label?: TemplateResult | string;
|
label?: TemplateResult | string;
|
||||||
type?: "numeric" | "icon" | "icon-button" | "overflow-menu";
|
type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex";
|
||||||
template?: (data: any, row: T) => TemplateResult | string | typeof nothing;
|
template?: (data: any, row: T) => TemplateResult | string | typeof nothing;
|
||||||
width?: string;
|
width?: string;
|
||||||
maxWidth?: string;
|
maxWidth?: string;
|
||||||
@ -359,10 +359,10 @@ export class HaDataTable extends LitElement {
|
|||||||
return nothing;
|
return nothing;
|
||||||
}
|
}
|
||||||
if (row.append) {
|
if (row.append) {
|
||||||
return html` <div class="mdc-data-table__row">${row.content}</div> `;
|
return html`<div class="mdc-data-table__row">${row.content}</div>`;
|
||||||
}
|
}
|
||||||
if (row.empty) {
|
if (row.empty) {
|
||||||
return html` <div class="mdc-data-table__row"></div> `;
|
return html`<div class="mdc-data-table__row"></div>`;
|
||||||
}
|
}
|
||||||
return html`
|
return html`
|
||||||
<div
|
<div
|
||||||
@ -406,6 +406,7 @@ export class HaDataTable extends LitElement {
|
|||||||
<div
|
<div
|
||||||
role=${column.main ? "rowheader" : "cell"}
|
role=${column.main ? "rowheader" : "cell"}
|
||||||
class="mdc-data-table__cell ${classMap({
|
class="mdc-data-table__cell ${classMap({
|
||||||
|
"mdc-data-table__cell--flex": column.type === "flex",
|
||||||
"mdc-data-table__cell--numeric": column.type === "numeric",
|
"mdc-data-table__cell--numeric": column.type === "numeric",
|
||||||
"mdc-data-table__cell--icon": column.type === "icon",
|
"mdc-data-table__cell--icon": column.type === "icon",
|
||||||
"mdc-data-table__cell--icon-button":
|
"mdc-data-table__cell--icon-button":
|
||||||
@ -663,6 +664,10 @@ export class HaDataTable extends LitElement {
|
|||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.mdc-data-table__cell.mdc-data-table__cell--flex {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
|
||||||
.mdc-data-table__cell.mdc-data-table__cell--icon {
|
.mdc-data-table__cell.mdc-data-table__cell--icon {
|
||||||
overflow: initial;
|
overflow: initial;
|
||||||
}
|
}
|
||||||
|
129
src/components/ha-aliases-editor.ts
Normal file
129
src/components/ha-aliases-editor.ts
Normal file
@ -0,0 +1,129 @@
|
|||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { haStyle } from "../resources/styles";
|
||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
import "./ha-area-picker";
|
||||||
|
import "./ha-textfield";
|
||||||
|
import type { HaTextField } from "./ha-textfield";
|
||||||
|
import { fireEvent } from "../common/dom/fire_event";
|
||||||
|
|
||||||
|
@customElement("ha-aliases-editor")
|
||||||
|
class AliasesEditor extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public aliases!: string[];
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.aliases) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this.aliases.map(
|
||||||
|
(alias, index) => html`
|
||||||
|
<div class="layout horizontal center-center row">
|
||||||
|
<ha-textfield
|
||||||
|
dialogInitialFocus=${index}
|
||||||
|
.index=${index}
|
||||||
|
class="flex-auto"
|
||||||
|
.label=${this.hass!.localize("ui.dialogs.aliases.input_label", {
|
||||||
|
number: index + 1,
|
||||||
|
})}
|
||||||
|
.value=${alias}
|
||||||
|
?data-last=${index === this.aliases.length - 1}
|
||||||
|
@input=${this._editAlias}
|
||||||
|
@keydown=${this._keyDownAlias}
|
||||||
|
></ha-textfield>
|
||||||
|
<ha-icon-button
|
||||||
|
.index=${index}
|
||||||
|
slot="navigationIcon"
|
||||||
|
label=${this.hass!.localize("ui.dialogs.aliases.remove_alias", {
|
||||||
|
number: index + 1,
|
||||||
|
})}
|
||||||
|
@click=${this._removeAlias}
|
||||||
|
.path=${mdiDeleteOutline}
|
||||||
|
></ha-icon-button>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
<div class="layout horizontal center-center">
|
||||||
|
<mwc-button @click=${this._addAlias}>
|
||||||
|
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</mwc-button>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _addAlias() {
|
||||||
|
this.aliases = [...this.aliases, ""];
|
||||||
|
this._fireChanged(this.aliases);
|
||||||
|
await this.updateComplete;
|
||||||
|
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
|
||||||
|
| HaTextField
|
||||||
|
| undefined;
|
||||||
|
field?.focus();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _editAlias(ev: Event) {
|
||||||
|
const index = (ev.target as any).index;
|
||||||
|
const aliases = [...this.aliases];
|
||||||
|
aliases[index] = (ev.target as any).value;
|
||||||
|
this._fireChanged(aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _keyDownAlias(ev: KeyboardEvent) {
|
||||||
|
if (ev.key === "Enter") {
|
||||||
|
ev.stopPropagation();
|
||||||
|
this._addAlias();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _removeAlias(ev: Event) {
|
||||||
|
const index = (ev.target as any).index;
|
||||||
|
const aliases = [...this.aliases];
|
||||||
|
aliases.splice(index, 1);
|
||||||
|
this._fireChanged(aliases);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _fireChanged(value) {
|
||||||
|
fireEvent(this, "value-changed", { value });
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
.row {
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
ha-textfield {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ha-icon-button {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
mwc-button {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
#alias_input {
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.alias {
|
||||||
|
border: 1px solid var(--divider-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-top: 4px;
|
||||||
|
--mdc-icon-button-size: 24px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-aliases-editor": AliasesEditor;
|
||||||
|
}
|
||||||
|
}
|
@ -9,15 +9,6 @@ interface CloudStatusNotLoggedIn {
|
|||||||
http_use_ssl: boolean;
|
http_use_ssl: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface GoogleEntityConfig {
|
|
||||||
should_expose?: boolean | null;
|
|
||||||
disable_2fa?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface AlexaEntityConfig {
|
|
||||||
should_expose?: boolean | null;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface CertificateInformation {
|
export interface CertificateInformation {
|
||||||
common_name: string;
|
common_name: string;
|
||||||
expire_date: string;
|
expire_date: string;
|
||||||
@ -30,14 +21,6 @@ export interface CloudPreferences {
|
|||||||
remote_enabled: boolean;
|
remote_enabled: boolean;
|
||||||
google_secure_devices_pin: string | undefined;
|
google_secure_devices_pin: string | undefined;
|
||||||
cloudhooks: { [webhookId: string]: CloudWebhook };
|
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||||
google_default_expose: string[] | null;
|
|
||||||
google_entity_configs: {
|
|
||||||
[entityId: string]: GoogleEntityConfig;
|
|
||||||
};
|
|
||||||
alexa_default_expose: string[] | null;
|
|
||||||
alexa_entity_configs: {
|
|
||||||
[entityId: string]: AlexaEntityConfig;
|
|
||||||
};
|
|
||||||
alexa_report_state: boolean;
|
alexa_report_state: boolean;
|
||||||
google_report_state: boolean;
|
google_report_state: boolean;
|
||||||
tts_default_voice: [string, string];
|
tts_default_voice: [string, string];
|
||||||
@ -150,10 +133,8 @@ export const updateCloudPref = (
|
|||||||
prefs: {
|
prefs: {
|
||||||
google_enabled?: CloudPreferences["google_enabled"];
|
google_enabled?: CloudPreferences["google_enabled"];
|
||||||
alexa_enabled?: CloudPreferences["alexa_enabled"];
|
alexa_enabled?: CloudPreferences["alexa_enabled"];
|
||||||
alexa_default_expose?: CloudPreferences["alexa_default_expose"];
|
|
||||||
alexa_report_state?: CloudPreferences["alexa_report_state"];
|
alexa_report_state?: CloudPreferences["alexa_report_state"];
|
||||||
google_report_state?: CloudPreferences["google_report_state"];
|
google_report_state?: CloudPreferences["google_report_state"];
|
||||||
google_default_expose?: CloudPreferences["google_default_expose"];
|
|
||||||
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
google_secure_devices_pin?: CloudPreferences["google_secure_devices_pin"];
|
||||||
tts_default_voice?: CloudPreferences["tts_default_voice"];
|
tts_default_voice?: CloudPreferences["tts_default_voice"];
|
||||||
}
|
}
|
||||||
@ -165,25 +146,14 @@ export const updateCloudPref = (
|
|||||||
|
|
||||||
export const updateCloudGoogleEntityConfig = (
|
export const updateCloudGoogleEntityConfig = (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entityId: string,
|
entity_id: string,
|
||||||
values: GoogleEntityConfig
|
disable_2fa: boolean
|
||||||
) =>
|
) =>
|
||||||
hass.callWS<GoogleEntityConfig>({
|
hass.callWS({
|
||||||
type: "cloud/google_assistant/entities/update",
|
type: "cloud/google_assistant/entities/update",
|
||||||
entity_id: entityId,
|
entity_id,
|
||||||
...values,
|
disable_2fa,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
|
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
|
||||||
hass.callApi("POST", "cloud/google_actions/sync");
|
hass.callApi("POST", "cloud/google_actions/sync");
|
||||||
|
|
||||||
export const updateCloudAlexaEntityConfig = (
|
|
||||||
hass: HomeAssistant,
|
|
||||||
entityId: string,
|
|
||||||
values: AlexaEntityConfig
|
|
||||||
) =>
|
|
||||||
hass.callWS<AlexaEntityConfig>({
|
|
||||||
type: "cloud/alexa/entities/update",
|
|
||||||
entity_id: entityId,
|
|
||||||
...values,
|
|
||||||
});
|
|
||||||
|
@ -9,5 +9,14 @@ export interface GoogleEntity {
|
|||||||
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
|
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
|
||||||
hass.callWS<GoogleEntity[]>({ type: "cloud/google_assistant/entities" });
|
hass.callWS<GoogleEntity[]>({ type: "cloud/google_assistant/entities" });
|
||||||
|
|
||||||
|
export const fetchCloudGoogleEntity = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entity_id: string
|
||||||
|
) =>
|
||||||
|
hass.callWS<GoogleEntity>({
|
||||||
|
type: "cloud/google_assistant/entities/get",
|
||||||
|
entity_id,
|
||||||
|
});
|
||||||
|
|
||||||
export const syncCloudGoogleEntities = (hass: HomeAssistant) =>
|
export const syncCloudGoogleEntities = (hass: HomeAssistant) =>
|
||||||
hass.callApi("POST", "cloud/google_actions/sync");
|
hass.callApi("POST", "cloud/google_actions/sync");
|
||||||
|
45
src/data/voice.ts
Normal file
45
src/data/voice.ts
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
|
export const voiceAssistants = {
|
||||||
|
conversation: { domain: "conversation", name: "Assist" },
|
||||||
|
"cloud.alexa": {
|
||||||
|
domain: "alexa",
|
||||||
|
name: "Amazon Alexa",
|
||||||
|
},
|
||||||
|
"cloud.google_assistant": {
|
||||||
|
domain: "google_assistant",
|
||||||
|
name: "Google Assistant",
|
||||||
|
},
|
||||||
|
} as const;
|
||||||
|
|
||||||
|
export const voiceAssistantKeys = Object.keys(voiceAssistants);
|
||||||
|
|
||||||
|
export const setExposeNewEntities = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
assistant: string,
|
||||||
|
expose_new: boolean
|
||||||
|
) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "homeassistant/expose_new_entities/set",
|
||||||
|
assistant,
|
||||||
|
expose_new,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getExposeNewEntities = (hass: HomeAssistant, assistant: string) =>
|
||||||
|
hass.callWS<{ expose_new: boolean }>({
|
||||||
|
type: "homeassistant/expose_new_entities/get",
|
||||||
|
assistant,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const exposeEntities = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
assistants: string[],
|
||||||
|
entity_ids: string[],
|
||||||
|
should_expose: boolean
|
||||||
|
) =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "homeassistant/expose_entity",
|
||||||
|
assistants,
|
||||||
|
entity_ids,
|
||||||
|
should_expose,
|
||||||
|
});
|
@ -1,16 +1,13 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import { mdiDeleteOutline, mdiPlus } from "@mdi/js";
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
import { fireEvent } from "../../common/dom/fire_event";
|
||||||
import "../../components/ha-alert";
|
import "../../components/ha-alert";
|
||||||
import "../../components/ha-area-picker";
|
|
||||||
import "../../components/ha-dialog";
|
import "../../components/ha-dialog";
|
||||||
import "../../components/ha-textfield";
|
|
||||||
import type { HaTextField } from "../../components/ha-textfield";
|
|
||||||
import { haStyle, haStyleDialog } from "../../resources/styles";
|
import { haStyle, haStyleDialog } from "../../resources/styles";
|
||||||
import { HomeAssistant } from "../../types";
|
import { HomeAssistant } from "../../types";
|
||||||
import { AliasesDialogParams } from "./show-dialog-aliases";
|
import { AliasesDialogParams } from "./show-dialog-aliases";
|
||||||
|
import "../../components/ha-aliases-editor";
|
||||||
|
|
||||||
@customElement("dialog-aliases")
|
@customElement("dialog-aliases")
|
||||||
class DialogAliases extends LitElement {
|
class DialogAliases extends LitElement {
|
||||||
@ -57,43 +54,11 @@ class DialogAliases extends LitElement {
|
|||||||
${this._error
|
${this._error
|
||||||
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
|
||||||
: ""}
|
: ""}
|
||||||
<div class="form">
|
<ha-aliases-editor
|
||||||
${this._aliases.map(
|
.hass=${this.hass}
|
||||||
(alias, index) => html`
|
.aliases=${this._aliases}
|
||||||
<div class="layout horizontal center-center row">
|
@value-changed=${this._aliasesChanged}
|
||||||
<ha-textfield
|
></ha-aliases-editor>
|
||||||
dialogInitialFocus=${index}
|
|
||||||
.index=${index}
|
|
||||||
class="flex-auto"
|
|
||||||
.label=${this.hass!.localize(
|
|
||||||
"ui.dialogs.aliases.input_label",
|
|
||||||
{ number: index + 1 }
|
|
||||||
)}
|
|
||||||
.value=${alias}
|
|
||||||
?data-last=${index === this._aliases.length - 1}
|
|
||||||
@input=${this._editAlias}
|
|
||||||
@keydown=${this._keyDownAlias}
|
|
||||||
></ha-textfield>
|
|
||||||
<ha-icon-button
|
|
||||||
.index=${index}
|
|
||||||
slot="navigationIcon"
|
|
||||||
label=${this.hass!.localize(
|
|
||||||
"ui.dialogs.aliases.remove_alias",
|
|
||||||
{ number: index + 1 }
|
|
||||||
)}
|
|
||||||
@click=${this._removeAlias}
|
|
||||||
.path=${mdiDeleteOutline}
|
|
||||||
></ha-icon-button>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
<div class="layout horizontal center-center">
|
|
||||||
<mwc-button @click=${this._addAlias}>
|
|
||||||
${this.hass!.localize("ui.dialogs.aliases.add_alias")}
|
|
||||||
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
|
||||||
</mwc-button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<mwc-button
|
<mwc-button
|
||||||
slot="secondaryAction"
|
slot="secondaryAction"
|
||||||
@ -113,32 +78,8 @@ class DialogAliases extends LitElement {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _addAlias() {
|
private _aliasesChanged(ev: CustomEvent): void {
|
||||||
this._aliases = [...this._aliases, ""];
|
this._aliases = ev.detail.value;
|
||||||
await this.updateComplete;
|
|
||||||
const field = this.shadowRoot?.querySelector(`ha-textfield[data-last]`) as
|
|
||||||
| HaTextField
|
|
||||||
| undefined;
|
|
||||||
field?.focus();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _editAlias(ev: Event) {
|
|
||||||
const index = (ev.target as any).index;
|
|
||||||
this._aliases[index] = (ev.target as any).value;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _keyDownAlias(ev: KeyboardEvent) {
|
|
||||||
if (ev.key === "Enter") {
|
|
||||||
ev.stopPropagation();
|
|
||||||
this._addAlias();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _removeAlias(ev: Event) {
|
|
||||||
const index = (ev.target as any).index;
|
|
||||||
const aliases = [...this._aliases];
|
|
||||||
aliases.splice(index, 1);
|
|
||||||
this._aliases = aliases;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _updateAliases(): Promise<void> {
|
private async _updateAliases(): Promise<void> {
|
||||||
|
@ -1,117 +0,0 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
|
||||||
import { customElement, state } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
import { createCloseHeading } from "../../components/ha-dialog";
|
|
||||||
import "../../components/ha-formfield";
|
|
||||||
import "../../components/ha-switch";
|
|
||||||
import { domainToName } from "../../data/integration";
|
|
||||||
import { haStyleDialog } from "../../resources/styles";
|
|
||||||
import { HomeAssistant } from "../../types";
|
|
||||||
import { HassDialog } from "../make-dialog-manager";
|
|
||||||
import { HaDomainTogglerDialogParams } from "./show-dialog-domain-toggler";
|
|
||||||
|
|
||||||
@customElement("dialog-domain-toggler")
|
|
||||||
class DomainTogglerDialog
|
|
||||||
extends LitElement
|
|
||||||
implements HassDialog<HaDomainTogglerDialogParams>
|
|
||||||
{
|
|
||||||
public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@state() private _params?: HaDomainTogglerDialogParams;
|
|
||||||
|
|
||||||
public showDialog(params: HaDomainTogglerDialogParams): void {
|
|
||||||
this._params = params;
|
|
||||||
}
|
|
||||||
|
|
||||||
public closeDialog() {
|
|
||||||
this._params = undefined;
|
|
||||||
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
|
||||||
}
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this._params) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const domains = this._params.domains
|
|
||||||
.map((domain) => [domainToName(this.hass.localize, domain), domain])
|
|
||||||
.sort();
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-dialog
|
|
||||||
open
|
|
||||||
@closed=${this.closeDialog}
|
|
||||||
scrimClickAction
|
|
||||||
escapeKeyAction
|
|
||||||
hideActions
|
|
||||||
.heading=${createCloseHeading(
|
|
||||||
this.hass,
|
|
||||||
this._params.title ||
|
|
||||||
this.hass.localize("ui.dialogs.domain_toggler.title")
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${this._params.description
|
|
||||||
? html`<div class="description">${this._params.description}</div>`
|
|
||||||
: ""}
|
|
||||||
<div class="domains">
|
|
||||||
${domains.map(
|
|
||||||
(domain) =>
|
|
||||||
html`
|
|
||||||
<ha-formfield .label=${domain[0]}>
|
|
||||||
<ha-switch
|
|
||||||
.domain=${domain[1]}
|
|
||||||
.checked=${!this._params!.exposedDomains ||
|
|
||||||
this._params!.exposedDomains.includes(domain[1])}
|
|
||||||
@change=${this._handleSwitch}
|
|
||||||
>
|
|
||||||
</ha-switch>
|
|
||||||
</ha-formfield>
|
|
||||||
<mwc-button .domain=${domain[1]} @click=${this._handleReset}>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.dialogs.domain_toggler.reset_entities"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</ha-dialog>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleSwitch(ev) {
|
|
||||||
this._params!.toggleDomain(ev.currentTarget.domain, ev.target.checked);
|
|
||||||
ev.currentTarget.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _handleReset(ev) {
|
|
||||||
this._params!.resetDomain(ev.currentTarget.domain);
|
|
||||||
ev.currentTarget.blur();
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyleDialog,
|
|
||||||
css`
|
|
||||||
ha-dialog {
|
|
||||||
--mdc-dialog-max-width: 500px;
|
|
||||||
}
|
|
||||||
.description {
|
|
||||||
margin-bottom: 8px;
|
|
||||||
}
|
|
||||||
.domains {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: auto auto;
|
|
||||||
grid-row-gap: 8px;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"dialog-domain-toggler": DomainTogglerDialog;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,23 +0,0 @@
|
|||||||
import { fireEvent } from "../../common/dom/fire_event";
|
|
||||||
|
|
||||||
export interface HaDomainTogglerDialogParams {
|
|
||||||
title?: string;
|
|
||||||
description?: string;
|
|
||||||
domains: string[];
|
|
||||||
exposedDomains: string[] | null;
|
|
||||||
toggleDomain: (domain: string, turnOn: boolean) => void;
|
|
||||||
resetDomain: (domain: string) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const loadDomainTogglerDialog = () => import("./dialog-domain-toggler");
|
|
||||||
|
|
||||||
export const showDomainTogglerDialog = (
|
|
||||||
element: HTMLElement,
|
|
||||||
dialogParams: HaDomainTogglerDialogParams
|
|
||||||
): void => {
|
|
||||||
fireEvent(element, "show-dialog", {
|
|
||||||
dialogTag: "dialog-domain-toggler",
|
|
||||||
dialogImport: loadDomainTogglerDialog,
|
|
||||||
dialogParams,
|
|
||||||
});
|
|
||||||
};
|
|
@ -0,0 +1,49 @@
|
|||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { ExtEntityRegistryEntry } from "../../../../data/entity_registry";
|
||||||
|
import "../../../../panels/config/voice-assistants/entity-voice-settings";
|
||||||
|
import { HomeAssistant } from "../../../../types";
|
||||||
|
|
||||||
|
@customElement("ha-more-info-view-voice-assistants")
|
||||||
|
class MoreInfoViewVoiceAssistants extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public entry!: ExtEntityRegistryEntry;
|
||||||
|
|
||||||
|
@property() public params?;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
return html`<entity-voice-settings
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entry=${this.entry}
|
||||||
|
></entity-voice-settings>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 24px;
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-more-info-view-voice-assistants": MoreInfoViewVoiceAssistants;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,16 @@
|
|||||||
|
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export const loadVoiceAssistantsView = () =>
|
||||||
|
import("./ha-more-info-view-voice-assistants");
|
||||||
|
|
||||||
|
export const showVoiceAssistantsView = (
|
||||||
|
element: HTMLElement,
|
||||||
|
title: string
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-child-view", {
|
||||||
|
viewTag: "ha-more-info-view-voice-assistants",
|
||||||
|
viewImport: loadVoiceAssistantsView,
|
||||||
|
viewTitle: title,
|
||||||
|
viewParams: {},
|
||||||
|
});
|
||||||
|
};
|
@ -181,10 +181,10 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
this.setView("settings");
|
this.setView("settings");
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _showChildView(ev: CustomEvent): Promise<void> {
|
private _showChildView(ev: CustomEvent): void {
|
||||||
const view = ev.detail as ChildView;
|
const view = ev.detail as ChildView;
|
||||||
if (view.viewImport) {
|
if (view.viewImport) {
|
||||||
await view.viewImport();
|
view.viewImport();
|
||||||
}
|
}
|
||||||
this._childView = view;
|
this._childView = view;
|
||||||
}
|
}
|
||||||
@ -369,12 +369,14 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
tabindex="-1"
|
tabindex="-1"
|
||||||
dialogInitialFocus
|
dialogInitialFocus
|
||||||
@show-child-view=${this._showChildView}
|
@show-child-view=${this._showChildView}
|
||||||
|
@entity-entry-updated=${this._entryUpdated}
|
||||||
>
|
>
|
||||||
${this._childView
|
${this._childView
|
||||||
? html`
|
? html`
|
||||||
<div class="child-view">
|
<div class="child-view">
|
||||||
${dynamicElement(this._childView.viewTag, {
|
${dynamicElement(this._childView.viewTag, {
|
||||||
hass: this.hass,
|
hass: this.hass,
|
||||||
|
entry: this._entry,
|
||||||
params: this._childView.viewParams,
|
params: this._childView.viewParams,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@ -401,7 +403,6 @@ export class MoreInfoDialog extends LitElement {
|
|||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
.entityId=${this._entityId}
|
.entityId=${this._entityId}
|
||||||
.entry=${this._entry}
|
.entry=${this._entry}
|
||||||
@entity-entry-updated=${this._entryUpdated}
|
|
||||||
></ha-more-info-settings>
|
></ha-more-info-settings>
|
||||||
`
|
`
|
||||||
: this._currView === "related"
|
: this._currView === "related"
|
||||||
|
@ -196,7 +196,7 @@ class HaBlueprintOverview extends LitElement {
|
|||||||
template: (_, blueprint: any) =>
|
template: (_, blueprint: any) =>
|
||||||
blueprint.error
|
blueprint.error
|
||||||
? ""
|
? ""
|
||||||
: html` <ha-icon-button
|
: html`<ha-icon-button
|
||||||
.blueprint=${blueprint}
|
.blueprint=${blueprint}
|
||||||
.label=${this.hass.localize(
|
.label=${this.hass.localize(
|
||||||
"ui.panel.config.blueprint.overview.delete_blueprint"
|
"ui.panel.config.blueprint.overview.delete_blueprint"
|
||||||
|
@ -10,6 +10,7 @@ import { debounce } from "../../../../common/util/debounce";
|
|||||||
import "../../../../components/buttons/ha-call-api-button";
|
import "../../../../components/buttons/ha-call-api-button";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-tip";
|
||||||
import {
|
import {
|
||||||
cloudLogout,
|
cloudLogout,
|
||||||
CloudStatusLoggedIn,
|
CloudStatusLoggedIn,
|
||||||
@ -22,8 +23,6 @@ import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
|
|||||||
import { haStyle } from "../../../../resources/styles";
|
import { haStyle } from "../../../../resources/styles";
|
||||||
import { HomeAssistant } from "../../../../types";
|
import { HomeAssistant } from "../../../../types";
|
||||||
import "../../ha-config-section";
|
import "../../ha-config-section";
|
||||||
import "./cloud-alexa-pref";
|
|
||||||
import "./cloud-google-pref";
|
|
||||||
import "./cloud-remote-pref";
|
import "./cloud-remote-pref";
|
||||||
import "./cloud-tts-pref";
|
import "./cloud-tts-pref";
|
||||||
import "./cloud-webhooks";
|
import "./cloud-webhooks";
|
||||||
@ -185,17 +184,13 @@ export class CloudAccount extends SubscribeMixin(LitElement) {
|
|||||||
dir=${this._rtlDirection}
|
dir=${this._rtlDirection}
|
||||||
></cloud-tts-pref>
|
></cloud-tts-pref>
|
||||||
|
|
||||||
<cloud-alexa-pref
|
<ha-tip .hass=${this.hass}>
|
||||||
.hass=${this.hass}
|
<a href="/config/voice-assistants">
|
||||||
.cloudStatus=${this.cloudStatus}
|
${this.hass.localize(
|
||||||
dir=${this._rtlDirection}
|
"ui.panel.config.cloud.account.tip_moved_voice_assistants"
|
||||||
></cloud-alexa-pref>
|
)}
|
||||||
|
</a>
|
||||||
<cloud-google-pref
|
</ha-tip>
|
||||||
.hass=${this.hass}
|
|
||||||
.cloudStatus=${this.cloudStatus}
|
|
||||||
dir=${this._rtlDirection}
|
|
||||||
></cloud-google-pref>
|
|
||||||
|
|
||||||
<cloud-webhooks
|
<cloud-webhooks
|
||||||
.hass=${this.hass}
|
.hass=${this.hass}
|
||||||
|
@ -1,203 +0,0 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import { mdiHelpCircle } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
|
||||||
import { property } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
|
||||||
import "../../../../components/ha-alert";
|
|
||||||
import "../../../../components/ha-card";
|
|
||||||
import "../../../../components/ha-settings-row";
|
|
||||||
import "../../../../components/ha-switch";
|
|
||||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
|
||||||
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
|
|
||||||
export class CloudAlexaPref extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public cloudStatus?: CloudStatusLoggedIn;
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this.cloudStatus) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const alexa_registered = this.cloudStatus.alexa_registered;
|
|
||||||
const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-card
|
|
||||||
outlined
|
|
||||||
header=${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.title"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div class="header-actions">
|
|
||||||
<a
|
|
||||||
href="https://www.nabucasa.com/config/amazon_alexa/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
class="icon-link"
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.link_learn_how_it_works"
|
|
||||||
)}
|
|
||||||
.path=${mdiHelpCircle}
|
|
||||||
></ha-icon-button>
|
|
||||||
</a>
|
|
||||||
<ha-switch
|
|
||||||
.checked=${alexa_enabled}
|
|
||||||
@change=${this._enabledToggleChanged}
|
|
||||||
></ha-switch>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<p>
|
|
||||||
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
|
|
||||||
</p>
|
|
||||||
${!alexa_enabled
|
|
||||||
? ""
|
|
||||||
: !alexa_registered
|
|
||||||
? html`
|
|
||||||
<ha-alert
|
|
||||||
.title=${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.not_configured_title"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.not_configured_text"
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://www.nabucasa.com/config/amazon_alexa/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.config_documentation"
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ha-alert>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.enable_state_reporting"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="description">
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.info_state_reporting"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<ha-switch
|
|
||||||
.checked=${alexa_report_state}
|
|
||||||
@change=${this._reportToggleChanged}
|
|
||||||
></ha-switch>
|
|
||||||
</ha-settings-row>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<a href="/config/cloud/alexa">
|
|
||||||
<mwc-button
|
|
||||||
>${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.manage_entities"
|
|
||||||
)}</mwc-button
|
|
||||||
>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _enabledToggleChanged(ev) {
|
|
||||||
const toggle = ev.target as HaSwitch;
|
|
||||||
try {
|
|
||||||
await updateCloudPref(this.hass!, { alexa_enabled: toggle.checked! });
|
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
|
||||||
} catch (err: any) {
|
|
||||||
toggle.checked = !toggle.checked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _reportToggleChanged(ev) {
|
|
||||||
const toggle = ev.target as HaSwitch;
|
|
||||||
try {
|
|
||||||
await updateCloudPref(this.hass!, {
|
|
||||||
alexa_report_state: toggle.checked!,
|
|
||||||
});
|
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(
|
|
||||||
`${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.alexa.state_reporting_error",
|
|
||||||
"enable_disable",
|
|
||||||
this.hass!.localize(
|
|
||||||
toggle.checked
|
|
||||||
? "ui.panel.config.cloud.account.alexa.enable"
|
|
||||||
: "ui.panel.config.cloud.account.alexa.disable"
|
|
||||||
)
|
|
||||||
)} ${err.message}`
|
|
||||||
);
|
|
||||||
toggle.checked = !toggle.checked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
ha-settings-row {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
.header-actions {
|
|
||||||
position: absolute;
|
|
||||||
right: 24px;
|
|
||||||
top: 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
:host([dir="rtl"]) .header-actions {
|
|
||||||
right: auto;
|
|
||||||
left: 24px;
|
|
||||||
}
|
|
||||||
.header-actions .icon-link {
|
|
||||||
margin-top: -16px;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
margin-right: 8px;
|
|
||||||
direction: var(--direction);
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.card-actions a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"cloud-alexa-pref": CloudAlexaPref;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("cloud-alexa-pref", CloudAlexaPref);
|
|
@ -1,274 +0,0 @@
|
|||||||
import "@material/mwc-button";
|
|
||||||
import { mdiHelpCircle } from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
|
||||||
import { property } from "lit/decorators";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
|
||||||
import "../../../../components/ha-alert";
|
|
||||||
import "../../../../components/ha-card";
|
|
||||||
import "../../../../components/ha-settings-row";
|
|
||||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
|
||||||
import "../../../../components/ha-textfield";
|
|
||||||
import type { HaTextField } from "../../../../components/ha-textfield";
|
|
||||||
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
|
|
||||||
|
|
||||||
export class CloudGooglePref extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
|
|
||||||
|
|
||||||
protected render() {
|
|
||||||
if (!this.cloudStatus) {
|
|
||||||
return nothing;
|
|
||||||
}
|
|
||||||
|
|
||||||
const google_registered = this.cloudStatus.google_registered;
|
|
||||||
const { google_enabled, google_report_state, google_secure_devices_pin } =
|
|
||||||
this.cloudStatus.prefs;
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<ha-card
|
|
||||||
outlined
|
|
||||||
header=${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.title"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
<div class="header-actions">
|
|
||||||
<a
|
|
||||||
href="https://www.nabucasa.com/config/google_assistant/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
class="icon-link"
|
|
||||||
>
|
|
||||||
<ha-icon-button
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.link_learn_how_it_works"
|
|
||||||
)}
|
|
||||||
.path=${mdiHelpCircle}
|
|
||||||
></ha-icon-button>
|
|
||||||
</a>
|
|
||||||
<ha-switch
|
|
||||||
.checked=${google_enabled}
|
|
||||||
@change=${this._enabledToggleChanged}
|
|
||||||
></ha-switch>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
|
||||||
<p>
|
|
||||||
${this.hass.localize("ui.panel.config.cloud.account.google.info")}
|
|
||||||
</p>
|
|
||||||
${!google_enabled
|
|
||||||
? ""
|
|
||||||
: !google_registered
|
|
||||||
? html`
|
|
||||||
<ha-alert
|
|
||||||
.title=${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.not_configured_title"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.not_configured_text"
|
|
||||||
)}
|
|
||||||
|
|
||||||
<ul>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://assistant.google.com/services/a/uid/00000091fd5fb875?hl=en-US"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.enable_ha_skill"
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
<li>
|
|
||||||
<a
|
|
||||||
href="https://www.nabucasa.com/config/google_assistant/"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.config_documentation"
|
|
||||||
)}
|
|
||||||
</a>
|
|
||||||
</li>
|
|
||||||
</ul>
|
|
||||||
</ha-alert>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
${this.cloudStatus.http_use_ssl
|
|
||||||
? html`
|
|
||||||
<ha-alert
|
|
||||||
alert-type="warning"
|
|
||||||
.title=${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.http_use_ssl_warning_title"
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.http_use_ssl_warning_text"
|
|
||||||
)}
|
|
||||||
<a
|
|
||||||
href="https://www.nabucasa.com/config/google_assistant/#local-communication"
|
|
||||||
target="_blank"
|
|
||||||
rel="noreferrer"
|
|
||||||
>${this.hass.localize(
|
|
||||||
"ui.panel.config.common.learn_more"
|
|
||||||
)}</a
|
|
||||||
>
|
|
||||||
</ha-alert>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.enable_state_reporting"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="description">
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.info_state_reporting"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<ha-switch
|
|
||||||
.checked=${google_report_state}
|
|
||||||
@change=${this._reportToggleChanged}
|
|
||||||
></ha-switch>
|
|
||||||
</ha-settings-row>
|
|
||||||
|
|
||||||
<ha-settings-row>
|
|
||||||
<span slot="heading">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.security_devices"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="description">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.enter_pin_info"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
</ha-settings-row>
|
|
||||||
|
|
||||||
<ha-textfield
|
|
||||||
id="google_secure_devices_pin"
|
|
||||||
.label=${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.devices_pin"
|
|
||||||
)}
|
|
||||||
.placeholder=${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.enter_pin_hint"
|
|
||||||
)}
|
|
||||||
.value=${google_secure_devices_pin || ""}
|
|
||||||
@change=${this._pinChanged}
|
|
||||||
></ha-textfield>
|
|
||||||
`}
|
|
||||||
</div>
|
|
||||||
<div class="card-actions">
|
|
||||||
<a href="/config/cloud/google-assistant">
|
|
||||||
<mwc-button>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.manage_entities"
|
|
||||||
)}
|
|
||||||
</mwc-button>
|
|
||||||
</a>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _enabledToggleChanged(ev) {
|
|
||||||
const toggle = ev.target as HaSwitch;
|
|
||||||
try {
|
|
||||||
await updateCloudPref(this.hass, { google_enabled: toggle.checked! });
|
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
|
||||||
} catch (err: any) {
|
|
||||||
toggle.checked = !toggle.checked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _reportToggleChanged(ev) {
|
|
||||||
const toggle = ev.target as HaSwitch;
|
|
||||||
try {
|
|
||||||
await updateCloudPref(this.hass, {
|
|
||||||
google_report_state: toggle.checked!,
|
|
||||||
});
|
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(
|
|
||||||
`Unable to ${toggle.checked ? "enable" : "disable"} report state. ${
|
|
||||||
err.message
|
|
||||||
}`
|
|
||||||
);
|
|
||||||
toggle.checked = !toggle.checked;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _pinChanged(ev) {
|
|
||||||
const input = ev.target as HaTextField;
|
|
||||||
try {
|
|
||||||
await updateCloudPref(this.hass, {
|
|
||||||
[input.id]: input.value || null,
|
|
||||||
});
|
|
||||||
showSaveSuccessToast(this, this.hass);
|
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(
|
|
||||||
`${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.account.google.enter_pin_error"
|
|
||||||
)} ${err.message}`
|
|
||||||
);
|
|
||||||
input.value = this.cloudStatus!.prefs.google_secure_devices_pin || "";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return css`
|
|
||||||
a {
|
|
||||||
color: var(--primary-color);
|
|
||||||
}
|
|
||||||
.header-actions {
|
|
||||||
position: absolute;
|
|
||||||
right: 24px;
|
|
||||||
top: 24px;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: row;
|
|
||||||
}
|
|
||||||
:host([dir="rtl"]) .header-actions {
|
|
||||||
right: auto;
|
|
||||||
left: 24px;
|
|
||||||
}
|
|
||||||
.header-actions .icon-link {
|
|
||||||
margin-top: -16px;
|
|
||||||
margin-inline-end: 8px;
|
|
||||||
margin-right: 8px;
|
|
||||||
direction: var(--direction);
|
|
||||||
color: var(--secondary-text-color);
|
|
||||||
}
|
|
||||||
ha-settings-row {
|
|
||||||
padding: 0;
|
|
||||||
}
|
|
||||||
ha-textfield {
|
|
||||||
width: 250px;
|
|
||||||
display: block;
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.card-actions {
|
|
||||||
display: flex;
|
|
||||||
}
|
|
||||||
.card-actions a {
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
.warning {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"cloud-google-pref": CloudGooglePref;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
customElements.define("cloud-google-pref", CloudGooglePref);
|
|
@ -1,569 +0,0 @@
|
|||||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
|
||||||
import "@material/mwc-list/mwc-list-item";
|
|
||||||
import {
|
|
||||||
mdiCheckboxMarked,
|
|
||||||
mdiCheckboxMultipleMarked,
|
|
||||||
mdiCloseBox,
|
|
||||||
mdiCloseBoxMultiple,
|
|
||||||
mdiDotsVertical,
|
|
||||||
mdiFormatListChecks,
|
|
||||||
mdiSync,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
|
||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
|
||||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
|
||||||
import {
|
|
||||||
EntityFilter,
|
|
||||||
generateFilter,
|
|
||||||
isEmptyFilter,
|
|
||||||
} from "../../../../common/entity/entity_filter";
|
|
||||||
import { stringCompare } from "../../../../common/string/compare";
|
|
||||||
import "../../../../components/entity/state-info";
|
|
||||||
import "../../../../components/ha-button-menu";
|
|
||||||
import "../../../../components/ha-card";
|
|
||||||
import "../../../../components/ha-formfield";
|
|
||||||
import "../../../../components/ha-icon-button";
|
|
||||||
import "../../../../components/ha-switch";
|
|
||||||
import {
|
|
||||||
AlexaEntity,
|
|
||||||
fetchCloudAlexaEntities,
|
|
||||||
syncCloudAlexaEntities,
|
|
||||||
} from "../../../../data/alexa";
|
|
||||||
import {
|
|
||||||
AlexaEntityConfig,
|
|
||||||
CloudPreferences,
|
|
||||||
CloudStatusLoggedIn,
|
|
||||||
updateCloudAlexaEntityConfig,
|
|
||||||
updateCloudPref,
|
|
||||||
} from "../../../../data/cloud";
|
|
||||||
import { EntityRegistryEntry } from "../../../../data/entity_registry";
|
|
||||||
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
|
|
||||||
import "../../../../layouts/hass-loading-screen";
|
|
||||||
import "../../../../layouts/hass-subpage";
|
|
||||||
import { haStyle } from "../../../../resources/styles";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG_EXPOSE = true;
|
|
||||||
|
|
||||||
@customElement("cloud-alexa")
|
|
||||||
class CloudAlexa extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property()
|
|
||||||
public cloudStatus!: CloudStatusLoggedIn;
|
|
||||||
|
|
||||||
@property({ type: Boolean }) public narrow!: boolean;
|
|
||||||
|
|
||||||
@state() private _entities?: AlexaEntity[];
|
|
||||||
|
|
||||||
@state() private _syncing = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private _entityConfigs: CloudPreferences["alexa_entity_configs"] = {};
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private _entityCategories?: Record<
|
|
||||||
string,
|
|
||||||
EntityRegistryEntry["entity_category"]
|
|
||||||
>;
|
|
||||||
|
|
||||||
private _popstateSyncAttached = false;
|
|
||||||
|
|
||||||
private _popstateReloadStatusAttached = false;
|
|
||||||
|
|
||||||
private _isInitialExposed?: Set<string>;
|
|
||||||
|
|
||||||
private _getEntityFilterFunc = memoizeOne((filter: EntityFilter) =>
|
|
||||||
generateFilter(
|
|
||||||
filter.include_domains,
|
|
||||||
filter.include_entities,
|
|
||||||
filter.exclude_domains,
|
|
||||||
filter.exclude_entities
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (this._entities === undefined || this._entityCategories === undefined) {
|
|
||||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
|
||||||
}
|
|
||||||
const emptyFilter = isEmptyFilter(this.cloudStatus.alexa_entities);
|
|
||||||
const filterFunc = this._getEntityFilterFunc(
|
|
||||||
this.cloudStatus.alexa_entities
|
|
||||||
);
|
|
||||||
|
|
||||||
// We will only generate `isInitialExposed` during first render.
|
|
||||||
// On each subsequent render we will use the same set so that cards
|
|
||||||
// will not jump around when we change the exposed setting.
|
|
||||||
const showInExposed = this._isInitialExposed || new Set();
|
|
||||||
const trackExposed = this._isInitialExposed === undefined;
|
|
||||||
|
|
||||||
let selected = 0;
|
|
||||||
|
|
||||||
// On first render we decide which cards show in which category.
|
|
||||||
// That way cards won't jump around when changing values.
|
|
||||||
const exposedCards: TemplateResult[] = [];
|
|
||||||
const notExposedCards: TemplateResult[] = [];
|
|
||||||
|
|
||||||
this._entities.forEach((entity) => {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
const config = this._entityConfigs[entity.entity_id] || {
|
|
||||||
should_expose: null,
|
|
||||||
};
|
|
||||||
const isExposed = emptyFilter
|
|
||||||
? this._configIsExposed(
|
|
||||||
entity.entity_id,
|
|
||||||
config,
|
|
||||||
this._entityCategories![entity.entity_id]
|
|
||||||
)
|
|
||||||
: filterFunc(entity.entity_id);
|
|
||||||
const isDomainExposed = emptyFilter
|
|
||||||
? this._configIsDomainExposed(
|
|
||||||
entity.entity_id,
|
|
||||||
this._entityCategories![entity.entity_id]
|
|
||||||
)
|
|
||||||
: filterFunc(entity.entity_id);
|
|
||||||
if (isExposed) {
|
|
||||||
selected++;
|
|
||||||
|
|
||||||
if (trackExposed) {
|
|
||||||
showInExposed.add(entity.entity_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = showInExposed.has(entity.entity_id)
|
|
||||||
? exposedCards
|
|
||||||
: notExposedCards;
|
|
||||||
|
|
||||||
const iconButton = html`<ha-icon-button
|
|
||||||
slot="trigger"
|
|
||||||
class=${classMap({
|
|
||||||
exposed: isExposed!,
|
|
||||||
"not-exposed": !isExposed,
|
|
||||||
})}
|
|
||||||
.disabled=${!emptyFilter}
|
|
||||||
.label=${this.hass!.localize("ui.panel.config.cloud.alexa.expose")}
|
|
||||||
.path=${config.should_expose !== null
|
|
||||||
? isExposed
|
|
||||||
? mdiCheckboxMarked
|
|
||||||
: mdiCloseBox
|
|
||||||
: isDomainExposed
|
|
||||||
? mdiCheckboxMultipleMarked
|
|
||||||
: mdiCloseBoxMultiple}
|
|
||||||
></ha-icon-button>`;
|
|
||||||
|
|
||||||
target.push(html`
|
|
||||||
<ha-card outlined>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="top-line">
|
|
||||||
<state-info
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
@click=${this._showMoreInfo}
|
|
||||||
>
|
|
||||||
</state-info>
|
|
||||||
${!emptyFilter
|
|
||||||
? html`${iconButton}`
|
|
||||||
: html`<ha-button-menu
|
|
||||||
.entityId=${stateObj.entity_id}
|
|
||||||
@action=${this._exposeChanged}
|
|
||||||
>
|
|
||||||
${iconButton}
|
|
||||||
<mwc-list-item hasMeta>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.expose_entity"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
class="exposed"
|
|
||||||
slot="meta"
|
|
||||||
.path=${mdiCheckboxMarked}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
<mwc-list-item hasMeta>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.dont_expose_entity"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
class="not-exposed"
|
|
||||||
slot="meta"
|
|
||||||
.path=${mdiCloseBox}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
<mwc-list-item hasMeta>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.follow_domain"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
class=${classMap({
|
|
||||||
exposed: isDomainExposed,
|
|
||||||
"not-exposed": !isDomainExposed,
|
|
||||||
})}
|
|
||||||
slot="meta"
|
|
||||||
.path=${isDomainExposed
|
|
||||||
? mdiCheckboxMultipleMarked
|
|
||||||
: mdiCloseBoxMultiple}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
</ha-button-menu>`}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (trackExposed) {
|
|
||||||
this._isInitialExposed = showInExposed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<hass-subpage
|
|
||||||
.hass=${this.hass}
|
|
||||||
.narrow=${this.narrow}
|
|
||||||
.header=${this.hass!.localize("ui.panel.config.cloud.alexa.title")}
|
|
||||||
>
|
|
||||||
<ha-button-menu slot="toolbar-icon">
|
|
||||||
<ha-icon-button
|
|
||||||
slot="trigger"
|
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
|
||||||
.path=${mdiDotsVertical}
|
|
||||||
></ha-icon-button>
|
|
||||||
|
|
||||||
<mwc-list-item
|
|
||||||
graphic="icon"
|
|
||||||
.disabled=${!emptyFilter}
|
|
||||||
@click=${this._openDomainToggler}
|
|
||||||
>
|
|
||||||
${this.hass.localize("ui.panel.config.cloud.alexa.manage_defaults")}
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiFormatListChecks}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
|
|
||||||
<mwc-list-item
|
|
||||||
graphic="icon"
|
|
||||||
.disabled=${this._syncing}
|
|
||||||
@click=${this._handleSync}
|
|
||||||
>
|
|
||||||
${this.hass.localize("ui.panel.config.cloud.alexa.sync_entities")}
|
|
||||||
<ha-svg-icon slot="graphic" .path=${mdiSync}></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
</ha-button-menu>
|
|
||||||
${!emptyFilter
|
|
||||||
? html`
|
|
||||||
<div class="banner">
|
|
||||||
${this.hass!.localize("ui.panel.config.cloud.alexa.banner")}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${exposedCards.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="header">
|
|
||||||
<h3>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.exposed_entities"
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
${!this.narrow
|
|
||||||
? this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.exposed",
|
|
||||||
"selected",
|
|
||||||
selected
|
|
||||||
)
|
|
||||||
: selected}
|
|
||||||
</div>
|
|
||||||
<div class="content">${exposedCards}</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
${notExposedCards.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="header second">
|
|
||||||
<h3>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.not_exposed_entities"
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
${!this.narrow
|
|
||||||
? this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.not_exposed",
|
|
||||||
"selected",
|
|
||||||
this._entities.length - selected
|
|
||||||
)
|
|
||||||
: this._entities.length - selected}
|
|
||||||
</div>
|
|
||||||
<div class="content">${notExposedCards}</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</hass-subpage>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
|
||||||
super.firstUpdated(changedProps);
|
|
||||||
this._fetchData();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps) {
|
|
||||||
super.updated(changedProps);
|
|
||||||
if (changedProps.has("cloudStatus")) {
|
|
||||||
this._entityConfigs = this.cloudStatus.prefs.alexa_entity_configs;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
changedProps.has("hass") &&
|
|
||||||
changedProps.get("hass")?.entities !== this.hass.entities
|
|
||||||
) {
|
|
||||||
const categories = {};
|
|
||||||
|
|
||||||
for (const entry of Object.values(this.hass.entities)) {
|
|
||||||
categories[entry.entity_id] = entry.entity_category;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._entityCategories = categories;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchData() {
|
|
||||||
const entities = await fetchCloudAlexaEntities(this.hass);
|
|
||||||
entities.sort((a, b) => {
|
|
||||||
const stateA = this.hass.states[a.entity_id];
|
|
||||||
const stateB = this.hass.states[b.entity_id];
|
|
||||||
return stringCompare(
|
|
||||||
stateA ? computeStateName(stateA) : a.entity_id,
|
|
||||||
stateB ? computeStateName(stateB) : b.entity_id,
|
|
||||||
this.hass.locale.language
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this._entities = entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _showMoreInfo(ev) {
|
|
||||||
const entityId = ev.currentTarget.stateObj.entity_id;
|
|
||||||
fireEvent(this, "hass-more-info", { entityId });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _configIsDomainExposed(
|
|
||||||
entityId: string,
|
|
||||||
entityCategory: EntityRegistryEntry["entity_category"] | undefined
|
|
||||||
) {
|
|
||||||
const domain = computeDomain(entityId);
|
|
||||||
return this.cloudStatus.prefs.alexa_default_expose
|
|
||||||
? !entityCategory &&
|
|
||||||
this.cloudStatus.prefs.alexa_default_expose.includes(domain)
|
|
||||||
: DEFAULT_CONFIG_EXPOSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _configIsExposed(
|
|
||||||
entityId: string,
|
|
||||||
config: AlexaEntityConfig,
|
|
||||||
entityCategory: EntityRegistryEntry["entity_category"] | undefined
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
config.should_expose ??
|
|
||||||
this._configIsDomainExposed(entityId, entityCategory)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
|
|
||||||
const entityId = (ev.currentTarget as any).entityId;
|
|
||||||
let newVal: boolean | null = null;
|
|
||||||
switch (ev.detail.index) {
|
|
||||||
case 0:
|
|
||||||
newVal = true;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
newVal = false;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
newVal = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await this._updateExposed(entityId, newVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _updateExposed(entityId: string, newExposed: boolean | null) {
|
|
||||||
await this._updateConfig(entityId, {
|
|
||||||
should_expose: newExposed,
|
|
||||||
});
|
|
||||||
this._ensureEntitySync();
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _updateConfig(entityId: string, values: AlexaEntityConfig) {
|
|
||||||
const updatedConfig = await updateCloudAlexaEntityConfig(
|
|
||||||
this.hass,
|
|
||||||
entityId,
|
|
||||||
values
|
|
||||||
);
|
|
||||||
this._entityConfigs = {
|
|
||||||
...this._entityConfigs,
|
|
||||||
[entityId]: updatedConfig,
|
|
||||||
};
|
|
||||||
this._ensureStatusReload();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _openDomainToggler() {
|
|
||||||
showDomainTogglerDialog(this, {
|
|
||||||
title: this.hass!.localize("ui.panel.config.cloud.alexa.manage_defaults"),
|
|
||||||
description: this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.manage_defaults_dialog_description"
|
|
||||||
),
|
|
||||||
domains: this._entities!.map((entity) =>
|
|
||||||
computeDomain(entity.entity_id)
|
|
||||||
).filter((value, idx, self) => self.indexOf(value) === idx),
|
|
||||||
exposedDomains: this.cloudStatus.prefs.alexa_default_expose,
|
|
||||||
toggleDomain: (domain, expose) => {
|
|
||||||
this._updateDomainExposed(domain, expose);
|
|
||||||
},
|
|
||||||
resetDomain: (domain) => {
|
|
||||||
this._entities!.forEach((entity) => {
|
|
||||||
if (computeDomain(entity.entity_id) === domain) {
|
|
||||||
this._updateExposed(entity.entity_id, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleSync() {
|
|
||||||
this._syncing = true;
|
|
||||||
try {
|
|
||||||
await syncCloudAlexaEntities(this.hass!);
|
|
||||||
} catch (err: any) {
|
|
||||||
alert(
|
|
||||||
`${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.alexa.sync_entities_error"
|
|
||||||
)} ${err.body.message}`
|
|
||||||
);
|
|
||||||
} finally {
|
|
||||||
this._syncing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _updateDomainExposed(domain: string, expose: boolean) {
|
|
||||||
const defaultExpose =
|
|
||||||
this.cloudStatus.prefs.alexa_default_expose ||
|
|
||||||
this._entities!.map((entity) => computeDomain(entity.entity_id)).filter(
|
|
||||||
(value, idx, self) => self.indexOf(value) === idx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
(expose && defaultExpose.includes(domain)) ||
|
|
||||||
(!expose && !defaultExpose.includes(domain))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expose) {
|
|
||||||
defaultExpose.push(domain);
|
|
||||||
} else {
|
|
||||||
defaultExpose.splice(defaultExpose.indexOf(domain), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateCloudPref(this.hass!, {
|
|
||||||
alexa_default_expose: defaultExpose,
|
|
||||||
});
|
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _ensureStatusReload() {
|
|
||||||
if (this._popstateReloadStatusAttached) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._popstateReloadStatusAttached = true;
|
|
||||||
// Cache parent because by the time popstate happens,
|
|
||||||
// this element is detached
|
|
||||||
const parent = this.parentElement!;
|
|
||||||
window.addEventListener(
|
|
||||||
"popstate",
|
|
||||||
() => fireEvent(parent, "ha-refresh-cloud-status"),
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private _ensureEntitySync() {
|
|
||||||
if (this._popstateSyncAttached) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._popstateSyncAttached = true;
|
|
||||||
// Cache parent because by the time popstate happens,
|
|
||||||
// this element is detached
|
|
||||||
window.addEventListener(
|
|
||||||
"popstate",
|
|
||||||
() => {
|
|
||||||
// We don't have anything yet.
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
css`
|
|
||||||
mwc-list-item > [slot="meta"] {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
.banner {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
background-color: var(
|
|
||||||
--ha-card-background,
|
|
||||||
var(--card-background-color, white)
|
|
||||||
);
|
|
||||||
padding: 16px 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
grid-gap: 8px 8px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
padding-bottom: 12px;
|
|
||||||
}
|
|
||||||
state-info {
|
|
||||||
cursor: pointer;
|
|
||||||
height: 40px;
|
|
||||||
}
|
|
||||||
ha-switch {
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
.top-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
background: var(--app-header-background-color);
|
|
||||||
}
|
|
||||||
.header.second {
|
|
||||||
border-top: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
.exposed {
|
|
||||||
color: var(--success-color);
|
|
||||||
}
|
|
||||||
.not-exposed {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
@media all and (max-width: 450px) {
|
|
||||||
ha-card {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"cloud-alexa": CloudAlexa;
|
|
||||||
}
|
|
||||||
}
|
|
@ -1,727 +0,0 @@
|
|||||||
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
|
|
||||||
import "@material/mwc-list/mwc-list-item";
|
|
||||||
import {
|
|
||||||
mdiCheckboxMarked,
|
|
||||||
mdiCheckboxMultipleMarked,
|
|
||||||
mdiCloseBox,
|
|
||||||
mdiCloseBoxMultiple,
|
|
||||||
mdiDotsVertical,
|
|
||||||
mdiFormatListChecks,
|
|
||||||
mdiSync,
|
|
||||||
} from "@mdi/js";
|
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
|
||||||
import { customElement, property, state } from "lit/decorators";
|
|
||||||
import { classMap } from "lit/directives/class-map";
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
|
||||||
import { computeDomain } from "../../../../common/entity/compute_domain";
|
|
||||||
import { computeStateName } from "../../../../common/entity/compute_state_name";
|
|
||||||
import {
|
|
||||||
EntityFilter,
|
|
||||||
generateFilter,
|
|
||||||
isEmptyFilter,
|
|
||||||
} from "../../../../common/entity/entity_filter";
|
|
||||||
import { stringCompare } from "../../../../common/string/compare";
|
|
||||||
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
|
|
||||||
import "../../../../components/entity/state-info";
|
|
||||||
import "../../../../components/ha-button-menu";
|
|
||||||
import "../../../../components/ha-card";
|
|
||||||
import "../../../../components/ha-formfield";
|
|
||||||
import "../../../../components/ha-icon-button";
|
|
||||||
import "../../../../components/ha-switch";
|
|
||||||
import type { HaSwitch } from "../../../../components/ha-switch";
|
|
||||||
import {
|
|
||||||
CloudPreferences,
|
|
||||||
CloudStatusLoggedIn,
|
|
||||||
cloudSyncGoogleAssistant,
|
|
||||||
GoogleEntityConfig,
|
|
||||||
updateCloudGoogleEntityConfig,
|
|
||||||
updateCloudPref,
|
|
||||||
} from "../../../../data/cloud";
|
|
||||||
import {
|
|
||||||
EntityRegistryEntry,
|
|
||||||
ExtEntityRegistryEntry,
|
|
||||||
getExtendedEntityRegistryEntries,
|
|
||||||
updateEntityRegistryEntry,
|
|
||||||
} from "../../../../data/entity_registry";
|
|
||||||
import {
|
|
||||||
fetchCloudGoogleEntities,
|
|
||||||
GoogleEntity,
|
|
||||||
} from "../../../../data/google_assistant";
|
|
||||||
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
|
|
||||||
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
|
|
||||||
import "../../../../layouts/hass-loading-screen";
|
|
||||||
import "../../../../layouts/hass-subpage";
|
|
||||||
import { buttonLinkStyle, haStyle } from "../../../../resources/styles";
|
|
||||||
import type { HomeAssistant } from "../../../../types";
|
|
||||||
import { showToast } from "../../../../util/toast";
|
|
||||||
import { showAliasesDialog } from "../../../../dialogs/aliases/show-dialog-aliases";
|
|
||||||
|
|
||||||
const DEFAULT_CONFIG_EXPOSE = true;
|
|
||||||
|
|
||||||
@customElement("cloud-google-assistant")
|
|
||||||
class CloudGoogleAssistant extends LitElement {
|
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
|
||||||
|
|
||||||
@property() public cloudStatus!: CloudStatusLoggedIn;
|
|
||||||
|
|
||||||
@property() public narrow!: boolean;
|
|
||||||
|
|
||||||
@state() private _entities?: GoogleEntity[];
|
|
||||||
|
|
||||||
@state() private _entries?: { [id: string]: ExtEntityRegistryEntry };
|
|
||||||
|
|
||||||
@state() private _syncing = false;
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private _entityConfigs: CloudPreferences["google_entity_configs"] = {};
|
|
||||||
|
|
||||||
@state()
|
|
||||||
private _entityCategories?: Record<
|
|
||||||
string,
|
|
||||||
EntityRegistryEntry["entity_category"]
|
|
||||||
>;
|
|
||||||
|
|
||||||
private _popstateSyncAttached = false;
|
|
||||||
|
|
||||||
private _popstateReloadStatusAttached = false;
|
|
||||||
|
|
||||||
private _isInitialExposed?: Set<string>;
|
|
||||||
|
|
||||||
private _getEntityFilterFunc = memoizeOne((filter: EntityFilter) =>
|
|
||||||
generateFilter(
|
|
||||||
filter.include_domains,
|
|
||||||
filter.include_entities,
|
|
||||||
filter.exclude_domains,
|
|
||||||
filter.exclude_entities
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
|
||||||
if (this._entities === undefined || this._entityCategories === undefined) {
|
|
||||||
return html` <hass-loading-screen></hass-loading-screen> `;
|
|
||||||
}
|
|
||||||
const emptyFilter = isEmptyFilter(this.cloudStatus.google_entities);
|
|
||||||
const filterFunc = this._getEntityFilterFunc(
|
|
||||||
this.cloudStatus.google_entities
|
|
||||||
);
|
|
||||||
const dir = computeRTLDirection(this.hass!);
|
|
||||||
|
|
||||||
// We will only generate `isInitialExposed` during first render.
|
|
||||||
// On each subsequent render we will use the same set so that cards
|
|
||||||
// will not jump around when we change the exposed setting.
|
|
||||||
const showInExposed = this._isInitialExposed || new Set();
|
|
||||||
const trackExposed = this._isInitialExposed === undefined;
|
|
||||||
|
|
||||||
let selected = 0;
|
|
||||||
|
|
||||||
// On first render we decide which cards show in which category.
|
|
||||||
// That way cards won't jump around when changing values.
|
|
||||||
const exposedCards: TemplateResult[] = [];
|
|
||||||
const notExposedCards: TemplateResult[] = [];
|
|
||||||
|
|
||||||
this._entities.forEach((entity) => {
|
|
||||||
const stateObj = this.hass.states[entity.entity_id];
|
|
||||||
const config = this._entityConfigs[entity.entity_id] || {
|
|
||||||
should_expose: null,
|
|
||||||
};
|
|
||||||
const isExposed = emptyFilter
|
|
||||||
? this._configIsExposed(
|
|
||||||
entity.entity_id,
|
|
||||||
config,
|
|
||||||
this._entityCategories![entity.entity_id]
|
|
||||||
)
|
|
||||||
: filterFunc(entity.entity_id);
|
|
||||||
const isDomainExposed = emptyFilter
|
|
||||||
? this._configIsDomainExposed(
|
|
||||||
entity.entity_id,
|
|
||||||
this._entityCategories![entity.entity_id]
|
|
||||||
)
|
|
||||||
: filterFunc(entity.entity_id);
|
|
||||||
if (isExposed) {
|
|
||||||
selected++;
|
|
||||||
|
|
||||||
if (trackExposed) {
|
|
||||||
showInExposed.add(entity.entity_id);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const target = showInExposed.has(entity.entity_id)
|
|
||||||
? exposedCards
|
|
||||||
: notExposedCards;
|
|
||||||
|
|
||||||
const iconButton = html`<ha-icon-button
|
|
||||||
slot="trigger"
|
|
||||||
class=${classMap({
|
|
||||||
exposed: isExposed!,
|
|
||||||
"not-exposed": !isExposed,
|
|
||||||
})}
|
|
||||||
.disabled=${!emptyFilter}
|
|
||||||
.label=${this.hass!.localize("ui.panel.config.cloud.google.expose")}
|
|
||||||
.path=${config.should_expose !== null
|
|
||||||
? isExposed
|
|
||||||
? mdiCheckboxMarked
|
|
||||||
: mdiCloseBox
|
|
||||||
: isDomainExposed
|
|
||||||
? mdiCheckboxMultipleMarked
|
|
||||||
: mdiCloseBoxMultiple}
|
|
||||||
></ha-icon-button>`;
|
|
||||||
|
|
||||||
const aliases = this._entries?.[entity.entity_id]?.aliases;
|
|
||||||
|
|
||||||
target.push(html`
|
|
||||||
<ha-card outlined>
|
|
||||||
<div class="card-content">
|
|
||||||
<div class="top-line">
|
|
||||||
<state-info
|
|
||||||
.hass=${this.hass}
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
secondary-line
|
|
||||||
@click=${this._showMoreInfo}
|
|
||||||
>
|
|
||||||
${aliases
|
|
||||||
? html`
|
|
||||||
<span>
|
|
||||||
${aliases.length > 0
|
|
||||||
? [...aliases]
|
|
||||||
.sort((a, b) =>
|
|
||||||
stringCompare(a, b, this.hass.locale.language)
|
|
||||||
)
|
|
||||||
.join(", ")
|
|
||||||
: this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.google.no_aliases"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<button
|
|
||||||
class="link"
|
|
||||||
.entityId=${entity.entity_id}
|
|
||||||
@click=${this._openAliasesSettings}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
`ui.panel.config.cloud.google.${
|
|
||||||
aliases.length > 0
|
|
||||||
? "manage_aliases"
|
|
||||||
: "add_aliases"
|
|
||||||
}`
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
<span>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.google.aliases_not_available"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<br />
|
|
||||||
<button
|
|
||||||
class="link"
|
|
||||||
.stateObj=${stateObj}
|
|
||||||
@click=${this._showMoreInfoSettings}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.google.aliases_not_available_learn_more"
|
|
||||||
)}
|
|
||||||
</button>
|
|
||||||
`}
|
|
||||||
</state-info>
|
|
||||||
${!emptyFilter
|
|
||||||
? html`${iconButton}`
|
|
||||||
: html`<ha-button-menu
|
|
||||||
.entityId=${entity.entity_id}
|
|
||||||
@action=${this._exposeChanged}
|
|
||||||
>
|
|
||||||
${iconButton}
|
|
||||||
<mwc-list-item hasMeta>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.expose_entity"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
class="exposed"
|
|
||||||
slot="meta"
|
|
||||||
.path=${mdiCheckboxMarked}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
<mwc-list-item hasMeta>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.dont_expose_entity"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
class="not-exposed"
|
|
||||||
slot="meta"
|
|
||||||
.path=${mdiCloseBox}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
<mwc-list-item hasMeta>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.follow_domain"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
class=${classMap({
|
|
||||||
exposed: isDomainExposed,
|
|
||||||
"not-exposed": !isDomainExposed,
|
|
||||||
})}
|
|
||||||
slot="meta"
|
|
||||||
.path=${isDomainExposed
|
|
||||||
? mdiCheckboxMultipleMarked
|
|
||||||
: mdiCloseBoxMultiple}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
</ha-button-menu>`}
|
|
||||||
</div>
|
|
||||||
${entity.might_2fa
|
|
||||||
? html`
|
|
||||||
<div>
|
|
||||||
<ha-formfield
|
|
||||||
.label=${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.disable_2FA"
|
|
||||||
)}
|
|
||||||
.dir=${dir}
|
|
||||||
>
|
|
||||||
<ha-switch
|
|
||||||
.entityId=${entity.entity_id}
|
|
||||||
.checked=${Boolean(config.disable_2fa)}
|
|
||||||
@change=${this._disable2FAChanged}
|
|
||||||
></ha-switch>
|
|
||||||
</ha-formfield>
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
</div>
|
|
||||||
</ha-card>
|
|
||||||
`);
|
|
||||||
});
|
|
||||||
|
|
||||||
if (trackExposed) {
|
|
||||||
this._isInitialExposed = showInExposed;
|
|
||||||
}
|
|
||||||
|
|
||||||
return html`
|
|
||||||
<hass-subpage
|
|
||||||
.hass=${this.hass}
|
|
||||||
.header=${this.hass!.localize("ui.panel.config.cloud.google.title")}
|
|
||||||
.narrow=${this.narrow}>
|
|
||||||
<ha-button-menu slot="toolbar-icon">
|
|
||||||
<ha-icon-button
|
|
||||||
slot="trigger"
|
|
||||||
.label=${this.hass.localize("ui.common.menu")}
|
|
||||||
.path=${mdiDotsVertical}
|
|
||||||
></ha-icon-button>
|
|
||||||
|
|
||||||
<mwc-list-item
|
|
||||||
graphic="icon"
|
|
||||||
.disabled=${!emptyFilter}
|
|
||||||
@click=${this._openDomainToggler}
|
|
||||||
>
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.panel.config.cloud.google.manage_defaults"
|
|
||||||
)}
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiFormatListChecks}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
|
|
||||||
<mwc-list-item
|
|
||||||
graphic="icon"
|
|
||||||
.disabled=${this._syncing}
|
|
||||||
@click=${this._handleSync}
|
|
||||||
>
|
|
||||||
${this.hass.localize("ui.panel.config.cloud.google.sync_entities")}
|
|
||||||
<ha-svg-icon
|
|
||||||
slot="graphic"
|
|
||||||
.path=${mdiSync}
|
|
||||||
></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
</ha-button-menu>
|
|
||||||
${
|
|
||||||
!emptyFilter
|
|
||||||
? html`
|
|
||||||
<div class="banner">
|
|
||||||
${this.hass!.localize("ui.panel.config.cloud.google.banner")}
|
|
||||||
</div>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
exposedCards.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="header">
|
|
||||||
<h3>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.exposed_entities"
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
${!this.narrow
|
|
||||||
? this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.exposed",
|
|
||||||
"selected",
|
|
||||||
selected
|
|
||||||
)
|
|
||||||
: selected}
|
|
||||||
</div>
|
|
||||||
<div class="content">${exposedCards}</div>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
${
|
|
||||||
notExposedCards.length > 0
|
|
||||||
? html`
|
|
||||||
<div class="header second">
|
|
||||||
<h3>
|
|
||||||
${this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.not_exposed_entities"
|
|
||||||
)}
|
|
||||||
</h3>
|
|
||||||
${!this.narrow
|
|
||||||
? this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.not_exposed",
|
|
||||||
"selected",
|
|
||||||
this._entities.length - selected
|
|
||||||
)
|
|
||||||
: this._entities.length - selected}
|
|
||||||
</div>
|
|
||||||
<div class="content">${notExposedCards}</div>
|
|
||||||
`
|
|
||||||
: ""
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</hass-subpage>
|
|
||||||
`;
|
|
||||||
}
|
|
||||||
|
|
||||||
protected firstUpdated(changedProps) {
|
|
||||||
super.firstUpdated(changedProps);
|
|
||||||
this._fetchData();
|
|
||||||
}
|
|
||||||
|
|
||||||
protected updated(changedProps) {
|
|
||||||
super.updated(changedProps);
|
|
||||||
if (changedProps.has("cloudStatus")) {
|
|
||||||
this._entityConfigs = this.cloudStatus.prefs.google_entity_configs;
|
|
||||||
}
|
|
||||||
if (
|
|
||||||
changedProps.has("hass") &&
|
|
||||||
changedProps.get("hass")?.entities !== this.hass.entities
|
|
||||||
) {
|
|
||||||
const categories = {};
|
|
||||||
|
|
||||||
for (const entry of Object.values(this.hass.entities)) {
|
|
||||||
categories[entry.entity_id] = entry.entity_category;
|
|
||||||
}
|
|
||||||
|
|
||||||
this._entityCategories = categories;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _openAliasesSettings(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const entityId = ev.target.entityId;
|
|
||||||
const entry = this._entries![entityId];
|
|
||||||
if (!entry) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
const stateObj = this.hass.states[entityId];
|
|
||||||
const name = (stateObj && computeStateName(stateObj)) || entityId;
|
|
||||||
|
|
||||||
showAliasesDialog(this, {
|
|
||||||
name,
|
|
||||||
aliases: entry.aliases,
|
|
||||||
updateAliases: async (aliases: string[]) => {
|
|
||||||
const result = await updateEntityRegistryEntry(this.hass, entityId, {
|
|
||||||
aliases,
|
|
||||||
});
|
|
||||||
this._entries![entityId] = result.entity_entry;
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private _configIsDomainExposed(
|
|
||||||
entityId: string,
|
|
||||||
entityCategory: EntityRegistryEntry["entity_category"] | undefined
|
|
||||||
) {
|
|
||||||
const domain = computeDomain(entityId);
|
|
||||||
return this.cloudStatus.prefs.google_default_expose
|
|
||||||
? !entityCategory &&
|
|
||||||
this.cloudStatus.prefs.google_default_expose.includes(domain)
|
|
||||||
: DEFAULT_CONFIG_EXPOSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _configIsExposed(
|
|
||||||
entityId: string,
|
|
||||||
config: GoogleEntityConfig,
|
|
||||||
entityCategory: EntityRegistryEntry["entity_category"] | undefined
|
|
||||||
) {
|
|
||||||
return (
|
|
||||||
config.should_expose ??
|
|
||||||
this._configIsDomainExposed(entityId, entityCategory)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _fetchData() {
|
|
||||||
const entities = await fetchCloudGoogleEntities(this.hass);
|
|
||||||
this._entries = await getExtendedEntityRegistryEntries(
|
|
||||||
this.hass,
|
|
||||||
entities
|
|
||||||
.filter((ent) => this.hass.entities[ent.entity_id])
|
|
||||||
.map((e) => e.entity_id)
|
|
||||||
);
|
|
||||||
|
|
||||||
entities.sort((a, b) => {
|
|
||||||
const stateA = this.hass.states[a.entity_id];
|
|
||||||
const stateB = this.hass.states[b.entity_id];
|
|
||||||
return stringCompare(
|
|
||||||
stateA ? computeStateName(stateA) : a.entity_id,
|
|
||||||
stateB ? computeStateName(stateB) : b.entity_id,
|
|
||||||
this.hass.locale.language
|
|
||||||
);
|
|
||||||
});
|
|
||||||
this._entities = entities;
|
|
||||||
}
|
|
||||||
|
|
||||||
private _showMoreInfo(ev) {
|
|
||||||
const entityId = ev.currentTarget.stateObj.entity_id;
|
|
||||||
fireEvent(this, "hass-more-info", { entityId });
|
|
||||||
}
|
|
||||||
|
|
||||||
private _showMoreInfoSettings(ev) {
|
|
||||||
ev.stopPropagation();
|
|
||||||
const entityId = ev.currentTarget.stateObj.entity_id;
|
|
||||||
fireEvent(this, "hass-more-info", { entityId, view: "settings" });
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
|
|
||||||
const entityId = (ev.currentTarget as any).entityId;
|
|
||||||
let newVal: boolean | null = null;
|
|
||||||
switch (ev.detail.index) {
|
|
||||||
case 0:
|
|
||||||
newVal = true;
|
|
||||||
break;
|
|
||||||
case 1:
|
|
||||||
newVal = false;
|
|
||||||
break;
|
|
||||||
case 2:
|
|
||||||
newVal = null;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
await this._updateExposed(entityId, newVal);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _updateExposed(entityId: string, newExposed: boolean | null) {
|
|
||||||
await this._updateConfig(entityId, {
|
|
||||||
should_expose: newExposed,
|
|
||||||
});
|
|
||||||
if (this.cloudStatus.google_registered) {
|
|
||||||
this._ensureEntitySync();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _disable2FAChanged(ev: Event) {
|
|
||||||
const entityId = (ev.currentTarget as any).entityId;
|
|
||||||
const newDisable2FA = (ev.target as HaSwitch).checked;
|
|
||||||
const curDisable2FA = Boolean(
|
|
||||||
(this._entityConfigs[entityId] || {}).disable_2fa
|
|
||||||
);
|
|
||||||
if (newDisable2FA === curDisable2FA) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await this._updateConfig(entityId, {
|
|
||||||
disable_2fa: newDisable2FA,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _updateConfig(entityId: string, values: GoogleEntityConfig) {
|
|
||||||
const updatedConfig = await updateCloudGoogleEntityConfig(
|
|
||||||
this.hass,
|
|
||||||
entityId,
|
|
||||||
values
|
|
||||||
);
|
|
||||||
this._entityConfigs = {
|
|
||||||
...this._entityConfigs,
|
|
||||||
[entityId]: updatedConfig,
|
|
||||||
};
|
|
||||||
this._ensureStatusReload();
|
|
||||||
}
|
|
||||||
|
|
||||||
private _openDomainToggler() {
|
|
||||||
showDomainTogglerDialog(this, {
|
|
||||||
title: this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.manage_defaults"
|
|
||||||
),
|
|
||||||
description: this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.manage_defaults_dialog_description"
|
|
||||||
),
|
|
||||||
domains: this._entities!.map((entity) =>
|
|
||||||
computeDomain(entity.entity_id)
|
|
||||||
).filter((value, idx, self) => self.indexOf(value) === idx),
|
|
||||||
exposedDomains: this.cloudStatus.prefs.google_default_expose,
|
|
||||||
toggleDomain: (domain, expose) => {
|
|
||||||
this._updateDomainExposed(domain, expose);
|
|
||||||
},
|
|
||||||
resetDomain: (domain) => {
|
|
||||||
this._entities!.forEach((entity) => {
|
|
||||||
if (computeDomain(entity.entity_id) === domain) {
|
|
||||||
this._updateExposed(entity.entity_id, null);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _updateDomainExposed(domain: string, expose: boolean) {
|
|
||||||
const defaultExpose =
|
|
||||||
this.cloudStatus.prefs.google_default_expose ||
|
|
||||||
this._entities!.map((entity) => computeDomain(entity.entity_id)).filter(
|
|
||||||
(value, idx, self) => self.indexOf(value) === idx
|
|
||||||
);
|
|
||||||
|
|
||||||
if (
|
|
||||||
(expose && defaultExpose.includes(domain)) ||
|
|
||||||
(!expose && !defaultExpose.includes(domain))
|
|
||||||
) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (expose) {
|
|
||||||
defaultExpose.push(domain);
|
|
||||||
} else {
|
|
||||||
defaultExpose.splice(defaultExpose.indexOf(domain), 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
await updateCloudPref(this.hass!, {
|
|
||||||
google_default_expose: defaultExpose,
|
|
||||||
});
|
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
|
||||||
}
|
|
||||||
|
|
||||||
private _ensureStatusReload() {
|
|
||||||
if (this._popstateReloadStatusAttached) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._popstateReloadStatusAttached = true;
|
|
||||||
// Cache parent because by the time popstate happens,
|
|
||||||
// this element is detached
|
|
||||||
const parent = this.parentElement!;
|
|
||||||
window.addEventListener(
|
|
||||||
"popstate",
|
|
||||||
() => fireEvent(parent, "ha-refresh-cloud-status"),
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
private async _handleSync() {
|
|
||||||
this._syncing = true;
|
|
||||||
try {
|
|
||||||
await cloudSyncGoogleAssistant(this.hass!);
|
|
||||||
} catch (err: any) {
|
|
||||||
showAlertDialog(this, {
|
|
||||||
title: this.hass.localize(
|
|
||||||
`ui.panel.config.cloud.google.${
|
|
||||||
err.status_code === 404
|
|
||||||
? "not_configured_title"
|
|
||||||
: "sync_failed_title"
|
|
||||||
}`
|
|
||||||
),
|
|
||||||
text: this.hass.localize(
|
|
||||||
`ui.panel.config.cloud.google.${
|
|
||||||
err.status_code === 404 ? "not_configured_text" : "sync_failed_text"
|
|
||||||
}`
|
|
||||||
),
|
|
||||||
});
|
|
||||||
fireEvent(this, "ha-refresh-cloud-status");
|
|
||||||
} finally {
|
|
||||||
this._syncing = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private _ensureEntitySync() {
|
|
||||||
if (this._popstateSyncAttached) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
this._popstateSyncAttached = true;
|
|
||||||
// Cache parent because by the time popstate happens,
|
|
||||||
// this element is detached
|
|
||||||
const parent = this.parentElement!;
|
|
||||||
window.addEventListener(
|
|
||||||
"popstate",
|
|
||||||
() => {
|
|
||||||
showToast(parent, {
|
|
||||||
message: this.hass!.localize(
|
|
||||||
"ui.panel.config.cloud.google.sync_to_google"
|
|
||||||
),
|
|
||||||
});
|
|
||||||
cloudSyncGoogleAssistant(this.hass);
|
|
||||||
},
|
|
||||||
{ once: true }
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
static get styles(): CSSResultGroup {
|
|
||||||
return [
|
|
||||||
haStyle,
|
|
||||||
buttonLinkStyle,
|
|
||||||
css`
|
|
||||||
mwc-list-item > [slot="meta"] {
|
|
||||||
margin-left: 4px;
|
|
||||||
}
|
|
||||||
.banner {
|
|
||||||
color: var(--primary-text-color);
|
|
||||||
background-color: var(
|
|
||||||
--ha-card-background,
|
|
||||||
var(--card-background-color, white)
|
|
||||||
);
|
|
||||||
padding: 16px 8px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
.content {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
|
|
||||||
grid-gap: 8px 8px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
|
||||||
.card-content {
|
|
||||||
padding-bottom: 12px;
|
|
||||||
}
|
|
||||||
state-info {
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
ha-switch {
|
|
||||||
padding: 8px 0;
|
|
||||||
}
|
|
||||||
.top-line {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
}
|
|
||||||
.header {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: space-between;
|
|
||||||
padding: 0 16px;
|
|
||||||
border-bottom: 1px solid var(--divider-color);
|
|
||||||
background: var(--app-header-background-color);
|
|
||||||
}
|
|
||||||
.header.second {
|
|
||||||
border-top: 1px solid var(--divider-color);
|
|
||||||
}
|
|
||||||
.exposed {
|
|
||||||
color: var(--success-color);
|
|
||||||
}
|
|
||||||
.not-exposed {
|
|
||||||
color: var(--error-color);
|
|
||||||
}
|
|
||||||
@media all and (max-width: 450px) {
|
|
||||||
ha-card {
|
|
||||||
max-width: 100%;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
`,
|
|
||||||
];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
declare global {
|
|
||||||
interface HTMLElementTagNameMap {
|
|
||||||
"cloud-google-assistant": CloudGoogleAssistant;
|
|
||||||
}
|
|
||||||
}
|
|
@ -56,14 +56,6 @@ class HaConfigCloud extends HassRouterPage {
|
|||||||
account: {
|
account: {
|
||||||
tag: "cloud-account",
|
tag: "cloud-account",
|
||||||
},
|
},
|
||||||
"google-assistant": {
|
|
||||||
tag: "cloud-google-assistant",
|
|
||||||
load: () => import("./google-assistant/cloud-google-assistant"),
|
|
||||||
},
|
|
||||||
alexa: {
|
|
||||||
tag: "cloud-alexa",
|
|
||||||
load: () => import("./alexa/cloud-alexa"),
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -8,7 +8,6 @@ import { blankBeforePercent } from "../../../common/translations/blank_before_pe
|
|||||||
import "../../../components/ha-card";
|
import "../../../components/ha-card";
|
||||||
import "../../../components/ha-icon-button";
|
import "../../../components/ha-icon-button";
|
||||||
import "../../../components/ha-navigation-list";
|
import "../../../components/ha-navigation-list";
|
||||||
import "../../../components/ha-tip";
|
|
||||||
import { BackupContent, fetchBackupInfo } from "../../../data/backup";
|
import { BackupContent, fetchBackupInfo } from "../../../data/backup";
|
||||||
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
|
import { CloudStatus, fetchCloudStatus } from "../../../data/cloud";
|
||||||
import { BOARD_NAMES, HardwareInfo } from "../../../data/hardware";
|
import { BOARD_NAMES, HardwareInfo } from "../../../data/hardware";
|
||||||
@ -270,9 +269,6 @@ class HaConfigSystemNavigation extends LitElement {
|
|||||||
ha-navigation-list {
|
ha-navigation-list {
|
||||||
--navigation-list-item-title-font-size: 16px;
|
--navigation-list-item-title-font-size: 16px;
|
||||||
}
|
}
|
||||||
ha-tip {
|
|
||||||
margin-bottom: max(env(safe-area-inset-bottom), 8px);
|
|
||||||
}
|
|
||||||
`,
|
`,
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
@ -1,15 +1,14 @@
|
|||||||
import "@material/mwc-button/mwc-button";
|
import "@material/mwc-button/mwc-button";
|
||||||
import "@material/mwc-formfield/mwc-formfield";
|
import "@material/mwc-formfield/mwc-formfield";
|
||||||
import "@material/mwc-list/mwc-list-item";
|
import "@material/mwc-list/mwc-list-item";
|
||||||
import { mdiPencil } from "@mdi/js";
|
|
||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
|
||||||
import {
|
import {
|
||||||
css,
|
css,
|
||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
|
||||||
nothing,
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@ -17,7 +16,6 @@ import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
|||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
import { stopPropagation } from "../../../common/dom/stop_propagation";
|
||||||
import { computeDomain } from "../../../common/entity/compute_domain";
|
import { computeDomain } from "../../../common/entity/compute_domain";
|
||||||
import { computeStateName } from "../../../common/entity/compute_state_name";
|
|
||||||
import { domainIcon } from "../../../common/entity/domain_icon";
|
import { domainIcon } from "../../../common/entity/domain_icon";
|
||||||
import { supportsFeature } from "../../../common/entity/supports-feature";
|
import { supportsFeature } from "../../../common/entity/supports-feature";
|
||||||
import { formatNumber } from "../../../common/number/format_number";
|
import { formatNumber } from "../../../common/number/format_number";
|
||||||
@ -30,6 +28,7 @@ import "../../../components/ha-alert";
|
|||||||
import "../../../components/ha-area-picker";
|
import "../../../components/ha-area-picker";
|
||||||
import "../../../components/ha-expansion-panel";
|
import "../../../components/ha-expansion-panel";
|
||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
|
import "../../../components/ha-icon-button-next";
|
||||||
import "../../../components/ha-icon-picker";
|
import "../../../components/ha-icon-picker";
|
||||||
import "../../../components/ha-radio";
|
import "../../../components/ha-radio";
|
||||||
import "../../../components/ha-select";
|
import "../../../components/ha-select";
|
||||||
@ -73,15 +72,15 @@ import { domainToName } from "../../../data/integration";
|
|||||||
import { getNumberDeviceClassConvertibleUnits } from "../../../data/number";
|
import { getNumberDeviceClassConvertibleUnits } from "../../../data/number";
|
||||||
import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor";
|
import { getSensorDeviceClassConvertibleUnits } from "../../../data/sensor";
|
||||||
import {
|
import {
|
||||||
WeatherUnits,
|
|
||||||
getWeatherConvertibleUnits,
|
getWeatherConvertibleUnits,
|
||||||
|
WeatherUnits,
|
||||||
} from "../../../data/weather";
|
} from "../../../data/weather";
|
||||||
import { showAliasesDialog } from "../../../dialogs/aliases/show-dialog-aliases";
|
|
||||||
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog-options-flow";
|
||||||
import {
|
import {
|
||||||
showAlertDialog,
|
showAlertDialog,
|
||||||
showConfirmationDialog,
|
showConfirmationDialog,
|
||||||
} from "../../../dialogs/generic/show-dialog-box";
|
} from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import { showVoiceAssistantsView } from "../../../dialogs/more-info/components/voice/show-view-voice-assistants";
|
||||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
import { haStyle } from "../../../resources/styles";
|
import { haStyle } from "../../../resources/styles";
|
||||||
@ -699,6 +698,23 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
@value-changed=${this._areaPicked}
|
@value-changed=${this._areaPicked}
|
||||||
></ha-area-picker>`
|
></ha-area-picker>`
|
||||||
: ""}
|
: ""}
|
||||||
|
<mwc-list class="aliases" @action=${this._handleVoiceAssistantsClicked}>
|
||||||
|
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
|
||||||
|
<span>Voice assistants</span>
|
||||||
|
<span slot="secondary">
|
||||||
|
${this.entry.aliases.length
|
||||||
|
? [...this.entry.aliases]
|
||||||
|
.sort((a, b) =>
|
||||||
|
stringCompare(a, b, this.hass.locale.language)
|
||||||
|
)
|
||||||
|
.join(", ")
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.dialogs.entity_registry.editor.no_aliases"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-icon-button-next slot="meta"></ha-icon-button-next>
|
||||||
|
</mwc-list-item>
|
||||||
|
</mwc-list>
|
||||||
${this._cameraPrefs
|
${this._cameraPrefs
|
||||||
? html`
|
? html`
|
||||||
<ha-settings-row>
|
<ha-settings-row>
|
||||||
@ -848,34 +864,6 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
</div>
|
</div>
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
|
|
||||||
<div class="label">
|
|
||||||
${this.hass.localize(
|
|
||||||
"ui.dialogs.entity_registry.editor.aliases_section"
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<mwc-list class="aliases" @action=${this._handleAliasesClicked}>
|
|
||||||
<mwc-list-item .twoline=${this.entry.aliases.length > 0} hasMeta>
|
|
||||||
<span>
|
|
||||||
${this.entry.aliases.length > 0
|
|
||||||
? this.hass.localize(
|
|
||||||
"ui.dialogs.entity_registry.editor.configured_aliases",
|
|
||||||
{ count: this.entry.aliases.length }
|
|
||||||
)
|
|
||||||
: this.hass.localize(
|
|
||||||
"ui.dialogs.entity_registry.editor.no_aliases"
|
|
||||||
)}
|
|
||||||
</span>
|
|
||||||
<span slot="secondary">
|
|
||||||
${[...this.entry.aliases]
|
|
||||||
.sort((a, b) =>
|
|
||||||
stringCompare(a, b, this.hass.locale.language)
|
|
||||||
)
|
|
||||||
.join(", ")}
|
|
||||||
</span>
|
|
||||||
<ha-svg-icon slot="meta" .path=${mdiPencil}></ha-svg-icon>
|
|
||||||
</mwc-list-item>
|
|
||||||
</mwc-list>
|
|
||||||
<div class="secondary">
|
<div class="secondary">
|
||||||
${this.hass.localize(
|
${this.hass.localize(
|
||||||
"ui.dialogs.entity_registry.editor.aliases_description"
|
"ui.dialogs.entity_registry.editor.aliases_description"
|
||||||
@ -1070,25 +1058,8 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
private _handleAliasesClicked(ev: CustomEvent) {
|
private _handleVoiceAssistantsClicked() {
|
||||||
if (ev.detail.index !== 0) return;
|
showVoiceAssistantsView(this, "Voice assistants");
|
||||||
|
|
||||||
const stateObj = this.hass.states[this.entry.entity_id];
|
|
||||||
const name =
|
|
||||||
(stateObj && computeStateName(stateObj)) || this.entry.entity_id;
|
|
||||||
|
|
||||||
showAliasesDialog(this, {
|
|
||||||
name,
|
|
||||||
aliases: this.entry!.aliases,
|
|
||||||
updateAliases: async (aliases: string[]) => {
|
|
||||||
const result = await updateEntityRegistryEntry(
|
|
||||||
this.hass,
|
|
||||||
this.entry.entity_id,
|
|
||||||
{ aliases }
|
|
||||||
);
|
|
||||||
fireEvent(this, "entity-entry-updated", result.entity_entry);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async _enableEntry() {
|
private async _enableEntry() {
|
||||||
|
@ -12,6 +12,7 @@ import {
|
|||||||
mdiMapMarkerRadius,
|
mdiMapMarkerRadius,
|
||||||
mdiMathLog,
|
mdiMathLog,
|
||||||
mdiMemory,
|
mdiMemory,
|
||||||
|
mdiMicrophone,
|
||||||
mdiNetwork,
|
mdiNetwork,
|
||||||
mdiNfcVariant,
|
mdiNfcVariant,
|
||||||
mdiPalette,
|
mdiPalette,
|
||||||
@ -82,6 +83,12 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
|||||||
iconColor: "#B1345C",
|
iconColor: "#B1345C",
|
||||||
component: "lovelace",
|
component: "lovelace",
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: "/config/voice-assistants",
|
||||||
|
translationKey: "voice_assistants",
|
||||||
|
iconPath: mdiMicrophone,
|
||||||
|
iconColor: "#3263C3",
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: "/config/tags",
|
path: "/config/tags",
|
||||||
translationKey: "tags",
|
translationKey: "tags",
|
||||||
@ -199,6 +206,14 @@ export const configSections: { [name: string]: PageNavigation[] } = {
|
|||||||
iconColor: "#616161",
|
iconColor: "#616161",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
voice_assistants: [
|
||||||
|
{
|
||||||
|
path: "/config/voice-assistants",
|
||||||
|
translationKey: "ui.panel.config.dashboard.voice_assistants.main",
|
||||||
|
iconPath: mdiMicrophone,
|
||||||
|
iconColor: "#3263C3",
|
||||||
|
},
|
||||||
|
],
|
||||||
// Not used as a tab, but this way it will stay in the quick bar
|
// Not used as a tab, but this way it will stay in the quick bar
|
||||||
energy: [
|
energy: [
|
||||||
{
|
{
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import { mdiViewDashboard } from "@mdi/js";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import {
|
import {
|
||||||
HassRouterPage,
|
HassRouterPage,
|
||||||
@ -10,7 +11,7 @@ export const lovelaceTabs = [
|
|||||||
component: "lovelace",
|
component: "lovelace",
|
||||||
path: "/config/lovelace/dashboards",
|
path: "/config/lovelace/dashboards",
|
||||||
translationKey: "ui.panel.config.lovelace.dashboards.caption",
|
translationKey: "ui.panel.config.lovelace.dashboards.caption",
|
||||||
icon: "hass:view-dashboard",
|
iconPath: mdiViewDashboard,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
272
src/panels/config/voice-assistants/cloud-alexa-pref.ts
Normal file
272
src/panels/config/voice-assistants/cloud-alexa-pref.ts
Normal file
@ -0,0 +1,272 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import { mdiHelpCircle } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { isEmptyFilter } from "../../../common/entity/entity_filter";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-settings-row";
|
||||||
|
import "../../../components/ha-switch";
|
||||||
|
import type { HaSwitch } from "../../../components/ha-switch";
|
||||||
|
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
|
||||||
|
import {
|
||||||
|
getExposeNewEntities,
|
||||||
|
setExposeNewEntities,
|
||||||
|
} from "../../../data/voice";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
|
||||||
|
export class CloudAlexaPref extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public cloudStatus?: CloudStatusLoggedIn;
|
||||||
|
|
||||||
|
@state() private _exposeNew?: boolean;
|
||||||
|
|
||||||
|
protected willUpdate() {
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
getExposeNewEntities(this.hass, "cloud.alexa").then((value) => {
|
||||||
|
this._exposeNew = value.expose_new;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.cloudStatus) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const alexa_registered = this.cloudStatus.alexa_registered;
|
||||||
|
const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs;
|
||||||
|
|
||||||
|
const manualConfig = !isEmptyFilter(this.cloudStatus.alexa_entities);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card outlined>
|
||||||
|
<h1 class="card-header">
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: "alexa",
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>${this.hass.localize("ui.panel.config.cloud.account.alexa.title")}
|
||||||
|
</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a
|
||||||
|
href="https://www.nabucasa.com/config/amazon_alexa/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="icon-link"
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.link_learn_how_it_works"
|
||||||
|
)}
|
||||||
|
.path=${mdiHelpCircle}
|
||||||
|
></ha-icon-button>
|
||||||
|
</a>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${alexa_enabled}
|
||||||
|
@change=${this._enabledToggleChanged}
|
||||||
|
></ha-switch>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<p>
|
||||||
|
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")}
|
||||||
|
</p>
|
||||||
|
${manualConfig
|
||||||
|
? html`<ha-alert alert-type="warning">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.manual_config"
|
||||||
|
)}
|
||||||
|
</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
${!alexa_enabled
|
||||||
|
? ""
|
||||||
|
: html`${!alexa_registered
|
||||||
|
? html`<ha-alert
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.not_configured_title"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.not_configured_text"
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://skills-store.amazon.com/deeplink/dp/B0772J1QKB?deviceType=app"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.enable_ha_skill"
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.nabucasa.com/config/amazon_alexa/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.config_documentation"
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ha-alert>`
|
||||||
|
: ""}<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.expose_new_entities"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.expose_new_entities_info"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${this._exposeNew}
|
||||||
|
.disabled=${this._exposeNew === undefined}
|
||||||
|
@change=${this._exposeNewToggleChanged}
|
||||||
|
></ha-switch> </ha-settings-row
|
||||||
|
>${alexa_registered
|
||||||
|
? html`
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.enable_state_reporting"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.info_state_reporting"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${alexa_report_state}
|
||||||
|
@change=${this._reportToggleChanged}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
`
|
||||||
|
: ""}`}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<a
|
||||||
|
href="/config/voice-assistants/expose?assistants=cloud.alexa&historyBack"
|
||||||
|
>
|
||||||
|
<mwc-button
|
||||||
|
>${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.manage_entities"
|
||||||
|
)}</mwc-button
|
||||||
|
>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _exposeNewToggleChanged(ev) {
|
||||||
|
const toggle = ev.target as HaSwitch;
|
||||||
|
if (this._exposeNew === undefined || this._exposeNew === toggle.checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await setExposeNewEntities(this.hass, "cloud.alexa", toggle.checked);
|
||||||
|
} catch (err: any) {
|
||||||
|
toggle.checked = !toggle.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _enabledToggleChanged(ev) {
|
||||||
|
const toggle = ev.target as HaSwitch;
|
||||||
|
try {
|
||||||
|
await updateCloudPref(this.hass!, { alexa_enabled: toggle.checked! });
|
||||||
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
|
} catch (err: any) {
|
||||||
|
toggle.checked = !toggle.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _reportToggleChanged(ev) {
|
||||||
|
const toggle = ev.target as HaSwitch;
|
||||||
|
try {
|
||||||
|
await updateCloudPref(this.hass!, {
|
||||||
|
alexa_report_state: toggle.checked!,
|
||||||
|
});
|
||||||
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(
|
||||||
|
`${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.alexa.state_reporting_error",
|
||||||
|
"enable_disable",
|
||||||
|
this.hass!.localize(
|
||||||
|
toggle.checked
|
||||||
|
? "ui.panel.config.cloud.account.alexa.enable"
|
||||||
|
: "ui.panel.config.cloud.account.alexa.disable"
|
||||||
|
)
|
||||||
|
)} ${err.message}`
|
||||||
|
);
|
||||||
|
toggle.checked = !toggle.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
.header-actions {
|
||||||
|
position: absolute;
|
||||||
|
right: 24px;
|
||||||
|
top: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
:host([dir="rtl"]) .header-actions {
|
||||||
|
right: auto;
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
.header-actions .icon-link {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
direction: var(--direction);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.card-actions a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
height: 28px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"cloud-alexa-pref": CloudAlexaPref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("cloud-alexa-pref", CloudAlexaPref);
|
352
src/panels/config/voice-assistants/cloud-google-pref.ts
Normal file
352
src/panels/config/voice-assistants/cloud-google-pref.ts
Normal file
@ -0,0 +1,352 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import { mdiHelpCircle } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import "../../../components/ha-alert";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/ha-settings-row";
|
||||||
|
import type { HaSwitch } from "../../../components/ha-switch";
|
||||||
|
import "../../../components/ha-textfield";
|
||||||
|
import type { HaTextField } from "../../../components/ha-textfield";
|
||||||
|
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
|
||||||
|
import { showSaveSuccessToast } from "../../../util/toast-saved-success";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
import { isEmptyFilter } from "../../../common/entity/entity_filter";
|
||||||
|
import {
|
||||||
|
getExposeNewEntities,
|
||||||
|
setExposeNewEntities,
|
||||||
|
} from "../../../data/voice";
|
||||||
|
|
||||||
|
export class CloudGooglePref extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
|
||||||
|
|
||||||
|
@state() private _exposeNew?: boolean;
|
||||||
|
|
||||||
|
protected willUpdate() {
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
getExposeNewEntities(this.hass, "cloud.google_assistant").then(
|
||||||
|
(value) => {
|
||||||
|
this._exposeNew = value.expose_new;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.cloudStatus) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
const google_registered = this.cloudStatus.google_registered;
|
||||||
|
const { google_enabled, google_report_state, google_secure_devices_pin } =
|
||||||
|
this.cloudStatus.prefs;
|
||||||
|
|
||||||
|
const manualConfig = !isEmptyFilter(this.cloudStatus.google_entities);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-card outlined>
|
||||||
|
<h1 class="card-header">
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: "google_assistant",
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>${this.hass.localize("ui.panel.config.cloud.account.google.title")}
|
||||||
|
</h1>
|
||||||
|
<div class="header-actions">
|
||||||
|
<a
|
||||||
|
href="https://www.nabucasa.com/config/google_assistant/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
class="icon-link"
|
||||||
|
>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.link_learn_how_it_works"
|
||||||
|
)}
|
||||||
|
.path=${mdiHelpCircle}
|
||||||
|
></ha-icon-button>
|
||||||
|
</a>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${google_enabled}
|
||||||
|
@change=${this._enabledToggleChanged}
|
||||||
|
></ha-switch>
|
||||||
|
</div>
|
||||||
|
<div class="card-content">
|
||||||
|
<p>
|
||||||
|
${this.hass.localize("ui.panel.config.cloud.account.google.info")}
|
||||||
|
</p>
|
||||||
|
${manualConfig
|
||||||
|
? html`<ha-alert alert-type="warning">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.manual_config"
|
||||||
|
)}
|
||||||
|
</ha-alert>`
|
||||||
|
: ""}
|
||||||
|
${!google_enabled
|
||||||
|
? ""
|
||||||
|
: html`${!google_registered
|
||||||
|
? html`
|
||||||
|
<ha-alert
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.not_configured_title"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.not_configured_text"
|
||||||
|
)}
|
||||||
|
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://assistant.google.com/services/a/uid/00000091fd5fb875?hl=en-US"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.enable_ha_skill"
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a
|
||||||
|
href="https://www.nabucasa.com/config/google_assistant/"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.config_documentation"
|
||||||
|
)}
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</ha-alert>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.expose_new_entities"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.expose_new_entities_info"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${this._exposeNew}
|
||||||
|
.disabled=${this._exposeNew === undefined}
|
||||||
|
@change=${this._exposeNewToggleChanged}
|
||||||
|
></ha-switch> </ha-settings-row
|
||||||
|
>${google_registered
|
||||||
|
? html`
|
||||||
|
${this.cloudStatus.http_use_ssl
|
||||||
|
? html`
|
||||||
|
<ha-alert
|
||||||
|
alert-type="warning"
|
||||||
|
.title=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.http_use_ssl_warning_title"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.http_use_ssl_warning_text"
|
||||||
|
)}
|
||||||
|
<a
|
||||||
|
href="https://www.nabucasa.com/config/google_assistant/#local-communication"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.common.learn_more"
|
||||||
|
)}</a
|
||||||
|
>
|
||||||
|
</ha-alert>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.enable_state_reporting"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hass!.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.info_state_reporting"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<ha-switch
|
||||||
|
.checked=${google_report_state}
|
||||||
|
@change=${this._reportToggleChanged}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
|
||||||
|
<ha-settings-row>
|
||||||
|
<span slot="heading">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.security_devices"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span slot="description">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.enter_pin_info"
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
</ha-settings-row>
|
||||||
|
|
||||||
|
<ha-textfield
|
||||||
|
id="google_secure_devices_pin"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.devices_pin"
|
||||||
|
)}
|
||||||
|
.placeholder=${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.enter_pin_hint"
|
||||||
|
)}
|
||||||
|
.value=${google_secure_devices_pin || ""}
|
||||||
|
@change=${this._pinChanged}
|
||||||
|
></ha-textfield>
|
||||||
|
`
|
||||||
|
: ""}`}
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<a
|
||||||
|
href="/config/voice-assistants/expose?assistants=cloud.google_assistant&historyBack"
|
||||||
|
>
|
||||||
|
<mwc-button>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.manage_entities"
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _exposeNewToggleChanged(ev) {
|
||||||
|
const toggle = ev.target as HaSwitch;
|
||||||
|
if (this._exposeNew === undefined || this._exposeNew === toggle.checked) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
await setExposeNewEntities(
|
||||||
|
this.hass,
|
||||||
|
"cloud.google_assistant",
|
||||||
|
toggle.checked
|
||||||
|
);
|
||||||
|
} catch (err: any) {
|
||||||
|
toggle.checked = !toggle.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _enabledToggleChanged(ev) {
|
||||||
|
const toggle = ev.target as HaSwitch;
|
||||||
|
try {
|
||||||
|
await updateCloudPref(this.hass, { google_enabled: toggle.checked! });
|
||||||
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
|
} catch (err: any) {
|
||||||
|
toggle.checked = !toggle.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _reportToggleChanged(ev) {
|
||||||
|
const toggle = ev.target as HaSwitch;
|
||||||
|
try {
|
||||||
|
await updateCloudPref(this.hass, {
|
||||||
|
google_report_state: toggle.checked!,
|
||||||
|
});
|
||||||
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(
|
||||||
|
`Unable to ${toggle.checked ? "enable" : "disable"} report state. ${
|
||||||
|
err.message
|
||||||
|
}`
|
||||||
|
);
|
||||||
|
toggle.checked = !toggle.checked;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _pinChanged(ev) {
|
||||||
|
const input = ev.target as HaTextField;
|
||||||
|
try {
|
||||||
|
await updateCloudPref(this.hass, {
|
||||||
|
[input.id]: input.value || null,
|
||||||
|
});
|
||||||
|
showSaveSuccessToast(this, this.hass);
|
||||||
|
fireEvent(this, "ha-refresh-cloud-status");
|
||||||
|
} catch (err: any) {
|
||||||
|
alert(
|
||||||
|
`${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.account.google.enter_pin_error"
|
||||||
|
)} ${err.message}`
|
||||||
|
);
|
||||||
|
input.value = this.cloudStatus!.prefs.google_secure_devices_pin || "";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return css`
|
||||||
|
a {
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
.header-actions {
|
||||||
|
position: absolute;
|
||||||
|
right: 24px;
|
||||||
|
top: 24px;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
:host([dir="rtl"]) .header-actions {
|
||||||
|
right: auto;
|
||||||
|
left: 24px;
|
||||||
|
}
|
||||||
|
.header-actions .icon-link {
|
||||||
|
margin-top: -16px;
|
||||||
|
margin-inline-end: 8px;
|
||||||
|
margin-right: 8px;
|
||||||
|
direction: var(--direction);
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
ha-textfield {
|
||||||
|
width: 250px;
|
||||||
|
display: block;
|
||||||
|
margin-top: 8px;
|
||||||
|
}
|
||||||
|
.card-actions {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.card-actions a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.warning {
|
||||||
|
color: var(--error-color);
|
||||||
|
}
|
||||||
|
.card-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
height: 28px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"cloud-google-pref": CloudGooglePref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("cloud-google-pref", CloudGooglePref);
|
218
src/panels/config/voice-assistants/dialog-expose-entity.ts
Normal file
218
src/panels/config/voice-assistants/dialog-expose-entity.ts
Normal file
@ -0,0 +1,218 @@
|
|||||||
|
import "@material/mwc-button";
|
||||||
|
import "@material/mwc-list";
|
||||||
|
import { mdiClose } from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import "../../../components/ha-check-list-item";
|
||||||
|
import "../../../components/search-input";
|
||||||
|
import {
|
||||||
|
computeEntityRegistryName,
|
||||||
|
ExtEntityRegistryEntry,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import "./entity-voice-settings";
|
||||||
|
import { ExposeEntityDialogParams } from "./show-dialog-expose-entity";
|
||||||
|
|
||||||
|
@customElement("dialog-expose-entity")
|
||||||
|
class DialogExposeEntity extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _params?: ExposeEntityDialogParams;
|
||||||
|
|
||||||
|
@state() private _filter?: string;
|
||||||
|
|
||||||
|
@state() private _selected: string[] = [];
|
||||||
|
|
||||||
|
public async showDialog(params: ExposeEntityDialogParams): Promise<void> {
|
||||||
|
this._params = params;
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._params = undefined;
|
||||||
|
this._selected = [];
|
||||||
|
this._filter = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._params) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
.heading=${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose_dialog.header"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div slot="heading">
|
||||||
|
<h2 class="header">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose_dialog.header"
|
||||||
|
)}
|
||||||
|
</h2>
|
||||||
|
<ha-icon-button
|
||||||
|
.label=${this.hass.localize("ui.dialogs.generic.close")}
|
||||||
|
.path=${mdiClose}
|
||||||
|
dialogAction="close"
|
||||||
|
class="header_button"
|
||||||
|
></ha-icon-button>
|
||||||
|
<search-input
|
||||||
|
.hass=${this.hass}
|
||||||
|
.filter=${this._filter}
|
||||||
|
@value-changed=${this._filterChanged}
|
||||||
|
></search-input>
|
||||||
|
</div>
|
||||||
|
<mwc-list multi>
|
||||||
|
${this._filterEntities(
|
||||||
|
this._params.extendedEntities,
|
||||||
|
this._filter
|
||||||
|
).map((entity) => this._renderItem(entity))}
|
||||||
|
</mwc-list>
|
||||||
|
<mwc-button
|
||||||
|
slot="primaryAction"
|
||||||
|
@click=${this._expose}
|
||||||
|
.disabled=${this._selected.length === 0}
|
||||||
|
>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose_dialog.expose_entities",
|
||||||
|
{ count: this._selected.length }
|
||||||
|
)}
|
||||||
|
</mwc-button>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSelected(ev) {
|
||||||
|
if (ev.detail.source !== "property") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const entityId = ev.target.value;
|
||||||
|
if (ev.detail.selected) {
|
||||||
|
if (this._selected.includes(entityId)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._selected = [...this._selected, entityId];
|
||||||
|
} else {
|
||||||
|
this._selected = this._selected.filter((item) => item !== entityId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterChanged(e) {
|
||||||
|
this._filter = e.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _filterEntities = memoizeOne(
|
||||||
|
(RegEntries: Record<string, ExtEntityRegistryEntry>, filter?: string) =>
|
||||||
|
Object.values(RegEntries).filter(
|
||||||
|
(entity) =>
|
||||||
|
this._params!.filterAssistants.some(
|
||||||
|
(ass) => !entity.options?.[ass]?.should_expose
|
||||||
|
) &&
|
||||||
|
(!filter ||
|
||||||
|
entity.entity_id.includes(filter) ||
|
||||||
|
computeEntityRegistryName(this.hass!, entity)?.includes(filter))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
private _renderItem = (entity: ExtEntityRegistryEntry) => {
|
||||||
|
const entityState = this.hass.states[entity.entity_id];
|
||||||
|
return html`<ha-check-list-item
|
||||||
|
graphic="icon"
|
||||||
|
.value=${entity.entity_id}
|
||||||
|
.selected=${this._selected.includes(entity.entity_id)}
|
||||||
|
@request-selected=${this._handleSelected}
|
||||||
|
>
|
||||||
|
<ha-state-icon
|
||||||
|
title=${ifDefined(entityState?.state)}
|
||||||
|
slot="graphic"
|
||||||
|
.state=${entityState}
|
||||||
|
></ha-state-icon>
|
||||||
|
${computeEntityRegistryName(this.hass!, entity)}
|
||||||
|
</ha-check-list-item>`;
|
||||||
|
};
|
||||||
|
|
||||||
|
private _expose() {
|
||||||
|
this._params!.exposeEntities(this._selected);
|
||||||
|
this.closeDialog();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-dialog {
|
||||||
|
--dialog-content-padding: 0;
|
||||||
|
}
|
||||||
|
search-input {
|
||||||
|
width: 100%;
|
||||||
|
display: block;
|
||||||
|
padding: 24px 16px 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.header {
|
||||||
|
pointer-events: auto;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
font-family: var(
|
||||||
|
--mdc-typography-headline6-font-family,
|
||||||
|
var(--mdc-typography-font-family, Roboto, sans-serif)
|
||||||
|
);
|
||||||
|
font-size: var(--mdc-typography-headline6-font-size, 1.25rem);
|
||||||
|
line-height: var(--mdc-typography-headline6-line-height, 2rem);
|
||||||
|
font-weight: var(--mdc-typography-headline6-font-weight, 500);
|
||||||
|
letter-spacing: var(
|
||||||
|
--mdc-typography-headline6-letter-spacing,
|
||||||
|
0.0125em
|
||||||
|
);
|
||||||
|
text-decoration: var(
|
||||||
|
--mdc-typography-headline6-text-decoration,
|
||||||
|
inherit
|
||||||
|
);
|
||||||
|
text-transform: var(
|
||||||
|
--mdc-typography-headline6-text-transform,
|
||||||
|
inherit
|
||||||
|
);
|
||||||
|
display: block;
|
||||||
|
position: relative;
|
||||||
|
flex-shrink: 0;
|
||||||
|
box-sizing: border-box;
|
||||||
|
margin: 0 0 1px;
|
||||||
|
padding: 24px 24px 0 24px;
|
||||||
|
padding-bottom: 15px;
|
||||||
|
color: var(--mdc-dialog-heading-ink-color, rgba(0, 0, 0, 0.87));
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.12);
|
||||||
|
margin-bottom: 0;
|
||||||
|
border-color: var(
|
||||||
|
--mdc-dialog-scroll-divider-color,
|
||||||
|
rgba(0, 0, 0, 0.12)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.header_button {
|
||||||
|
position: absolute;
|
||||||
|
right: 16px;
|
||||||
|
top: 14px;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
.header_button {
|
||||||
|
inset-inline-start: initial;
|
||||||
|
inset-inline-end: 16px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-expose-entity": DialogExposeEntity;
|
||||||
|
}
|
||||||
|
}
|
82
src/panels/config/voice-assistants/dialog-voice-settings.ts
Normal file
82
src/panels/config/voice-assistants/dialog-voice-settings.ts
Normal file
@ -0,0 +1,82 @@
|
|||||||
|
import "@material/mwc-button/mwc-button";
|
||||||
|
import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
|
||||||
|
import { customElement, property, state } from "lit/decorators";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
ExtEntityRegistryEntry,
|
||||||
|
computeEntityRegistryName,
|
||||||
|
getExtendedEntityRegistryEntry,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
|
import { haStyle, haStyleDialog } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { VoiceSettingsDialogParams } from "./show-dialog-voice-settings";
|
||||||
|
import "./entity-voice-settings";
|
||||||
|
import { createCloseHeading } from "../../../components/ha-dialog";
|
||||||
|
|
||||||
|
@customElement("dialog-voice-settings")
|
||||||
|
class DialogVoiceSettings extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@state() private _extEntityReg?: ExtEntityRegistryEntry;
|
||||||
|
|
||||||
|
public async showDialog(params: VoiceSettingsDialogParams): Promise<void> {
|
||||||
|
this._extEntityReg = await getExtendedEntityRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
params.entityId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public closeDialog(): void {
|
||||||
|
this._extEntityReg = undefined;
|
||||||
|
fireEvent(this, "dialog-closed", { dialog: this.localName });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this._extEntityReg) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<ha-dialog
|
||||||
|
open
|
||||||
|
@closed=${this.closeDialog}
|
||||||
|
hideActions
|
||||||
|
.heading=${createCloseHeading(
|
||||||
|
this.hass,
|
||||||
|
computeEntityRegistryName(this.hass, this._extEntityReg) ||
|
||||||
|
"Unnamed entity"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<entity-voice-settings
|
||||||
|
.hass=${this.hass}
|
||||||
|
.entry=${this._extEntityReg}
|
||||||
|
@entity-entry-updated=${this._entityEntryUpdated}
|
||||||
|
></entity-voice-settings>
|
||||||
|
</div>
|
||||||
|
</ha-dialog>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _entityEntryUpdated(ev: CustomEvent) {
|
||||||
|
this._extEntityReg = ev.detail;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
haStyleDialog,
|
||||||
|
css`
|
||||||
|
ha-dialog {
|
||||||
|
--dialog-content-padding: 0;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"dialog-voice-settings": DialogVoiceSettings;
|
||||||
|
}
|
||||||
|
}
|
327
src/panels/config/voice-assistants/entity-voice-settings.ts
Normal file
327
src/panels/config/voice-assistants/entity-voice-settings.ts
Normal file
@ -0,0 +1,327 @@
|
|||||||
|
import { css, CSSResultGroup, html, LitElement, PropertyValues } 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 {
|
||||||
|
EntityFilter,
|
||||||
|
FilterFunc,
|
||||||
|
generateFilter,
|
||||||
|
isEmptyFilter,
|
||||||
|
} from "../../../common/entity/entity_filter";
|
||||||
|
import "../../../components/ha-aliases-editor";
|
||||||
|
import "../../../components/ha-settings-row";
|
||||||
|
import "../../../components/ha-switch";
|
||||||
|
import {
|
||||||
|
CloudStatus,
|
||||||
|
CloudStatusLoggedIn,
|
||||||
|
fetchCloudStatus,
|
||||||
|
updateCloudGoogleEntityConfig,
|
||||||
|
} from "../../../data/cloud";
|
||||||
|
import {
|
||||||
|
ExtEntityRegistryEntry,
|
||||||
|
getExtendedEntityRegistryEntry,
|
||||||
|
updateEntityRegistryEntry,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
|
import {
|
||||||
|
GoogleEntity,
|
||||||
|
fetchCloudGoogleEntity,
|
||||||
|
} from "../../../data/google_assistant";
|
||||||
|
import {
|
||||||
|
exposeEntities,
|
||||||
|
voiceAssistantKeys,
|
||||||
|
voiceAssistants,
|
||||||
|
} from "../../../data/voice";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import type { HomeAssistant } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
import { EntityRegistrySettings } from "../entities/entity-registry-settings";
|
||||||
|
|
||||||
|
@customElement("entity-voice-settings")
|
||||||
|
export class EntityVoiceSettings extends SubscribeMixin(LitElement) {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ type: Object }) public entry!: ExtEntityRegistryEntry;
|
||||||
|
|
||||||
|
@state() private _cloudStatus?: CloudStatus;
|
||||||
|
|
||||||
|
@state() private _aliases?: string[];
|
||||||
|
|
||||||
|
@state() private _googleEntity?: GoogleEntity;
|
||||||
|
|
||||||
|
protected willUpdate(changedProps: PropertyValues<this>) {
|
||||||
|
if (!isComponentLoaded(this.hass, "cloud")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (changedProps.has("entry") && this.entry) {
|
||||||
|
fetchCloudGoogleEntity(this.hass, this.entry.entity_id).then(
|
||||||
|
(googleEntity) => {
|
||||||
|
this._googleEntity = googleEntity;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (!this.hasUpdated) {
|
||||||
|
fetchCloudStatus(this.hass).then((status) => {
|
||||||
|
this._cloudStatus = status;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _getEntityFilterFuncs = memoizeOne(
|
||||||
|
(googleFilter: EntityFilter, alexaFilter: EntityFilter) => ({
|
||||||
|
google: generateFilter(
|
||||||
|
googleFilter.include_domains,
|
||||||
|
googleFilter.include_entities,
|
||||||
|
googleFilter.exclude_domains,
|
||||||
|
googleFilter.exclude_entities
|
||||||
|
),
|
||||||
|
alexa: generateFilter(
|
||||||
|
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 = [...voiceAssistantKeys];
|
||||||
|
const uiAssistants = [...voiceAssistantKeys];
|
||||||
|
|
||||||
|
const alexaManual =
|
||||||
|
alexaEnabled &&
|
||||||
|
!isEmptyFilter((this._cloudStatus as CloudStatusLoggedIn).alexa_entities);
|
||||||
|
const googleManual =
|
||||||
|
googleEnabled &&
|
||||||
|
!isEmptyFilter(
|
||||||
|
(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.entry.options?.[key]?.should_expose
|
||||||
|
);
|
||||||
|
|
||||||
|
let manFilterFuncs:
|
||||||
|
| {
|
||||||
|
google: FilterFunc;
|
||||||
|
alexa: FilterFunc;
|
||||||
|
}
|
||||||
|
| 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.entry.entity_id);
|
||||||
|
const manExposedGoogle =
|
||||||
|
googleManual && manFilterFuncs!.google(this.entry.entity_id);
|
||||||
|
|
||||||
|
const anyExposed = uiExposed || manExposedAlexa || manExposedGoogle;
|
||||||
|
|
||||||
|
return html`<ha-settings-row>
|
||||||
|
<span slot="heading"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.dialogs.voice-settings.expose_header"
|
||||||
|
)}</span
|
||||||
|
>
|
||||||
|
<ha-switch
|
||||||
|
@change=${this._toggleAll}
|
||||||
|
.checked=${anyExposed}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>
|
||||||
|
${anyExposed
|
||||||
|
? showAssistants.map(
|
||||||
|
(key) => html`<ha-settings-row>
|
||||||
|
<img
|
||||||
|
alt=""
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: voiceAssistants[key].domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
slot="prefix"
|
||||||
|
/>
|
||||||
|
<span slot="heading">${voiceAssistants[key].name}</span>
|
||||||
|
${key === "cloud.google_assistant" &&
|
||||||
|
!googleManual &&
|
||||||
|
this._googleEntity?.might_2fa
|
||||||
|
? html`<ha-formfield
|
||||||
|
slot="description"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.dialogs.voice-settings.ask_pin"
|
||||||
|
)}
|
||||||
|
>
|
||||||
|
<ha-checkbox
|
||||||
|
.checked=${!this.entry.options?.[key]?.disable_2fa}
|
||||||
|
@change=${this._2faChanged}
|
||||||
|
></ha-checkbox>
|
||||||
|
</ha-formfield>`
|
||||||
|
: (alexaManual && key === "cloud.alexa") ||
|
||||||
|
(googleManual && key === "cloud.google_assistant")
|
||||||
|
? html`<span slot="description"
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.dialogs.voice-settings.manual_config"
|
||||||
|
)}</span
|
||||||
|
>`
|
||||||
|
: ""}
|
||||||
|
<ha-switch
|
||||||
|
.assistant=${key}
|
||||||
|
@change=${this._toggleAssistant}
|
||||||
|
.disabled=${(alexaManual && key === "cloud.alexa") ||
|
||||||
|
(googleManual && key === "cloud.google_assistant")}
|
||||||
|
.checked=${alexaManual && key === "cloud.alexa"
|
||||||
|
? manExposedAlexa
|
||||||
|
: googleManual && key === "cloud.google_assistant"
|
||||||
|
? manExposedGoogle
|
||||||
|
: this.entry.options?.[key]?.should_expose}
|
||||||
|
></ha-switch>
|
||||||
|
</ha-settings-row>`
|
||||||
|
)
|
||||||
|
: ""}
|
||||||
|
|
||||||
|
<h3>
|
||||||
|
${this.hass.localize("ui.dialogs.voice-settings.aliasses_header")}
|
||||||
|
</h3>
|
||||||
|
|
||||||
|
<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) {
|
||||||
|
this._aliases = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _2faChanged(ev) {
|
||||||
|
try {
|
||||||
|
await updateCloudGoogleEntityConfig(
|
||||||
|
this.hass,
|
||||||
|
this.entry.entity_id,
|
||||||
|
!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.entry.entity_id,
|
||||||
|
{
|
||||||
|
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.entry.entity_id],
|
||||||
|
ev.target.checked
|
||||||
|
);
|
||||||
|
const entry = await getExtendedEntityRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
this.entry.entity_id
|
||||||
|
);
|
||||||
|
fireEvent(this, "entity-entry-updated", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _toggleAll(ev) {
|
||||||
|
exposeEntities(
|
||||||
|
this.hass,
|
||||||
|
voiceAssistantKeys,
|
||||||
|
[this.entry.entity_id],
|
||||||
|
ev.target.checked
|
||||||
|
);
|
||||||
|
const entry = await getExtendedEntityRegistryEntry(
|
||||||
|
this.hass,
|
||||||
|
this.entry.entity_id
|
||||||
|
);
|
||||||
|
fireEvent(this, "entity-entry-updated", entry);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
:host {
|
||||||
|
display: block;
|
||||||
|
margin: 32px;
|
||||||
|
margin-top: 0;
|
||||||
|
--settings-row-prefix-display: contents;
|
||||||
|
}
|
||||||
|
ha-settings-row {
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
img {
|
||||||
|
height: 32px;
|
||||||
|
margin-right: 16px;
|
||||||
|
}
|
||||||
|
ha-aliases-editor {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
ha-alert {
|
||||||
|
display: block;
|
||||||
|
margin-top: 16px;
|
||||||
|
}
|
||||||
|
ha-formfield {
|
||||||
|
margin-left: -8px;
|
||||||
|
}
|
||||||
|
ha-checkbox {
|
||||||
|
--mdc-checkbox-state-layer-size: 40px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"entity-registry-settings": EntityRegistrySettings;
|
||||||
|
}
|
||||||
|
interface HASSDomEvents {
|
||||||
|
"entity-entry-updated": ExtEntityRegistryEntry;
|
||||||
|
}
|
||||||
|
}
|
@ -0,0 +1,111 @@
|
|||||||
|
import { css, html, LitElement } from "lit";
|
||||||
|
import { customElement, property } from "lit/decorators";
|
||||||
|
import { computeRTLDirection } from "../../../common/util/compute_rtl";
|
||||||
|
import { CloudStatus } from "../../../data/cloud";
|
||||||
|
import "../../../layouts/hass-tabs-subpage";
|
||||||
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
|
import "./cloud-alexa-pref";
|
||||||
|
import "./cloud-google-pref";
|
||||||
|
import { voiceAssistantTabs } from "./ha-config-voice-assistants";
|
||||||
|
import "@polymer/paper-item/paper-item";
|
||||||
|
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||||
|
|
||||||
|
@customElement("ha-config-voice-assistants-assistants")
|
||||||
|
export class HaConfigVoiceAssistantsAssistants extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||||
|
|
||||||
|
@property() public isWide!: boolean;
|
||||||
|
|
||||||
|
@property() public narrow!: boolean;
|
||||||
|
|
||||||
|
@property() public route!: Route;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass) {
|
||||||
|
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
back-path="/config"
|
||||||
|
.route=${this.route}
|
||||||
|
.tabs=${voiceAssistantTabs}
|
||||||
|
>
|
||||||
|
<div class="content">
|
||||||
|
${this.cloudStatus?.logged_in
|
||||||
|
? html`<cloud-alexa-pref
|
||||||
|
.hass=${this.hass}
|
||||||
|
.cloudStatus=${this.cloudStatus}
|
||||||
|
dir=${computeRTLDirection(this.hass)}
|
||||||
|
></cloud-alexa-pref>
|
||||||
|
<cloud-google-pref
|
||||||
|
.hass=${this.hass}
|
||||||
|
.cloudStatus=${this.cloudStatus}
|
||||||
|
dir=${computeRTLDirection(this.hass)}
|
||||||
|
></cloud-google-pref>`
|
||||||
|
: html`<ha-card
|
||||||
|
header="Easily connect to voice assistants with Home Assistant Cloud"
|
||||||
|
>
|
||||||
|
<div class="card-content">
|
||||||
|
With Home Assistant Cloud, you can connect your Home
|
||||||
|
Assistant instance in a few simple clicks to both Google
|
||||||
|
Assistant and Amazon Alexa. If you can connect it to Home
|
||||||
|
Assistant, you can now control it with your voice using the
|
||||||
|
Amazon Echo, Google Home or your Android phone.
|
||||||
|
</div>
|
||||||
|
<div class="card-actions">
|
||||||
|
<a
|
||||||
|
href="https://www.nabucasa.com"
|
||||||
|
target="_blank"
|
||||||
|
rel="noreferrer"
|
||||||
|
>
|
||||||
|
<mwc-button>Learn more</mwc-button>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
${isComponentLoaded(this.hass, "cloud")
|
||||||
|
? html` <ha-card outlined>
|
||||||
|
<a href="/config/cloud/register">
|
||||||
|
<paper-item>
|
||||||
|
<paper-item-body two-line>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.login.start_trial"
|
||||||
|
)}
|
||||||
|
<div secondary>
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.cloud.login.trial_info"
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</paper-item-body>
|
||||||
|
<ha-icon-next></ha-icon-next>
|
||||||
|
</paper-item>
|
||||||
|
</a>
|
||||||
|
</ha-card>`
|
||||||
|
: ""}`}
|
||||||
|
</div>
|
||||||
|
</hass-tabs-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static styles = css`
|
||||||
|
.content {
|
||||||
|
padding: 28px 20px 0;
|
||||||
|
max-width: 1040px;
|
||||||
|
margin: 0 auto;
|
||||||
|
}
|
||||||
|
.content > * {
|
||||||
|
display: block;
|
||||||
|
margin: auto;
|
||||||
|
max-width: 800px;
|
||||||
|
margin-bottom: 24px;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
@ -0,0 +1,710 @@
|
|||||||
|
import { consume } from "@lit-labs/context";
|
||||||
|
import "@lrnwebcomponents/simple-tooltip/simple-tooltip";
|
||||||
|
import {
|
||||||
|
mdiMinusCircle,
|
||||||
|
mdiMinusCircleOutline,
|
||||||
|
mdiPlus,
|
||||||
|
mdiPlusCircle,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
|
||||||
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
|
import { classMap } from "lit/directives/class-map";
|
||||||
|
import { ifDefined } from "lit/directives/if-defined";
|
||||||
|
import { styleMap } from "lit/directives/style-map";
|
||||||
|
import memoize from "memoize-one";
|
||||||
|
import { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||||
|
import {
|
||||||
|
EntityFilter,
|
||||||
|
generateFilter,
|
||||||
|
isEmptyFilter,
|
||||||
|
} from "../../../common/entity/entity_filter";
|
||||||
|
import { navigate } from "../../../common/navigate";
|
||||||
|
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||||
|
import {
|
||||||
|
DataTableColumnContainer,
|
||||||
|
DataTableRowData,
|
||||||
|
RowClickedEvent,
|
||||||
|
SelectionChangedEvent,
|
||||||
|
} from "../../../components/data-table/ha-data-table";
|
||||||
|
import "../../../components/ha-fab";
|
||||||
|
import { CloudStatus, CloudStatusLoggedIn } from "../../../data/cloud";
|
||||||
|
import { entitiesContext } from "../../../data/context";
|
||||||
|
import {
|
||||||
|
computeEntityRegistryName,
|
||||||
|
EntityRegistryEntry,
|
||||||
|
ExtEntityRegistryEntry,
|
||||||
|
getExtendedEntityRegistryEntries,
|
||||||
|
} from "../../../data/entity_registry";
|
||||||
|
import {
|
||||||
|
exposeEntities,
|
||||||
|
voiceAssistantKeys,
|
||||||
|
voiceAssistants,
|
||||||
|
} from "../../../data/voice";
|
||||||
|
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||||
|
import "../../../layouts/hass-loading-screen";
|
||||||
|
import "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
|
import type { HaTabsSubpageDataTable } from "../../../layouts/hass-tabs-subpage-data-table";
|
||||||
|
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||||
|
import { haStyle } from "../../../resources/styles";
|
||||||
|
import { HomeAssistant, Route } from "../../../types";
|
||||||
|
import { brandsUrl } from "../../../util/brands-url";
|
||||||
|
import { voiceAssistantTabs } from "./ha-config-voice-assistants";
|
||||||
|
import { showExposeEntityDialog } from "./show-dialog-expose-entity";
|
||||||
|
import { showVoiceSettingsDialog } from "./show-dialog-voice-settings";
|
||||||
|
|
||||||
|
@customElement("ha-config-voice-assistants-expose")
|
||||||
|
export class VoiceAssistantsExpose extends SubscribeMixin(LitElement) {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public cloudStatus?: CloudStatus;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public isWide!: boolean;
|
||||||
|
|
||||||
|
@property({ type: Boolean }) public narrow!: boolean;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public route!: Route;
|
||||||
|
|
||||||
|
@state()
|
||||||
|
@consume({ context: entitiesContext, subscribe: true })
|
||||||
|
_entities!: HomeAssistant["entities"];
|
||||||
|
|
||||||
|
@state() private _extEntities?: Record<string, ExtEntityRegistryEntry>;
|
||||||
|
|
||||||
|
@state() private _filter: string = history.state?.filter || "";
|
||||||
|
|
||||||
|
@state() private _numHiddenEntities = 0;
|
||||||
|
|
||||||
|
@state() private _searchParms = new URLSearchParams(window.location.search);
|
||||||
|
|
||||||
|
@state() private _selectedEntities: string[] = [];
|
||||||
|
|
||||||
|
@query("hass-tabs-subpage-data-table", true)
|
||||||
|
private _dataTable!: HaTabsSubpageDataTable;
|
||||||
|
|
||||||
|
private _activeFilters = memoize(
|
||||||
|
(filters: URLSearchParams): string[] | undefined => {
|
||||||
|
const filterTexts: string[] = [];
|
||||||
|
filters.forEach((value, key) => {
|
||||||
|
switch (key) {
|
||||||
|
case "assistants": {
|
||||||
|
const assistants = value.split(",");
|
||||||
|
assistants.forEach((assistant) => {
|
||||||
|
filterTexts.push(voiceAssistants[assistant]?.name || assistant);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return filterTexts.length ? filterTexts : undefined;
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
private _columns = memoize(
|
||||||
|
(narrow, _language): DataTableColumnContainer => ({
|
||||||
|
icon: {
|
||||||
|
title: "",
|
||||||
|
type: "icon",
|
||||||
|
template: (_, entry) => html`
|
||||||
|
<ha-state-icon
|
||||||
|
title=${ifDefined(entry.entity?.state)}
|
||||||
|
.state=${entry.entity}
|
||||||
|
></ha-state-icon>
|
||||||
|
`,
|
||||||
|
},
|
||||||
|
name: {
|
||||||
|
main: true,
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.headers.name"
|
||||||
|
),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
direction: "asc",
|
||||||
|
grows: true,
|
||||||
|
template: narrow
|
||||||
|
? (name, entry) =>
|
||||||
|
html`
|
||||||
|
${name}<br />
|
||||||
|
<div class="secondary">${entry.entity_id}</div>
|
||||||
|
`
|
||||||
|
: undefined,
|
||||||
|
},
|
||||||
|
area: {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.headers.area"
|
||||||
|
),
|
||||||
|
sortable: true,
|
||||||
|
hidden: narrow,
|
||||||
|
filterable: true,
|
||||||
|
width: "15%",
|
||||||
|
},
|
||||||
|
assistants: {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.headers.assistants"
|
||||||
|
),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
width: "160px",
|
||||||
|
type: "flex",
|
||||||
|
template: (assistants, entry) =>
|
||||||
|
html`${voiceAssistantKeys.map((key) =>
|
||||||
|
assistants.includes(key)
|
||||||
|
? html`<div>
|
||||||
|
<img
|
||||||
|
style="height: 24px; margin-right: 16px;${styleMap({
|
||||||
|
filter: entry.manAssistants?.includes(key)
|
||||||
|
? "grayscale(100%)"
|
||||||
|
: "",
|
||||||
|
})}"
|
||||||
|
alt=""
|
||||||
|
src=${brandsUrl({
|
||||||
|
domain: voiceAssistants[key].domain,
|
||||||
|
type: "icon",
|
||||||
|
darkOptimized: this.hass.themes?.darkMode,
|
||||||
|
})}
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
slot="prefix"
|
||||||
|
/>${entry.manAssistants?.includes(key)
|
||||||
|
? html`<simple-tooltip
|
||||||
|
animation-delay="0"
|
||||||
|
position="bottom"
|
||||||
|
offset="1"
|
||||||
|
>
|
||||||
|
Configured in YAML, not editable in UI
|
||||||
|
</simple-tooltip>`
|
||||||
|
: ""}
|
||||||
|
</div>`
|
||||||
|
: html`<div style="width: 40px;"></div>`
|
||||||
|
)}`,
|
||||||
|
},
|
||||||
|
aliases: {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.headers.aliases"
|
||||||
|
),
|
||||||
|
sortable: true,
|
||||||
|
filterable: true,
|
||||||
|
width: "15%",
|
||||||
|
template: (aliases) =>
|
||||||
|
aliases.length === 0
|
||||||
|
? "-"
|
||||||
|
: aliases.length === 1
|
||||||
|
? aliases[0]
|
||||||
|
: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.aliases",
|
||||||
|
{ count: aliases.length }
|
||||||
|
),
|
||||||
|
},
|
||||||
|
remove: {
|
||||||
|
title: "",
|
||||||
|
type: "icon-button",
|
||||||
|
template: () =>
|
||||||
|
html`<ha-icon-button
|
||||||
|
@click=${this._removeEntity}
|
||||||
|
.path=${mdiMinusCircleOutline}
|
||||||
|
></ha-icon-button>`,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
private _getEntityFilterFuncs = memoize(
|
||||||
|
(googleFilter: EntityFilter, alexaFilter: EntityFilter) => ({
|
||||||
|
google: generateFilter(
|
||||||
|
googleFilter.include_domains,
|
||||||
|
googleFilter.include_entities,
|
||||||
|
googleFilter.exclude_domains,
|
||||||
|
googleFilter.exclude_entities
|
||||||
|
),
|
||||||
|
amazon: generateFilter(
|
||||||
|
alexaFilter.include_domains,
|
||||||
|
alexaFilter.include_entities,
|
||||||
|
alexaFilter.exclude_domains,
|
||||||
|
alexaFilter.exclude_entities
|
||||||
|
),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
private _filteredEntities = memoize(
|
||||||
|
(
|
||||||
|
entities: HomeAssistant["entities"],
|
||||||
|
extEntities: Record<string, ExtEntityRegistryEntry> | undefined,
|
||||||
|
devices: HomeAssistant["devices"],
|
||||||
|
areas: HomeAssistant["areas"],
|
||||||
|
cloudStatus: CloudStatus | undefined,
|
||||||
|
filters: URLSearchParams
|
||||||
|
) => {
|
||||||
|
const googleEnabled =
|
||||||
|
cloudStatus?.logged_in === true &&
|
||||||
|
cloudStatus.prefs.google_enabled === true;
|
||||||
|
const alexaEnabled =
|
||||||
|
cloudStatus?.logged_in === true &&
|
||||||
|
cloudStatus.prefs.alexa_enabled === true;
|
||||||
|
|
||||||
|
const showAssistants = [...voiceAssistantKeys];
|
||||||
|
|
||||||
|
const alexaManual =
|
||||||
|
alexaEnabled &&
|
||||||
|
!isEmptyFilter(
|
||||||
|
(this.cloudStatus as CloudStatusLoggedIn).alexa_entities
|
||||||
|
);
|
||||||
|
const googleManual =
|
||||||
|
googleEnabled &&
|
||||||
|
!isEmptyFilter(
|
||||||
|
(this.cloudStatus as CloudStatusLoggedIn).google_entities
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!googleEnabled || googleManual) {
|
||||||
|
showAssistants.splice(
|
||||||
|
showAssistants.indexOf("cloud.google_assistant"),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!alexaEnabled || alexaManual) {
|
||||||
|
showAssistants.splice(showAssistants.indexOf("cloud.alexa"), 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const result: Record<string, DataTableRowData> = {};
|
||||||
|
|
||||||
|
let filteredEntities = Object.values(entities);
|
||||||
|
|
||||||
|
filteredEntities = filteredEntities.filter((entity) =>
|
||||||
|
showAssistants.some(
|
||||||
|
(assis) =>
|
||||||
|
extEntities?.[entity.entity_id].options?.[assis]?.should_expose
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// If nothing gets filtered, this is our correct count of entities
|
||||||
|
const startLength = filteredEntities.length;
|
||||||
|
|
||||||
|
let filteredAssistants: string[];
|
||||||
|
|
||||||
|
filters.forEach((value, key) => {
|
||||||
|
if (key === "assistants") {
|
||||||
|
filteredAssistants = value.split(",");
|
||||||
|
filteredEntities = filteredEntities.filter((entity) =>
|
||||||
|
filteredAssistants.some(
|
||||||
|
(assis) =>
|
||||||
|
!(assis === "cloud.alexa" && alexaManual) &&
|
||||||
|
extEntities?.[entity.entity_id].options?.[assis]?.should_expose
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const entry of filteredEntities) {
|
||||||
|
const entity = this.hass.states[entry.entity_id];
|
||||||
|
const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id;
|
||||||
|
const area = areaId ? areas[areaId] : undefined;
|
||||||
|
|
||||||
|
result[entry.entity_id] = {
|
||||||
|
entity_id: entry.entity_id,
|
||||||
|
entity,
|
||||||
|
name: computeEntityRegistryName(
|
||||||
|
this.hass!,
|
||||||
|
entry as EntityRegistryEntry
|
||||||
|
),
|
||||||
|
area: area ? area.name : "—",
|
||||||
|
assistants: Object.keys(
|
||||||
|
extEntities![entry.entity_id].options!
|
||||||
|
).filter(
|
||||||
|
(key) =>
|
||||||
|
showAssistants.includes(key) &&
|
||||||
|
extEntities![entry.entity_id].options![key]?.should_expose
|
||||||
|
),
|
||||||
|
aliases: extEntities?.[entry.entity_id].aliases,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this._numHiddenEntities = startLength - Object.values(result).length;
|
||||||
|
|
||||||
|
if (alexaManual || googleManual) {
|
||||||
|
const manFilterFuncs = this._getEntityFilterFuncs(
|
||||||
|
(this.cloudStatus as CloudStatusLoggedIn).google_entities,
|
||||||
|
(this.cloudStatus as CloudStatusLoggedIn).alexa_entities
|
||||||
|
);
|
||||||
|
Object.keys(entities).forEach((entityId) => {
|
||||||
|
const assistants: string[] = [];
|
||||||
|
if (
|
||||||
|
alexaManual &&
|
||||||
|
(!filteredAssistants ||
|
||||||
|
filteredAssistants.includes("cloud.alexa")) &&
|
||||||
|
manFilterFuncs.amazon(entityId)
|
||||||
|
) {
|
||||||
|
assistants.push("cloud.alexa");
|
||||||
|
}
|
||||||
|
if (
|
||||||
|
googleManual &&
|
||||||
|
(!filteredAssistants ||
|
||||||
|
filteredAssistants.includes("cloud.google_assistant")) &&
|
||||||
|
manFilterFuncs.google(entityId)
|
||||||
|
) {
|
||||||
|
assistants.push("cloud.google_assistant");
|
||||||
|
}
|
||||||
|
if (!assistants.length) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (entityId in result) {
|
||||||
|
result[entityId].assistants.push(...assistants);
|
||||||
|
result[entityId].manAssistants = assistants;
|
||||||
|
} else {
|
||||||
|
const entry = entities[entityId];
|
||||||
|
const areaId = entry.area_id ?? devices[entry.device_id!]?.area_id;
|
||||||
|
const area = areaId ? areas[areaId] : undefined;
|
||||||
|
result[entityId] = {
|
||||||
|
entity_id: entry.entity_id,
|
||||||
|
entity: this.hass.states[entityId],
|
||||||
|
name: computeEntityRegistryName(
|
||||||
|
this.hass!,
|
||||||
|
entry as EntityRegistryEntry
|
||||||
|
),
|
||||||
|
area: area ? area.name : "—",
|
||||||
|
assistants: [
|
||||||
|
...(extEntities
|
||||||
|
? Object.keys(extEntities[entry.entity_id].options!).filter(
|
||||||
|
(key) =>
|
||||||
|
showAssistants.includes(key) &&
|
||||||
|
extEntities[entry.entity_id].options![key]
|
||||||
|
?.should_expose
|
||||||
|
)
|
||||||
|
: []),
|
||||||
|
...assistants,
|
||||||
|
],
|
||||||
|
manAssistants: assistants,
|
||||||
|
aliases: extEntities?.[entityId].aliases,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return Object.values(result);
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
public constructor() {
|
||||||
|
super();
|
||||||
|
window.addEventListener("location-changed", () => {
|
||||||
|
if (
|
||||||
|
window.location.search.substring(1) !== this._searchParms.toString()
|
||||||
|
) {
|
||||||
|
this._searchParms = new URLSearchParams(window.location.search);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
window.addEventListener("popstate", () => {
|
||||||
|
if (
|
||||||
|
window.location.search.substring(1) !== this._searchParms.toString()
|
||||||
|
) {
|
||||||
|
this._searchParms = new URLSearchParams(window.location.search);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchExtendedEntities() {
|
||||||
|
this._extEntities = await getExtendedEntityRegistryEntries(
|
||||||
|
this.hass,
|
||||||
|
Object.keys(this._entities)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
public willUpdate(changedProperties: PropertyValues): void {
|
||||||
|
if (changedProperties.has("_entities")) {
|
||||||
|
this._fetchExtendedEntities();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
if (!this.hass || this.hass.entities === undefined) {
|
||||||
|
return html`<hass-loading-screen></hass-loading-screen>`;
|
||||||
|
}
|
||||||
|
const activeFilters = this._activeFilters(this._searchParms);
|
||||||
|
|
||||||
|
const filteredEntities = this._filteredEntities(
|
||||||
|
this._entities,
|
||||||
|
this._extEntities,
|
||||||
|
this.hass.devices,
|
||||||
|
this.hass.areas,
|
||||||
|
this.cloudStatus,
|
||||||
|
this._searchParms
|
||||||
|
);
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage-data-table
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.backPath=${this._searchParms.has("historyBack")
|
||||||
|
? undefined
|
||||||
|
: "/config"}
|
||||||
|
.route=${this.route}
|
||||||
|
.tabs=${voiceAssistantTabs}
|
||||||
|
.columns=${this._columns(this.narrow, this.hass.language)}
|
||||||
|
.data=${filteredEntities}
|
||||||
|
.activeFilters=${activeFilters}
|
||||||
|
.numHidden=${this._numHiddenEntities}
|
||||||
|
.hideFilterMenu=${this._selectedEntities.length > 0}
|
||||||
|
.searchLabel=${this.hass.localize(
|
||||||
|
"ui.panel.config.entities.picker.search"
|
||||||
|
)}
|
||||||
|
.hiddenLabel=${this.hass.localize(
|
||||||
|
"ui.panel.config.entities.picker.filter.hidden_entities",
|
||||||
|
"number",
|
||||||
|
this._numHiddenEntities
|
||||||
|
)}
|
||||||
|
.filter=${this._filter}
|
||||||
|
selectable
|
||||||
|
clickable
|
||||||
|
@selection-changed=${this._handleSelectionChanged}
|
||||||
|
@clear-filter=${this._clearFilter}
|
||||||
|
@search-changed=${this._handleSearchChange}
|
||||||
|
@row-click=${this._openEditEntry}
|
||||||
|
id="entity_id"
|
||||||
|
hasFab
|
||||||
|
>
|
||||||
|
${this._selectedEntities.length
|
||||||
|
? html`
|
||||||
|
<div
|
||||||
|
class=${classMap({
|
||||||
|
"header-toolbar": this.narrow,
|
||||||
|
"table-header": !this.narrow,
|
||||||
|
})}
|
||||||
|
slot="header"
|
||||||
|
>
|
||||||
|
<p class="selected-txt">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.entities.picker.selected",
|
||||||
|
"number",
|
||||||
|
this._selectedEntities.length
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<div class="header-btns">
|
||||||
|
${!this.narrow
|
||||||
|
? html`
|
||||||
|
<mwc-button @click=${this._exposeSelected}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose"
|
||||||
|
)}</mwc-button
|
||||||
|
>
|
||||||
|
<mwc-button @click=${this._unexposeSelected}
|
||||||
|
>${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.unexpose"
|
||||||
|
)}</mwc-button
|
||||||
|
>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
<ha-icon-button
|
||||||
|
id="enable-btn"
|
||||||
|
@click=${this._exposeSelected}
|
||||||
|
.path=${mdiPlusCircle}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose"
|
||||||
|
)}
|
||||||
|
></ha-icon-button>
|
||||||
|
<simple-tooltip animation-delay="0" for="enable-btn">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose"
|
||||||
|
)}
|
||||||
|
</simple-tooltip>
|
||||||
|
<ha-icon-button
|
||||||
|
id="disable-btn"
|
||||||
|
@click=${this._unexposeSelected}
|
||||||
|
.path=${mdiMinusCircle}
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.unexpose"
|
||||||
|
)}
|
||||||
|
></ha-icon-button>
|
||||||
|
<simple-tooltip animation-delay="0" for="disable-btn">
|
||||||
|
${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.unexpose"
|
||||||
|
)}
|
||||||
|
</simple-tooltip>
|
||||||
|
`}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<ha-fab
|
||||||
|
slot="fab"
|
||||||
|
.label=${this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.add"
|
||||||
|
)}
|
||||||
|
extended
|
||||||
|
?rtl=${computeRTL(this.hass)}
|
||||||
|
@click=${this._addEntry}
|
||||||
|
>
|
||||||
|
<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
|
||||||
|
</ha-fab>
|
||||||
|
</hass-tabs-subpage-data-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _addEntry() {
|
||||||
|
const assistants = this._searchParms.has("assistants")
|
||||||
|
? this._searchParms.get("assistants")!.split(",")
|
||||||
|
: voiceAssistantKeys;
|
||||||
|
showExposeEntityDialog(this, {
|
||||||
|
filterAssistants: assistants,
|
||||||
|
extendedEntities: this._extEntities!,
|
||||||
|
exposeEntities: (entities) => {
|
||||||
|
exposeEntities(this.hass, assistants, entities, true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSearchChange(ev: CustomEvent) {
|
||||||
|
this._filter = ev.detail.value;
|
||||||
|
history.replaceState({ filter: this._filter }, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleSelectionChanged(
|
||||||
|
ev: HASSDomEvent<SelectionChangedEvent>
|
||||||
|
): void {
|
||||||
|
this._selectedEntities = ev.detail.value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _removeEntity = (ev) => {
|
||||||
|
ev.stopPropagation();
|
||||||
|
const entityId = ev.currentTarget.closest(".mdc-data-table__row").rowId;
|
||||||
|
const assistants = this._searchParms.has("assistants")
|
||||||
|
? this._searchParms.get("assistants")!.split(",")
|
||||||
|
: voiceAssistantKeys;
|
||||||
|
exposeEntities(this.hass, assistants, [entityId], false);
|
||||||
|
};
|
||||||
|
|
||||||
|
private _unexposeSelected() {
|
||||||
|
const assistants = this._searchParms.has("assistants")
|
||||||
|
? this._searchParms.get("assistants")!.split(",")
|
||||||
|
: voiceAssistantKeys;
|
||||||
|
showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.unexpose_confirm_title"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.unexpose_confirm_text",
|
||||||
|
{
|
||||||
|
assistants: assistants
|
||||||
|
.map((ass) => voiceAssistants[ass].name)
|
||||||
|
.join(", "),
|
||||||
|
entities: this._selectedEntities.length,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
confirmText: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.unexpose"
|
||||||
|
),
|
||||||
|
dismissText: this.hass.localize("ui.common.cancel"),
|
||||||
|
confirm: () => {
|
||||||
|
exposeEntities(this.hass, assistants, this._selectedEntities, false);
|
||||||
|
this._clearSelection();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _exposeSelected() {
|
||||||
|
const assistants = this._searchParms.has("assistants")
|
||||||
|
? this._searchParms.get("assistants")!.split(",")
|
||||||
|
: voiceAssistantKeys;
|
||||||
|
showConfirmationDialog(this, {
|
||||||
|
title: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose_confirm_title"
|
||||||
|
),
|
||||||
|
text: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose_confirm_text",
|
||||||
|
{
|
||||||
|
assistants: assistants
|
||||||
|
.map((ass) => voiceAssistants[ass].name)
|
||||||
|
.join(", "),
|
||||||
|
entities: this._selectedEntities.length,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
confirmText: this.hass.localize(
|
||||||
|
"ui.panel.config.voice_assistants.expose.expose"
|
||||||
|
),
|
||||||
|
dismissText: this.hass.localize("ui.common.cancel"),
|
||||||
|
confirm: () => {
|
||||||
|
exposeEntities(this.hass, assistants, this._selectedEntities, true);
|
||||||
|
this._clearSelection();
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearSelection() {
|
||||||
|
this._dataTable.clearSelection();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _openEditEntry(ev: CustomEvent): void {
|
||||||
|
const entityId = (ev.detail as RowClickedEvent).id;
|
||||||
|
showVoiceSettingsDialog(this, { entityId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private _clearFilter() {
|
||||||
|
if (this._activeFilters(this._searchParms)) {
|
||||||
|
navigate(window.location.pathname, { replace: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResultGroup {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
hass-loading-screen {
|
||||||
|
--app-header-background-color: var(--sidebar-background-color);
|
||||||
|
--app-header-text-color: var(--sidebar-text-color);
|
||||||
|
}
|
||||||
|
.table-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
height: 56px;
|
||||||
|
background-color: var(--mdc-text-field-fill-color, whitesmoke);
|
||||||
|
border-bottom: 1px solid
|
||||||
|
var(--mdc-text-field-idle-line-color, rgba(0, 0, 0, 0.42));
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
.header-toolbar {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
color: var(--secondary-text-color);
|
||||||
|
position: relative;
|
||||||
|
top: -4px;
|
||||||
|
}
|
||||||
|
.selected-txt {
|
||||||
|
font-weight: bold;
|
||||||
|
padding-left: 16px;
|
||||||
|
padding-inline-start: 16px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.table-header .selected-txt {
|
||||||
|
margin-top: 20px;
|
||||||
|
}
|
||||||
|
.header-toolbar .selected-txt {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
.header-toolbar .header-btns {
|
||||||
|
margin-right: -12px;
|
||||||
|
margin-inline-end: -12px;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
.header-btns {
|
||||||
|
display: flex;
|
||||||
|
}
|
||||||
|
.header-btns > mwc-button,
|
||||||
|
.header-btns > ha-icon-button {
|
||||||
|
margin: 8px;
|
||||||
|
}
|
||||||
|
ha-button-menu {
|
||||||
|
margin-left: 8px;
|
||||||
|
}
|
||||||
|
.clear {
|
||||||
|
color: var(--primary-color);
|
||||||
|
padding-left: 8px;
|
||||||
|
padding-inline-start: 8px;
|
||||||
|
text-transform: uppercase;
|
||||||
|
direction: var(--direction);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-voice-assistants-expose": VoiceAssistantsExpose;
|
||||||
|
}
|
||||||
|
}
|
@ -1,21 +1,47 @@
|
|||||||
|
import { mdiDevices, mdiMicrophone } from "@mdi/js";
|
||||||
import { customElement, property } from "lit/decorators";
|
import { customElement, property } from "lit/decorators";
|
||||||
import {
|
import {
|
||||||
HassRouterPage,
|
HassRouterPage,
|
||||||
RouterOptions,
|
RouterOptions,
|
||||||
} from "../../../layouts/hass-router-page";
|
} from "../../../layouts/hass-router-page";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { CloudStatus } from "../../../data/cloud";
|
||||||
|
|
||||||
|
export const voiceAssistantTabs = [
|
||||||
|
{
|
||||||
|
path: "/config/voice-assistants/assistants",
|
||||||
|
translationKey: "ui.panel.config.voice_assistants.assistants.caption",
|
||||||
|
iconPath: mdiMicrophone,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: "/config/voice-assistants/expose",
|
||||||
|
translationKey: "ui.panel.config.voice_assistants.expose.caption",
|
||||||
|
iconPath: mdiDevices,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
@customElement("ha-config-voice-assistants")
|
@customElement("ha-config-voice-assistants")
|
||||||
class HaConfigVoiceAssistants extends HassRouterPage {
|
class HaConfigVoiceAssistants extends HassRouterPage {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) public cloudStatus!: CloudStatus;
|
||||||
|
|
||||||
@property() public narrow!: boolean;
|
@property() public narrow!: boolean;
|
||||||
|
|
||||||
@property() public isWide!: boolean;
|
@property() public isWide!: boolean;
|
||||||
|
|
||||||
protected routerOptions: RouterOptions = {
|
protected routerOptions: RouterOptions = {
|
||||||
defaultPage: "debug",
|
defaultPage: "assistants",
|
||||||
routes: {
|
routes: {
|
||||||
|
assistants: {
|
||||||
|
tag: "ha-config-voice-assistants-assistants",
|
||||||
|
load: () => import("./ha-config-voice-assistants-assistants"),
|
||||||
|
cache: true,
|
||||||
|
},
|
||||||
|
expose: {
|
||||||
|
tag: "ha-config-voice-assistants-expose",
|
||||||
|
load: () => import("./ha-config-voice-assistants-expose"),
|
||||||
|
},
|
||||||
debug: {
|
debug: {
|
||||||
tag: "assist-pipeline-debug",
|
tag: "assist-pipeline-debug",
|
||||||
load: () =>
|
load: () =>
|
||||||
@ -28,6 +54,7 @@ class HaConfigVoiceAssistants extends HassRouterPage {
|
|||||||
|
|
||||||
protected updatePageEl(pageEl) {
|
protected updatePageEl(pageEl) {
|
||||||
pageEl.hass = this.hass;
|
pageEl.hass = this.hass;
|
||||||
|
pageEl.cloudStatus = this.cloudStatus;
|
||||||
pageEl.narrow = this.narrow;
|
pageEl.narrow = this.narrow;
|
||||||
pageEl.isWide = this.isWide;
|
pageEl.isWide = this.isWide;
|
||||||
pageEl.route = this.routeTail;
|
pageEl.route = this.routeTail;
|
||||||
|
@ -0,0 +1,21 @@
|
|||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
|
|
||||||
|
export interface ExposeEntityDialogParams {
|
||||||
|
filterAssistants: string[];
|
||||||
|
extendedEntities: Record<string, ExtEntityRegistryEntry>;
|
||||||
|
exposeEntities: (entities: string[]) => void;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadExposeEntityDialog = () => import("./dialog-expose-entity");
|
||||||
|
|
||||||
|
export const showExposeEntityDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
dialogParams: ExposeEntityDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-expose-entity",
|
||||||
|
dialogImport: loadExposeEntityDialog,
|
||||||
|
dialogParams,
|
||||||
|
});
|
||||||
|
};
|
@ -0,0 +1,18 @@
|
|||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
|
||||||
|
export interface VoiceSettingsDialogParams {
|
||||||
|
entityId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadVoiceSettingsDialog = () => import("./dialog-voice-settings");
|
||||||
|
|
||||||
|
export const showVoiceSettingsDialog = (
|
||||||
|
element: HTMLElement,
|
||||||
|
aliasesParams: VoiceSettingsDialogParams
|
||||||
|
): void => {
|
||||||
|
fireEvent(element, "show-dialog", {
|
||||||
|
dialogTag: "dialog-voice-settings",
|
||||||
|
dialogImport: loadVoiceSettingsDialog,
|
||||||
|
dialogParams: aliasesParams,
|
||||||
|
});
|
||||||
|
};
|
@ -141,6 +141,9 @@ export const getMyRedirects = (hasSupervisor: boolean): Redirects => ({
|
|||||||
component: "tag",
|
component: "tag",
|
||||||
redirect: "/config/tags",
|
redirect: "/config/tags",
|
||||||
},
|
},
|
||||||
|
"voice-assistants": {
|
||||||
|
redirect: "/config/voice-assistants",
|
||||||
|
},
|
||||||
lovelace_dashboards: {
|
lovelace_dashboards: {
|
||||||
component: "lovelace",
|
component: "lovelace",
|
||||||
redirect: "/config/lovelace/dashboards",
|
redirect: "/config/lovelace/dashboards",
|
||||||
|
@ -1060,6 +1060,12 @@
|
|||||||
"aliases_description": "Aliases are alternative names used in voice assistants to refer to this entity."
|
"aliases_description": "Aliases are alternative names used in voice assistants to refer to this entity."
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"voice-settings": {
|
||||||
|
"expose_header": "Expose",
|
||||||
|
"aliasses_header": "Aliasses",
|
||||||
|
"ask_pin": "Ask for PIN",
|
||||||
|
"manual_config": "Managed with filters in configuration.yaml"
|
||||||
|
},
|
||||||
"restart": {
|
"restart": {
|
||||||
"heading": "Restart Home Assistant",
|
"heading": "Restart Home Assistant",
|
||||||
"advanced_options": "Advanced options",
|
"advanced_options": "Advanced options",
|
||||||
@ -1246,10 +1252,7 @@
|
|||||||
"device_name_placeholder": "Change device name"
|
"device_name_placeholder": "Change device name"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"domain_toggler": {
|
"entity_voice_settings": {},
|
||||||
"title": "Toggle Domains",
|
|
||||||
"reset_entities": "Reset Entity overrides"
|
|
||||||
},
|
|
||||||
"mqtt_device_debug_info": {
|
"mqtt_device_debug_info": {
|
||||||
"title": "{device} debug info",
|
"title": "{device} debug info",
|
||||||
"deserialize": "Attempt to parse MQTT messages as JSON",
|
"deserialize": "Attempt to parse MQTT messages as JSON",
|
||||||
@ -1406,6 +1409,10 @@
|
|||||||
"main": "Dashboards",
|
"main": "Dashboards",
|
||||||
"secondary": "Organize how you interact with your home"
|
"secondary": "Organize how you interact with your home"
|
||||||
},
|
},
|
||||||
|
"voice_assistants": {
|
||||||
|
"main": "Voice assistants",
|
||||||
|
"secondary": "Manage your voice assistants"
|
||||||
|
},
|
||||||
"energy": {
|
"energy": {
|
||||||
"main": "Energy",
|
"main": "Energy",
|
||||||
"secondary": "Monitor your energy production and consumption"
|
"secondary": "Monitor your energy production and consumption"
|
||||||
@ -1992,6 +1999,32 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"voice_assistants": {
|
||||||
|
"assistants": {
|
||||||
|
"caption": "Assistants"
|
||||||
|
},
|
||||||
|
"expose": {
|
||||||
|
"caption": "Expose",
|
||||||
|
"headers": {
|
||||||
|
"name": "Name",
|
||||||
|
"area": "Area",
|
||||||
|
"assistants": "Assistants",
|
||||||
|
"aliases": "Aliases"
|
||||||
|
},
|
||||||
|
"aliases": "{count} aliases",
|
||||||
|
"expose": "Expose",
|
||||||
|
"unexpose": "Unexpose",
|
||||||
|
"add": "Expose entities",
|
||||||
|
"expose_confirm_title": "Expose selected entities?",
|
||||||
|
"expose_confirm_text": "Do you want to expose {entities} entities to {assistants}?",
|
||||||
|
"unexpose_confirm_title": "Stop exposing selected entities?",
|
||||||
|
"unexpose_confirm_text": "Do you want to stop exposing {entities} entities to {assistants}?",
|
||||||
|
"expose_dialog": {
|
||||||
|
"header": "Expose entity",
|
||||||
|
"expose_entities": "Expose {count} {count, plural,\n one {entity}\n other {entities}\n}"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"automation": {
|
"automation": {
|
||||||
"caption": "Automations",
|
"caption": "Automations",
|
||||||
"description": "Create custom behavior rules for your home",
|
"description": "Create custom behavior rules for your home",
|
||||||
@ -2677,6 +2710,7 @@
|
|||||||
"integrations_introduction": "Integrations for Home Assistant Cloud allow you to connect with services in the cloud without having to expose your Home Assistant instance publicly on the internet.",
|
"integrations_introduction": "Integrations for Home Assistant Cloud allow you to connect with services in the cloud without having to expose your Home Assistant instance publicly on the internet.",
|
||||||
"integrations_introduction2": "Check the website for ",
|
"integrations_introduction2": "Check the website for ",
|
||||||
"integrations_link_all_features": " all available features",
|
"integrations_link_all_features": " all available features",
|
||||||
|
"tip_moved_voice_assistants": "Looking for Google Assistant and Amazon Alexa settings? They are moved to the new voice assistants page.",
|
||||||
"connected": "Connected",
|
"connected": "Connected",
|
||||||
"connecting": "Connecting…",
|
"connecting": "Connecting…",
|
||||||
"not_connected": "Not Connected",
|
"not_connected": "Not Connected",
|
||||||
@ -2718,11 +2752,14 @@
|
|||||||
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.",
|
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Amazon. This allows you to always see the latest states in the Alexa app and use the state changes to create routines.",
|
||||||
"state_reporting_error": "Unable to {enable_disable} report state.",
|
"state_reporting_error": "Unable to {enable_disable} report state.",
|
||||||
"manage_entities": "[%key:ui::panel::config::cloud::account::google::manage_entities%]",
|
"manage_entities": "[%key:ui::panel::config::cloud::account::google::manage_entities%]",
|
||||||
|
"manual_config": "[%key:ui::panel::config::cloud::account::google::manual_config%]",
|
||||||
"enable": "enable",
|
"enable": "enable",
|
||||||
"disable": "disable",
|
"disable": "disable",
|
||||||
"not_configured_title": "Alexa is not activated",
|
"not_configured_title": "Continue setting up Alexa",
|
||||||
"not_configured_text": "Before you can use Alexa, you need to activate the Home Assistant skill for Alexa in the Alexa app.",
|
"not_configured_text": "Before you can use Alexa, you need to activate the Home Assistant skill for Alexa in the Alexa app.",
|
||||||
"link_learn_how_it_works": "[%key:ui::panel::config::cloud::account::remote::link_learn_how_it_works%]"
|
"link_learn_how_it_works": "[%key:ui::panel::config::cloud::account::remote::link_learn_how_it_works%]",
|
||||||
|
"expose_new_entities": "[%key:ui::panel::config::cloud::account::google::expose_new_entities%]",
|
||||||
|
"expose_new_entities_info": "Should new entities, that are supported and have no security risks be exposed to Alexa automatically?"
|
||||||
},
|
},
|
||||||
"google": {
|
"google": {
|
||||||
"title": "Google Assistant",
|
"title": "Google Assistant",
|
||||||
@ -2738,10 +2775,13 @@
|
|||||||
"devices_pin": "Security Devices PIN",
|
"devices_pin": "Security Devices PIN",
|
||||||
"enter_pin_hint": "Enter a PIN to use security devices",
|
"enter_pin_hint": "Enter a PIN to use security devices",
|
||||||
"manage_entities": "Manage Entities",
|
"manage_entities": "Manage Entities",
|
||||||
|
"manual_config": "Editing which entities are exposed via the UI is disabled because you have configured entity filters in configuration.yaml.",
|
||||||
"enter_pin_error": "Unable to store PIN:",
|
"enter_pin_error": "Unable to store PIN:",
|
||||||
"not_configured_title": "Google Assistant is not activated",
|
"not_configured_title": "Continue setting up Google Assistant",
|
||||||
"not_configured_text": "Before you can use Google Assistant, you need to activate the Home Assistant Cloud skill for Google Assistant in the Google Home app.",
|
"not_configured_text": "Before you can use Google Assistant, you need to activate the Home Assistant Cloud skill for Google Assistant in the Google Home app.",
|
||||||
"link_learn_how_it_works": "[%key:ui::panel::config::cloud::account::remote::link_learn_how_it_works%]"
|
"link_learn_how_it_works": "[%key:ui::panel::config::cloud::account::remote::link_learn_how_it_works%]",
|
||||||
|
"expose_new_entities": "Expose new entities",
|
||||||
|
"expose_new_entities_info": "Should new entities, that are supported and have no security risks be exposed to Google Assistant automatically?"
|
||||||
},
|
},
|
||||||
"webhooks": {
|
"webhooks": {
|
||||||
"title": "Webhooks",
|
"title": "Webhooks",
|
||||||
@ -2756,23 +2796,6 @@
|
|||||||
"disable_hook_error_msg": "Failed to disable webhook:"
|
"disable_hook_error_msg": "Failed to disable webhook:"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"alexa": {
|
|
||||||
"title": "Alexa",
|
|
||||||
"banner": "[%key:ui::panel::config::cloud::google::banner%]",
|
|
||||||
"exposed_entities": "[%key:ui::panel::config::cloud::google::exposed_entities%]",
|
|
||||||
"not_exposed_entities": "[%key:ui::panel::config::cloud::google::not_exposed_entities%]",
|
|
||||||
"manage_defaults": "[%key:ui::panel::config::cloud::google::manage_defaults%]",
|
|
||||||
"manage_defaults_dialog_description": "[%key:ui::panel::config::cloud::google::manage_defaults_dialog_description%]",
|
|
||||||
"expose_entity": "[%key:ui::panel::config::cloud::google::expose_entity%]",
|
|
||||||
"dont_expose_entity": "[%key:ui::panel::config::cloud::google::dont_expose_entity%]",
|
|
||||||
"follow_domain": "[%key:ui::panel::config::cloud::google::follow_domain%]",
|
|
||||||
"exposed": "[%key:ui::panel::config::cloud::google::exposed%]",
|
|
||||||
"not_exposed": "[%key:ui::panel::config::cloud::google::not_exposed%]",
|
|
||||||
"manage_aliases": "[%key:ui::panel::config::cloud::google::manage_aliases%]",
|
|
||||||
"expose": "Expose to Alexa",
|
|
||||||
"sync_entities": "Synchronize entities",
|
|
||||||
"sync_entities_error": "Failed to sync entities:"
|
|
||||||
},
|
|
||||||
"dialog_certificate": {
|
"dialog_certificate": {
|
||||||
"certificate_information": "Certificate Information",
|
"certificate_information": "Certificate Information",
|
||||||
"certificate_expiration_date": "Certificate expiration date:",
|
"certificate_expiration_date": "Certificate expiration date:",
|
||||||
@ -2780,33 +2803,6 @@
|
|||||||
"fingerprint": "Certificate fingerprint:",
|
"fingerprint": "Certificate fingerprint:",
|
||||||
"close": "Close"
|
"close": "Close"
|
||||||
},
|
},
|
||||||
"google": {
|
|
||||||
"title": "Google Assistant",
|
|
||||||
"expose": "Expose to Google Assistant",
|
|
||||||
"disable_2FA": "Disable two factor authentication",
|
|
||||||
"banner": "Editing which entities are exposed via this UI is disabled because you have configured entity filters in configuration.yaml.",
|
|
||||||
"exposed_entities": "Exposed entities",
|
|
||||||
"not_exposed_entities": "Not exposed entities",
|
|
||||||
"manage_defaults": "Manage defaults",
|
|
||||||
"manage_defaults_dialog_description": "Entities can be exposed by default based on their type.",
|
|
||||||
"expose_entity": "Expose entity",
|
|
||||||
"dont_expose_entity": "Don't expose entity",
|
|
||||||
"follow_domain": "Follow domain",
|
|
||||||
"exposed": "{selected} exposed",
|
|
||||||
"not_exposed": "{selected} not exposed",
|
|
||||||
"manage_aliases": "Manage aliases",
|
|
||||||
"add_aliases": "Add aliases",
|
|
||||||
"no_aliases": "No aliases",
|
|
||||||
"aliases_not_available": "Aliases not available",
|
|
||||||
"aliases_not_available_learn_more": "Learn more",
|
|
||||||
"sync_to_google": "Synchronizing changes to Google.",
|
|
||||||
"sync_entities": "Synchronize entities",
|
|
||||||
"sync_entities_error": "Failed to sync entities:",
|
|
||||||
"not_configured_title": "[%key:ui::panel::config::cloud::account::google::not_configured_title%]",
|
|
||||||
"not_configured_text": "[%key:ui::panel::config::cloud::account::google::not_configured_text%]",
|
|
||||||
"sync_failed_title": "Syncing failed",
|
|
||||||
"sync_failed_text": "Syncing your entities failed, try again or check the logs."
|
|
||||||
},
|
|
||||||
"dialog_cloudhook": {
|
"dialog_cloudhook": {
|
||||||
"webhook_for": "Webhook for {name}",
|
"webhook_for": "Webhook for {name}",
|
||||||
"managed_by_integration": "This webhook is managed by an integration and cannot be disabled.",
|
"managed_by_integration": "This webhook is managed by an integration and cannot be disabled.",
|
||||||
|
Loading…
x
Reference in New Issue
Block a user