Improve cloud dashboard (#11422)

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Paulus Schoutsen 2022-01-26 02:00:50 -08:00 committed by GitHub
parent 68bee4dd58
commit f398692e75
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 326 additions and 119 deletions

View File

@ -51,11 +51,13 @@ export interface CloudStatusLoggedIn {
google_registered: boolean; google_registered: boolean;
google_entities: EntityFilter; google_entities: EntityFilter;
google_domains: string[]; google_domains: string[];
alexa_registered: boolean;
alexa_entities: EntityFilter; alexa_entities: EntityFilter;
prefs: CloudPreferences; prefs: CloudPreferences;
remote_domain: string | undefined; remote_domain: string | undefined;
remote_connected: boolean; remote_connected: boolean;
remote_certificate: undefined | CertificateInformation; remote_certificate: undefined | CertificateInformation;
http_use_ssl: boolean;
} }
export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn; export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn;

View File

@ -8,3 +8,6 @@ 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 syncCloudGoogleEntities = (hass: HomeAssistant) =>
hass.callApi("POST", "cloud/google_actions/sync");

View File

@ -1,5 +1,8 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@material/mwc-list/mwc-list-item";
import type { ActionDetail } from "@material/mwc-list";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
import { mdiDotsVertical } from "@mdi/js";
import { LitElement, css, html, PropertyValues } from "lit"; import { LitElement, css, html, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { formatDateTime } from "../../../../common/datetime/format_date_time"; import { formatDateTime } from "../../../../common/datetime/format_date_time";
@ -7,6 +10,8 @@ import { fireEvent } from "../../../../common/dom/fire_event";
import { computeRTLDirection } from "../../../../common/util/compute_rtl"; import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/buttons/ha-call-api-button"; import "../../../../components/buttons/ha-call-api-button";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-icon-button";
import { import {
cloudLogout, cloudLogout,
CloudStatusLoggedIn, CloudStatusLoggedIn,
@ -21,9 +26,10 @@ 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";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
@customElement("cloud-account") @customElement("cloud-account")
export class CloudAccount extends LitElement { export class CloudAccount extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ type: Boolean }) public isWide = false; @property({ type: Boolean }) public isWide = false;
@ -43,6 +49,23 @@ export class CloudAccount extends LitElement {
.narrow=${this.narrow} .narrow=${this.narrow}
header="Home Assistant Cloud" header="Home Assistant Cloud"
> >
<ha-button-menu
slot="toolbar-icon"
corner="BOTTOM_START"
@action=${this._handleMenuAction}
activatable
>
<ha-icon-button
slot="trigger"
.label=${this.hass.localize("ui.common.menu")}
.path=${mdiDotsVertical}
></ha-icon-button>
<mwc-list-item>
${this.hass.localize("ui.panel.config.cloud.account.sign_out")}
</mwc-list-item>
</ha-button-menu>
<div class="content"> <div class="content">
<ha-config-section .isWide=${this.isWide}> <ha-config-section .isWide=${this.isWide}>
<span slot="header">Home Assistant Cloud</span> <span slot="header">Home Assistant Cloud</span>
@ -115,11 +138,6 @@ export class CloudAccount extends LitElement {
)} )}
</mwc-button> </mwc-button>
</a> </a>
<mwc-button @click=${this._handleLogout}
>${this.hass.localize(
"ui.panel.config.cloud.account.sign_out"
)}</mwc-button
>
</div> </div>
</ha-card> </ha-card>
</ha-config-section> </ha-config-section>
@ -200,6 +218,33 @@ export class CloudAccount extends LitElement {
} }
} }
protected override hassSubscribe() {
const googleCheck = () => {
if (!this.cloudStatus?.google_registered) {
fireEvent(this, "ha-refresh-cloud-status");
}
};
return [
this.hass.connection.subscribeEvents(() => {
if (!this.cloudStatus?.alexa_registered) {
fireEvent(this, "ha-refresh-cloud-status");
}
}, "alexa_smart_home"),
this.hass.connection.subscribeEvents(
googleCheck,
"google_assistant_command"
),
this.hass.connection.subscribeEvents(
googleCheck,
"google_assistant_query"
),
this.hass.connection.subscribeEvents(
googleCheck,
"google_assistant_sync"
),
];
}
private async _fetchSubscriptionInfo() { private async _fetchSubscriptionInfo() {
this._subscription = await fetchCloudSubscriptionInfo(this.hass); this._subscription = await fetchCloudSubscriptionInfo(this.hass);
if ( if (
@ -211,10 +256,13 @@ export class CloudAccount extends LitElement {
} }
} }
private async _handleLogout() { private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
await cloudLogout(this.hass); await cloudLogout(this.hass);
fireEvent(this, "ha-refresh-cloud-status"); fireEvent(this, "ha-refresh-cloud-status");
} }
}
_computeRTLDirection(hass) { _computeRTLDirection(hass) {
return computeRTLDirection(hass); return computeRTLDirection(hass);
@ -237,7 +285,7 @@ export class CloudAccount extends LitElement {
} }
.card-actions { .card-actions {
display: flex; display: flex;
justify-content: space-between; flex-direction: row-reverse;
} }
.card-actions a { .card-actions a {
text-decoration: none; text-decoration: none;

View File

@ -10,7 +10,7 @@ import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
export class CloudAlexaPref extends LitElement { export class CloudAlexaPref extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus?: CloudStatusLoggedIn; @property() public cloudStatus?: CloudStatusLoggedIn;
@ -21,6 +21,7 @@ export class CloudAlexaPref extends LitElement {
return html``; return html``;
} }
const alexa_registered = this.cloudStatus.alexa_registered;
const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs; const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs;
return html` return html`
@ -36,7 +37,22 @@ export class CloudAlexaPref extends LitElement {
></ha-switch> ></ha-switch>
</div> </div>
<div class="card-content"> <div class="card-content">
<p>
${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")} ${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> <ul>
<li> <li>
<a <a
@ -61,8 +77,9 @@ export class CloudAlexaPref extends LitElement {
</a> </a>
</li> </li>
</ul> </ul>
${alexa_enabled </ha-alert>
? html` `
: html`
<div class="state-reporting"> <div class="state-reporting">
<h3> <h3>
${this.hass!.localize( ${this.hass!.localize(
@ -81,10 +98,11 @@ export class CloudAlexaPref extends LitElement {
"ui.panel.config.cloud.account.alexa.info_state_reporting" "ui.panel.config.cloud.account.alexa.info_state_reporting"
)} )}
</p> </p>
` `}
: ""}
</div> </div>
<div class="card-actions"> <div class="card-actions">
${alexa_registered
? html`
<mwc-button <mwc-button
@click=${this._handleSync} @click=${this._handleSync}
.disabled=${!alexa_enabled || this._syncing} .disabled=${!alexa_enabled || this._syncing}
@ -93,6 +111,8 @@ export class CloudAlexaPref extends LitElement {
"ui.panel.config.cloud.account.alexa.sync_entities" "ui.panel.config.cloud.account.alexa.sync_entities"
)} )}
</mwc-button> </mwc-button>
`
: ""}
<div class="spacer"></div> <div class="spacer"></div>
<a href="/config/cloud/alexa"> <a href="/config/cloud/alexa">
<mwc-button <mwc-button

View File

@ -1,14 +1,14 @@
import "@material/mwc-button"; import "@material/mwc-button";
import "@polymer/paper-input/paper-input"; import "@material/mwc-textfield/mwc-textfield";
import type { PaperInputElement } from "@polymer/paper-input/paper-input"; import type { TextField } from "@material/mwc-textfield/mwc-textfield";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { property } from "lit/decorators"; import { property, state } from "lit/decorators";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/buttons/ha-call-api-button";
import "../../../../components/ha-card";
import "../../../../components/ha-alert"; import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import type { HaSwitch } from "../../../../components/ha-switch"; import type { HaSwitch } from "../../../../components/ha-switch";
import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud";
import { syncCloudGoogleEntities } from "../../../../data/google_assistant";
import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showSaveSuccessToast } from "../../../../util/toast-saved-success"; import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
@ -16,13 +16,16 @@ import { showSaveSuccessToast } from "../../../../util/toast-saved-success";
export class CloudGooglePref extends LitElement { export class CloudGooglePref extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus?: CloudStatusLoggedIn; @property({ attribute: false }) public cloudStatus?: CloudStatusLoggedIn;
@state() private _syncing = false;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.cloudStatus) { if (!this.cloudStatus) {
return html``; return html``;
} }
const google_registered = this.cloudStatus.google_registered;
const { google_enabled, google_report_state, google_secure_devices_pin } = const { google_enabled, google_report_state, google_secure_devices_pin } =
this.cloudStatus.prefs; this.cloudStatus.prefs;
@ -43,7 +46,9 @@ export class CloudGooglePref extends LitElement {
<p> <p>
${this.hass.localize("ui.panel.config.cloud.account.google.info")} ${this.hass.localize("ui.panel.config.cloud.account.google.info")}
</p> </p>
${google_enabled && !this.cloudStatus.google_registered ${!google_enabled
? ""
: !google_registered
? html` ? html`
<ha-alert <ha-alert
.title=${this.hass.localize( .title=${this.hass.localize(
@ -80,9 +85,30 @@ export class CloudGooglePref extends LitElement {
</ul> </ul>
</ha-alert> </ha-alert>
` `
: ""} : html`
${google_enabled ${this.cloudStatus.http_use_ssl
? html` ? 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>
`
: ""}
<div class="state-reporting"> <div class="state-reporting">
<h3> <h3>
${this.hass.localize( ${this.hass.localize(
@ -110,32 +136,33 @@ export class CloudGooglePref extends LitElement {
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.cloud.account.google.enter_pin_info" "ui.panel.config.cloud.account.google.enter_pin_info"
)} )}
<paper-input <mwc-textfield
label=${this.hass.localize( .label=${this.hass.localize(
"ui.panel.config.cloud.account.google.devices_pin" "ui.panel.config.cloud.account.google.devices_pin"
)} )}
id="google_secure_devices_pin" .placeholder=${this.hass.localize(
placeholder=${this.hass.localize(
"ui.panel.config.cloud.account.google.enter_pin_hint" "ui.panel.config.cloud.account.google.enter_pin_hint"
)} )}
.value=${google_secure_devices_pin || ""} .value=${google_secure_devices_pin || ""}
@change=${this._pinChanged} @change=${this._pinChanged}
></paper-input> ></mwc-textfield>
</div> </div>
` `}
: ""}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-call-api-button ${google_registered
.hass=${this.hass} ? html`
.disabled=${!google_enabled} <mwc-button
@hass-api-called=${this._syncEntitiesCalled} @click=${this._handleSync}
path="cloud/google_actions/sync" .disabled=${!google_enabled || this._syncing}
> >
${this.hass.localize( ${this.hass.localize(
"ui.panel.config.cloud.account.google.sync_entities" "ui.panel.config.cloud.account.google.sync_entities"
)} )}
</ha-call-api-button> </mwc-button>
`
: ""}
<div class="spacer"></div>
<a href="/config/cloud/google-assistant"> <a href="/config/cloud/google-assistant">
<mwc-button> <mwc-button>
${this.hass.localize( ${this.hass.localize(
@ -148,22 +175,29 @@ export class CloudGooglePref extends LitElement {
`; `;
} }
private async _syncEntitiesCalled(ev: CustomEvent) { private async _handleSync() {
if (!ev.detail.success && ev.detail.response.status_code === 404) { this._syncing = true;
this._syncFailed(); try {
} await syncCloudGoogleEntities(this.hass!);
} } catch (err: any) {
private async _syncFailed() {
showAlertDialog(this, { showAlertDialog(this, {
title: this.hass.localize( title: this.hass.localize(
"ui.panel.config.cloud.account.google.not_configured_title" `ui.panel.config.cloud.account.google.${
err.status_code === 404
? "not_configured_title"
: "sync_failed_title"
}`
), ),
text: this.hass.localize( text: this.hass.localize(
"ui.panel.config.cloud.account.google.not_configured_text" `ui.panel.config.cloud.account.google.${
err.status_code === 404 ? "not_configured_text" : "sync_failed_text"
}`
), ),
}); });
fireEvent(this, "ha-refresh-cloud-status"); fireEvent(this, "ha-refresh-cloud-status");
} finally {
this._syncing = false;
}
} }
private async _enableToggleChanged(ev) { private async _enableToggleChanged(ev) {
@ -194,7 +228,7 @@ export class CloudGooglePref extends LitElement {
} }
private async _pinChanged(ev) { private async _pinChanged(ev) {
const input = ev.target as PaperInputElement; const input = ev.target as TextField;
try { try {
await updateCloudPref(this.hass, { await updateCloudPref(this.hass, {
[input.id]: input.value || null, [input.id]: input.value || null,
@ -207,7 +241,7 @@ export class CloudGooglePref extends LitElement {
"ui.panel.config.cloud.account.google.enter_pin_error" "ui.panel.config.cloud.account.google.enter_pin_error"
)} ${err.message}` )} ${err.message}`
); );
input.value = this.cloudStatus!.prefs.google_secure_devices_pin; input.value = this.cloudStatus!.prefs.google_secure_devices_pin || "";
} }
} }
@ -225,16 +259,13 @@ export class CloudGooglePref extends LitElement {
right: auto; right: auto;
left: 24px; left: 24px;
} }
ha-call-api-button { mwc-textfield {
color: var(--primary-color);
font-weight: 500;
}
paper-input {
width: 250px; width: 250px;
display: block;
margin-top: 8px;
} }
.card-actions { .card-actions {
display: flex; display: flex;
justify-content: space-between;
} }
.card-actions a { .card-actions a {
text-decoration: none; text-decoration: none;
@ -245,6 +276,10 @@ export class CloudGooglePref extends LitElement {
.secure_devices { .secure_devices {
padding-top: 8px; padding-top: 8px;
} }
.spacer {
flex-grow: 1;
}
.state-reporting { .state-reporting {
display: flex; display: flex;
margin-top: 1.5em; margin-top: 1.5em;

View File

@ -6,6 +6,7 @@ import {
mdiCloseBox, mdiCloseBox,
mdiCloseBoxMultiple, mdiCloseBoxMultiple,
} from "@mdi/js"; } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
@ -33,9 +34,14 @@ import {
updateCloudAlexaEntityConfig, updateCloudAlexaEntityConfig,
updateCloudPref, updateCloudPref,
} from "../../../../data/cloud"; } from "../../../../data/cloud";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity_registry";
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
@ -43,7 +49,7 @@ const DEFAULT_CONFIG_EXPOSE = true;
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"]; const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
@customElement("cloud-alexa") @customElement("cloud-alexa")
class CloudAlexa extends LitElement { class CloudAlexa extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() @property()
@ -53,9 +59,15 @@ class CloudAlexa extends LitElement {
@state() private _entities?: AlexaEntity[]; @state() private _entities?: AlexaEntity[];
@property() @state()
private _entityConfigs: CloudPreferences["alexa_entity_configs"] = {}; private _entityConfigs: CloudPreferences["alexa_entity_configs"] = {};
@state()
private _entityCategories?: Record<
string,
EntityRegistryEntry["entity_category"]
>;
private _popstateSyncAttached = false; private _popstateSyncAttached = false;
private _popstateReloadStatusAttached = false; private _popstateReloadStatusAttached = false;
@ -72,7 +84,7 @@ class CloudAlexa extends LitElement {
); );
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._entities === undefined) { if (this._entities === undefined || this._entityCategories === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
} }
const emptyFilter = isEmptyFilter(this.cloudStatus.alexa_entities); const emptyFilter = isEmptyFilter(this.cloudStatus.alexa_entities);
@ -99,10 +111,17 @@ class CloudAlexa extends LitElement {
should_expose: null, should_expose: null,
}; };
const isExposed = emptyFilter const isExposed = emptyFilter
? this._configIsExposed(entity.entity_id, config) ? this._configIsExposed(
entity.entity_id,
config,
this._entityCategories![entity.entity_id]
)
: filterFunc(entity.entity_id); : filterFunc(entity.entity_id);
const isDomainExposed = emptyFilter const isDomainExposed = emptyFilter
? this._configIsDomainExposed(entity.entity_id) ? this._configIsDomainExposed(
entity.entity_id,
this._entityCategories![entity.entity_id]
)
: filterFunc(entity.entity_id); : filterFunc(entity.entity_id);
if (isExposed) { if (isExposed) {
selected++; selected++;
@ -287,6 +306,23 @@ class CloudAlexa extends LitElement {
} }
} }
protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
}
private async _fetchData() { private async _fetchData() {
const entities = await fetchCloudAlexaEntities(this.hass); const entities = await fetchCloudAlexaEntities(this.hass);
entities.sort((a, b) => { entities.sort((a, b) => {
@ -305,15 +341,26 @@ class CloudAlexa extends LitElement {
fireEvent(this, "hass-more-info", { entityId }); fireEvent(this, "hass-more-info", { entityId });
} }
private _configIsDomainExposed(entityId: string) { private _configIsDomainExposed(
entityId: string,
entityCategory: EntityRegistryEntry["entity_category"] | undefined
) {
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
return this.cloudStatus.prefs.alexa_default_expose return this.cloudStatus.prefs.alexa_default_expose
? this.cloudStatus.prefs.alexa_default_expose.includes(domain) ? !entityCategory &&
this.cloudStatus.prefs.alexa_default_expose.includes(domain)
: DEFAULT_CONFIG_EXPOSE; : DEFAULT_CONFIG_EXPOSE;
} }
private _configIsExposed(entityId: string, config: AlexaEntityConfig) { private _configIsExposed(
return config.should_expose ?? this._configIsDomainExposed(entityId); entityId: string,
config: AlexaEntityConfig,
entityCategory: EntityRegistryEntry["entity_category"] | undefined
) {
return (
config.should_expose ??
this._configIsDomainExposed(entityId, entityCategory)
);
} }
private async _exposeChanged(ev: CustomEvent<ActionDetail>) { private async _exposeChanged(ev: CustomEvent<ActionDetail>) {

View File

@ -6,6 +6,7 @@ import {
mdiCloseBox, mdiCloseBox,
mdiCloseBoxMultiple, mdiCloseBoxMultiple,
} from "@mdi/js"; } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
@ -35,6 +36,10 @@ import {
updateCloudGoogleEntityConfig, updateCloudGoogleEntityConfig,
updateCloudPref, updateCloudPref,
} from "../../../../data/cloud"; } from "../../../../data/cloud";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../../../data/entity_registry";
import { import {
fetchCloudGoogleEntities, fetchCloudGoogleEntities,
GoogleEntity, GoogleEntity,
@ -42,6 +47,7 @@ import {
import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler";
import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-loading-screen";
import "../../../../layouts/hass-subpage"; import "../../../../layouts/hass-subpage";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { haStyle } from "../../../../resources/styles"; import { haStyle } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
@ -49,7 +55,7 @@ import { showToast } from "../../../../util/toast";
const DEFAULT_CONFIG_EXPOSE = true; const DEFAULT_CONFIG_EXPOSE = true;
@customElement("cloud-google-assistant") @customElement("cloud-google-assistant")
class CloudGoogleAssistant extends LitElement { class CloudGoogleAssistant extends SubscribeMixin(LitElement) {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public cloudStatus!: CloudStatusLoggedIn; @property() public cloudStatus!: CloudStatusLoggedIn;
@ -58,9 +64,15 @@ class CloudGoogleAssistant extends LitElement {
@state() private _entities?: GoogleEntity[]; @state() private _entities?: GoogleEntity[];
@property() @state()
private _entityConfigs: CloudPreferences["google_entity_configs"] = {}; private _entityConfigs: CloudPreferences["google_entity_configs"] = {};
@state()
private _entityCategories?: Record<
string,
EntityRegistryEntry["entity_category"]
>;
private _popstateSyncAttached = false; private _popstateSyncAttached = false;
private _popstateReloadStatusAttached = false; private _popstateReloadStatusAttached = false;
@ -77,7 +89,7 @@ class CloudGoogleAssistant extends LitElement {
); );
protected render(): TemplateResult { protected render(): TemplateResult {
if (this._entities === undefined) { if (this._entities === undefined || this._entityCategories === undefined) {
return html` <hass-loading-screen></hass-loading-screen> `; return html` <hass-loading-screen></hass-loading-screen> `;
} }
const emptyFilter = isEmptyFilter(this.cloudStatus.google_entities); const emptyFilter = isEmptyFilter(this.cloudStatus.google_entities);
@ -105,10 +117,17 @@ class CloudGoogleAssistant extends LitElement {
should_expose: null, should_expose: null,
}; };
const isExposed = emptyFilter const isExposed = emptyFilter
? this._configIsExposed(entity.entity_id, config) ? this._configIsExposed(
entity.entity_id,
config,
this._entityCategories![entity.entity_id]
)
: filterFunc(entity.entity_id); : filterFunc(entity.entity_id);
const isDomainExposed = emptyFilter const isDomainExposed = emptyFilter
? this._configIsDomainExposed(entity.entity_id) ? this._configIsDomainExposed(
entity.entity_id,
this._entityCategories![entity.entity_id]
)
: filterFunc(entity.entity_id); : filterFunc(entity.entity_id);
if (isExposed) { if (isExposed) {
selected++; selected++;
@ -311,15 +330,43 @@ class CloudGoogleAssistant extends LitElement {
} }
} }
private _configIsDomainExposed(entityId: string) { protected override hassSubscribe(): (
| UnsubscribeFunc
| Promise<UnsubscribeFunc>
)[] {
return [
subscribeEntityRegistry(this.hass.connection, (entries) => {
const categories = {};
for (const entry of entries) {
categories[entry.entity_id] = entry.entity_category;
}
this._entityCategories = categories;
}),
];
}
private _configIsDomainExposed(
entityId: string,
entityCategory: EntityRegistryEntry["entity_category"] | undefined
) {
const domain = computeDomain(entityId); const domain = computeDomain(entityId);
return this.cloudStatus.prefs.google_default_expose return this.cloudStatus.prefs.google_default_expose
? this.cloudStatus.prefs.google_default_expose.includes(domain) ? !entityCategory &&
this.cloudStatus.prefs.google_default_expose.includes(domain)
: DEFAULT_CONFIG_EXPOSE; : DEFAULT_CONFIG_EXPOSE;
} }
private _configIsExposed(entityId: string, config: GoogleEntityConfig) { private _configIsExposed(
return config.should_expose ?? this._configIsDomainExposed(entityId); entityId: string,
config: GoogleEntityConfig,
entityCategory: EntityRegistryEntry["entity_category"] | undefined
) {
return (
config.should_expose ??
this._configIsDomainExposed(entityId, entityCategory)
);
} }
private async _fetchData() { private async _fetchData() {

View File

@ -2115,25 +2115,30 @@
"sync_entities_error": "Failed to sync entities:", "sync_entities_error": "Failed to sync entities:",
"state_reporting_error": "Unable to {enable_disable} report state.", "state_reporting_error": "Unable to {enable_disable} report state.",
"enable": "enable", "enable": "enable",
"disable": "disable" "disable": "disable",
"not_configured_title": "Alexa is not activated",
"not_configured_text": "Before you can use Alexa, you need to activate the Home Assistant skill for Alexa in the Alexa app."
}, },
"google": { "google": {
"title": "Google Assistant", "title": "Google Assistant",
"info": "With the Google Assistant integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Google Assistant-enabled device.", "info": "With the Google Assistant integration for Home Assistant Cloud you'll be able to control all your Home Assistant devices via any Google Assistant-enabled device.",
"http_use_ssl_warning_title": "Local communication unavailable",
"http_use_ssl_warning_text": "Google devices will not be able to talk locally with Home Assistant because you have configured an SSL certificate for your HTTP integration.",
"enable_ha_skill": "Activate the Home Assistant Cloud skill for Google Assistant", "enable_ha_skill": "Activate the Home Assistant Cloud skill for Google Assistant",
"config_documentation": "Configuration documentation", "config_documentation": "Configuration documentation",
"enable_state_reporting": "Enable State Reporting", "enable_state_reporting": "Enable State Reporting",
"info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Google. This allows you to always see the latest states in the Google app.", "info_state_reporting": "If you enable state reporting, Home Assistant will send all state changes of exposed entities to Google. This speeds up voice commands and allows you to always see the latest states in the Google app.",
"security_devices": "Security Devices", "security_devices": "Security Devices",
"enter_pin_info": "Please enter a PIN to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this PIN when interacting with such devices via Google Assistant.", "enter_pin_info": "Please enter a PIN to interact with security devices. Security devices are doors, garage doors and locks. You will be asked to say/enter this PIN when interacting with such devices via Google Assistant.",
"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",
"sync_entities": "Sync Entities to Google", "sync_entities": "Sync Entities to Google",
"sync_entities_404_message": "Failed to sync your entities to Google, ask Google 'Hey Google, sync my devices' to sync your entities.",
"manage_entities": "Manage Entities", "manage_entities": "Manage Entities",
"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": "Google Assistant is not activated",
"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.",
"sync_failed_title": "Syncing failed",
"sync_failed_text": "Syncing your entities failed, try again or check the logs."
}, },
"webhooks": { "webhooks": {
"title": "Webhooks", "title": "Webhooks",