From f398692e75787ccb5a31a0ee8236583a227d3a64 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Wed, 26 Jan 2022 02:00:50 -0800 Subject: [PATCH] Improve cloud dashboard (#11422) Co-authored-by: Bram Kragten --- src/data/cloud.ts | 2 + src/data/google_assistant.ts | 3 + .../config/cloud/account/cloud-account.ts | 68 +++++++-- .../config/cloud/account/cloud-alexa-pref.ts | 94 +++++++----- .../config/cloud/account/cloud-google-pref.ts | 135 +++++++++++------- src/panels/config/cloud/alexa/cloud-alexa.ts | 65 +++++++-- .../cloud-google-assistant.ts | 65 +++++++-- src/translations/en.json | 13 +- 8 files changed, 326 insertions(+), 119 deletions(-) diff --git a/src/data/cloud.ts b/src/data/cloud.ts index a2ddc98c1b..4679d97a75 100644 --- a/src/data/cloud.ts +++ b/src/data/cloud.ts @@ -51,11 +51,13 @@ export interface CloudStatusLoggedIn { google_registered: boolean; google_entities: EntityFilter; google_domains: string[]; + alexa_registered: boolean; alexa_entities: EntityFilter; prefs: CloudPreferences; remote_domain: string | undefined; remote_connected: boolean; remote_certificate: undefined | CertificateInformation; + http_use_ssl: boolean; } export type CloudStatus = CloudStatusNotLoggedIn | CloudStatusLoggedIn; diff --git a/src/data/google_assistant.ts b/src/data/google_assistant.ts index 982f3c456a..2665be90b3 100644 --- a/src/data/google_assistant.ts +++ b/src/data/google_assistant.ts @@ -8,3 +8,6 @@ export interface GoogleEntity { export const fetchCloudGoogleEntities = (hass: HomeAssistant) => hass.callWS({ type: "cloud/google_assistant/entities" }); + +export const syncCloudGoogleEntities = (hass: HomeAssistant) => + hass.callApi("POST", "cloud/google_actions/sync"); diff --git a/src/panels/config/cloud/account/cloud-account.ts b/src/panels/config/cloud/account/cloud-account.ts index 8e98482979..63ea847326 100644 --- a/src/panels/config/cloud/account/cloud-account.ts +++ b/src/panels/config/cloud/account/cloud-account.ts @@ -1,5 +1,8 @@ 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 { mdiDotsVertical } from "@mdi/js"; import { LitElement, css, html, PropertyValues } from "lit"; import { customElement, property, state } from "lit/decorators"; 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 "../../../../components/buttons/ha-call-api-button"; import "../../../../components/ha-card"; +import "../../../../components/ha-button-menu"; +import "../../../../components/ha-icon-button"; import { cloudLogout, CloudStatusLoggedIn, @@ -21,9 +26,10 @@ import "./cloud-google-pref"; import "./cloud-remote-pref"; import "./cloud-tts-pref"; import "./cloud-webhooks"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; @customElement("cloud-account") -export class CloudAccount extends LitElement { +export class CloudAccount extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property({ type: Boolean }) public isWide = false; @@ -43,6 +49,23 @@ export class CloudAccount extends LitElement { .narrow=${this.narrow} header="Home Assistant Cloud" > + + + + + ${this.hass.localize("ui.panel.config.cloud.account.sign_out")} + + +
Home Assistant Cloud @@ -115,11 +138,6 @@ export class CloudAccount extends LitElement { )} - ${this.hass.localize( - "ui.panel.config.cloud.account.sign_out" - )}
@@ -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() { this._subscription = await fetchCloudSubscriptionInfo(this.hass); if ( @@ -211,9 +256,12 @@ export class CloudAccount extends LitElement { } } - private async _handleLogout() { - await cloudLogout(this.hass); - fireEvent(this, "ha-refresh-cloud-status"); + private async _handleMenuAction(ev: CustomEvent) { + switch (ev.detail.index) { + case 0: + await cloudLogout(this.hass); + fireEvent(this, "ha-refresh-cloud-status"); + } } _computeRTLDirection(hass) { @@ -237,7 +285,7 @@ export class CloudAccount extends LitElement { } .card-actions { display: flex; - justify-content: space-between; + flex-direction: row-reverse; } .card-actions a { text-decoration: none; diff --git a/src/panels/config/cloud/account/cloud-alexa-pref.ts b/src/panels/config/cloud/account/cloud-alexa-pref.ts index cfd2ae531e..652c5817d7 100644 --- a/src/panels/config/cloud/account/cloud-alexa-pref.ts +++ b/src/panels/config/cloud/account/cloud-alexa-pref.ts @@ -10,7 +10,7 @@ import { CloudStatusLoggedIn, updateCloudPref } from "../../../../data/cloud"; import type { HomeAssistant } from "../../../../types"; export class CloudAlexaPref extends LitElement { - @property({ attribute: false }) public hass?: HomeAssistant; + @property({ attribute: false }) public hass!: HomeAssistant; @property() public cloudStatus?: CloudStatusLoggedIn; @@ -21,6 +21,7 @@ export class CloudAlexaPref extends LitElement { return html``; } + const alexa_registered = this.cloudStatus.alexa_registered; const { alexa_enabled, alexa_report_state } = this.cloudStatus!.prefs; return html` @@ -36,33 +37,49 @@ export class CloudAlexaPref extends LitElement { >
- ${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")} - - ${alexa_enabled +

+ ${this.hass!.localize("ui.panel.config.cloud.account.alexa.info")} +

+ ${!alexa_enabled + ? "" + : !alexa_registered ? html` + + ${this.hass.localize( + "ui.panel.config.cloud.account.alexa.not_configured_text" + )} + + + + ` + : html`

${this.hass!.localize( @@ -81,18 +98,21 @@ export class CloudAlexaPref extends LitElement { "ui.panel.config.cloud.account.alexa.info_state_reporting" )}

- ` - : ""} + `}

- - ${this.hass!.localize( - "ui.panel.config.cloud.account.alexa.sync_entities" - )} - + ${alexa_registered + ? html` + + ${this.hass!.localize( + "ui.panel.config.cloud.account.alexa.sync_entities" + )} + + ` + : ""}
${this.hass.localize("ui.panel.config.cloud.account.google.info")}

- ${google_enabled && !this.cloudStatus.google_registered + ${!google_enabled + ? "" + : !google_registered ? html` ` - : ""} - ${google_enabled - ? html` + : html` + ${this.cloudStatus.http_use_ssl + ? html` + + ${this.hass.localize( + "ui.panel.config.cloud.account.google.http_use_ssl_warning_text" + )} +
${this.hass.localize( + "ui.panel.config.common.learn_more" + )} + + ` + : ""} +

${this.hass.localize( @@ -110,32 +136,33 @@ export class CloudGooglePref extends LitElement { ${this.hass.localize( "ui.panel.config.cloud.account.google.enter_pin_info" )} - + >

- ` - : ""} + `}
- - ${this.hass.localize( - "ui.panel.config.cloud.account.google.sync_entities" - )} - + ${google_registered + ? html` + + ${this.hass.localize( + "ui.panel.config.cloud.account.google.sync_entities" + )} + + ` + : ""} +
${this.hass.localize( @@ -148,24 +175,31 @@ export class CloudGooglePref extends LitElement { `; } - private async _syncEntitiesCalled(ev: CustomEvent) { - if (!ev.detail.success && ev.detail.response.status_code === 404) { - this._syncFailed(); + private async _handleSync() { + this._syncing = true; + try { + await syncCloudGoogleEntities(this.hass!); + } catch (err: any) { + showAlertDialog(this, { + title: this.hass.localize( + `ui.panel.config.cloud.account.google.${ + err.status_code === 404 + ? "not_configured_title" + : "sync_failed_title" + }` + ), + text: this.hass.localize( + `ui.panel.config.cloud.account.google.${ + err.status_code === 404 ? "not_configured_text" : "sync_failed_text" + }` + ), + }); + fireEvent(this, "ha-refresh-cloud-status"); + } finally { + this._syncing = false; } } - private async _syncFailed() { - showAlertDialog(this, { - title: this.hass.localize( - "ui.panel.config.cloud.account.google.not_configured_title" - ), - text: this.hass.localize( - "ui.panel.config.cloud.account.google.not_configured_text" - ), - }); - fireEvent(this, "ha-refresh-cloud-status"); - } - private async _enableToggleChanged(ev) { const toggle = ev.target as HaSwitch; try { @@ -194,7 +228,7 @@ export class CloudGooglePref extends LitElement { } private async _pinChanged(ev) { - const input = ev.target as PaperInputElement; + const input = ev.target as TextField; try { await updateCloudPref(this.hass, { [input.id]: input.value || null, @@ -207,7 +241,7 @@ export class CloudGooglePref extends LitElement { "ui.panel.config.cloud.account.google.enter_pin_error" )} ${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; left: 24px; } - ha-call-api-button { - color: var(--primary-color); - font-weight: 500; - } - paper-input { + mwc-textfield { width: 250px; + display: block; + margin-top: 8px; } .card-actions { display: flex; - justify-content: space-between; } .card-actions a { text-decoration: none; @@ -245,6 +276,10 @@ export class CloudGooglePref extends LitElement { .secure_devices { padding-top: 8px; } + .spacer { + flex-grow: 1; + } + .state-reporting { display: flex; margin-top: 1.5em; diff --git a/src/panels/config/cloud/alexa/cloud-alexa.ts b/src/panels/config/cloud/alexa/cloud-alexa.ts index 90f43f83da..7324805768 100644 --- a/src/panels/config/cloud/alexa/cloud-alexa.ts +++ b/src/panels/config/cloud/alexa/cloud-alexa.ts @@ -6,6 +6,7 @@ import { mdiCloseBox, mdiCloseBoxMultiple, } from "@mdi/js"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; @@ -33,9 +34,14 @@ import { updateCloudAlexaEntityConfig, updateCloudPref, } from "../../../../data/cloud"; +import { + EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../../../data/entity_registry"; import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-subpage"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; @@ -43,7 +49,7 @@ const DEFAULT_CONFIG_EXPOSE = true; const IGNORE_INTERFACES = ["Alexa.EndpointHealth"]; @customElement("cloud-alexa") -class CloudAlexa extends LitElement { +class CloudAlexa extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property() @@ -53,9 +59,15 @@ class CloudAlexa extends LitElement { @state() private _entities?: AlexaEntity[]; - @property() + @state() private _entityConfigs: CloudPreferences["alexa_entity_configs"] = {}; + @state() + private _entityCategories?: Record< + string, + EntityRegistryEntry["entity_category"] + >; + private _popstateSyncAttached = false; private _popstateReloadStatusAttached = false; @@ -72,7 +84,7 @@ class CloudAlexa extends LitElement { ); protected render(): TemplateResult { - if (this._entities === undefined) { + if (this._entities === undefined || this._entityCategories === undefined) { return html` `; } const emptyFilter = isEmptyFilter(this.cloudStatus.alexa_entities); @@ -99,10 +111,17 @@ class CloudAlexa extends LitElement { should_expose: null, }; const isExposed = emptyFilter - ? this._configIsExposed(entity.entity_id, config) + ? this._configIsExposed( + entity.entity_id, + config, + this._entityCategories![entity.entity_id] + ) : filterFunc(entity.entity_id); const isDomainExposed = emptyFilter - ? this._configIsDomainExposed(entity.entity_id) + ? this._configIsDomainExposed( + entity.entity_id, + this._entityCategories![entity.entity_id] + ) : filterFunc(entity.entity_id); if (isExposed) { selected++; @@ -287,6 +306,23 @@ class CloudAlexa extends LitElement { } } + protected override hassSubscribe(): ( + | UnsubscribeFunc + | Promise + )[] { + 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() { const entities = await fetchCloudAlexaEntities(this.hass); entities.sort((a, b) => { @@ -305,15 +341,26 @@ class CloudAlexa extends LitElement { fireEvent(this, "hass-more-info", { entityId }); } - private _configIsDomainExposed(entityId: string) { + private _configIsDomainExposed( + entityId: string, + entityCategory: EntityRegistryEntry["entity_category"] | undefined + ) { const domain = computeDomain(entityId); 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; } - private _configIsExposed(entityId: string, config: AlexaEntityConfig) { - return config.should_expose ?? this._configIsDomainExposed(entityId); + private _configIsExposed( + entityId: string, + config: AlexaEntityConfig, + entityCategory: EntityRegistryEntry["entity_category"] | undefined + ) { + return ( + config.should_expose ?? + this._configIsDomainExposed(entityId, entityCategory) + ); } private async _exposeChanged(ev: CustomEvent) { diff --git a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts index 51ffa4360a..a14d3e7623 100644 --- a/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts +++ b/src/panels/config/cloud/google-assistant/cloud-google-assistant.ts @@ -6,6 +6,7 @@ import { mdiCloseBox, mdiCloseBoxMultiple, } from "@mdi/js"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; @@ -35,6 +36,10 @@ import { updateCloudGoogleEntityConfig, updateCloudPref, } from "../../../../data/cloud"; +import { + EntityRegistryEntry, + subscribeEntityRegistry, +} from "../../../../data/entity_registry"; import { fetchCloudGoogleEntities, GoogleEntity, @@ -42,6 +47,7 @@ import { import { showDomainTogglerDialog } from "../../../../dialogs/domain-toggler/show-dialog-domain-toggler"; import "../../../../layouts/hass-loading-screen"; import "../../../../layouts/hass-subpage"; +import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; import { haStyle } from "../../../../resources/styles"; import type { HomeAssistant } from "../../../../types"; import { showToast } from "../../../../util/toast"; @@ -49,7 +55,7 @@ import { showToast } from "../../../../util/toast"; const DEFAULT_CONFIG_EXPOSE = true; @customElement("cloud-google-assistant") -class CloudGoogleAssistant extends LitElement { +class CloudGoogleAssistant extends SubscribeMixin(LitElement) { @property({ attribute: false }) public hass!: HomeAssistant; @property() public cloudStatus!: CloudStatusLoggedIn; @@ -58,9 +64,15 @@ class CloudGoogleAssistant extends LitElement { @state() private _entities?: GoogleEntity[]; - @property() + @state() private _entityConfigs: CloudPreferences["google_entity_configs"] = {}; + @state() + private _entityCategories?: Record< + string, + EntityRegistryEntry["entity_category"] + >; + private _popstateSyncAttached = false; private _popstateReloadStatusAttached = false; @@ -77,7 +89,7 @@ class CloudGoogleAssistant extends LitElement { ); protected render(): TemplateResult { - if (this._entities === undefined) { + if (this._entities === undefined || this._entityCategories === undefined) { return html` `; } const emptyFilter = isEmptyFilter(this.cloudStatus.google_entities); @@ -105,10 +117,17 @@ class CloudGoogleAssistant extends LitElement { should_expose: null, }; const isExposed = emptyFilter - ? this._configIsExposed(entity.entity_id, config) + ? this._configIsExposed( + entity.entity_id, + config, + this._entityCategories![entity.entity_id] + ) : filterFunc(entity.entity_id); const isDomainExposed = emptyFilter - ? this._configIsDomainExposed(entity.entity_id) + ? this._configIsDomainExposed( + entity.entity_id, + this._entityCategories![entity.entity_id] + ) : filterFunc(entity.entity_id); if (isExposed) { selected++; @@ -311,15 +330,43 @@ class CloudGoogleAssistant extends LitElement { } } - private _configIsDomainExposed(entityId: string) { + protected override hassSubscribe(): ( + | UnsubscribeFunc + | Promise + )[] { + 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); 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; } - private _configIsExposed(entityId: string, config: GoogleEntityConfig) { - return config.should_expose ?? this._configIsDomainExposed(entityId); + private _configIsExposed( + entityId: string, + config: GoogleEntityConfig, + entityCategory: EntityRegistryEntry["entity_category"] | undefined + ) { + return ( + config.should_expose ?? + this._configIsDomainExposed(entityId, entityCategory) + ); } private async _fetchData() { diff --git a/src/translations/en.json b/src/translations/en.json index 0fa20bc789..5fec66e0fc 100755 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -2115,25 +2115,30 @@ "sync_entities_error": "Failed to sync entities:", "state_reporting_error": "Unable to {enable_disable} report state.", "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": { "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.", + "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", "config_documentation": "Configuration documentation", "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", "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", "enter_pin_hint": "Enter a PIN to use security devices", "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", "enter_pin_error": "Unable to store PIN:", "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": { "title": "Webhooks",