Allow exposing domains in cloud (#6696)

* Allow exposing domains in cloud

https://github.com/home-assistant/core/pull/39216

* Update styles

* Lint

* Apply suggestions from code review

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

* Comments

* Add translations

* Apply suggestions from code review

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
This commit is contained in:
Bram Kragten 2020-09-01 10:23:59 +02:00 committed by GitHub
parent 994a397231
commit 5292119e6e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 551 additions and 243 deletions

View File

@ -9,14 +9,14 @@ interface CloudStatusBase {
}
export interface GoogleEntityConfig {
should_expose?: boolean;
should_expose?: boolean | null;
override_name?: string;
aliases?: string[];
disable_2fa?: boolean;
}
export interface AlexaEntityConfig {
should_expose?: boolean;
should_expose?: boolean | null;
}
export interface CertificateInformation {
@ -31,9 +31,11 @@ export interface CloudPreferences {
remote_enabled: boolean;
google_secure_devices_pin: string | undefined;
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;
};
@ -106,8 +108,10 @@ export const updateCloudPref = (
prefs: {
google_enabled?: CloudPreferences["google_enabled"];
alexa_enabled?: CloudPreferences["alexa_enabled"];
alexa_default_expose?: CloudPreferences["alexa_default_expose"];
alexa_report_state?: CloudPreferences["alexa_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"];
}
) =>

View File

@ -4,27 +4,35 @@ import {
CSSResultArray,
customElement,
html,
LitElement,
internalProperty,
LitElement,
TemplateResult,
} from "lit-element";
import "../../components/dialog/ha-paper-dialog";
import { fireEvent } from "../../common/dom/fire_event";
import { createCloseHeading } from "../../components/ha-dialog";
import "../../components/ha-switch";
import "../../components/ha-formfield";
import { domainToName } from "../../data/integration";
import { PolymerChangedEvent } from "../../polymer-types";
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 {
class DomainTogglerDialog extends LitElement implements HassDialog {
public hass!: HomeAssistant;
@internalProperty() private _params?: HaDomainTogglerDialogParams;
public async showDialog(params: HaDomainTogglerDialogParams): Promise<void> {
public showDialog(params: HaDomainTogglerDialogParams): void {
this._params = params;
}
public closeDialog() {
this._params = undefined;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
protected render(): TemplateResult {
if (!this._params) {
return html``;
@ -35,46 +43,47 @@ class DomainTogglerDialog extends LitElement {
.sort();
return html`
<ha-paper-dialog
with-backdrop
opened
@opened-changed=${this._openedChanged}
<ha-dialog
open
@closed=${this.closeDialog}
scrimClickAction
escapeKeyAction
hideActions
.heading=${createCloseHeading(
this.hass,
this.hass.localize("ui.dialogs.domain_toggler.title")
)}
>
<h2>
${this.hass.localize("ui.dialogs.domain_toggler.title")}
</h2>
<div>
${domains.map(
(domain) =>
html`
<div>${domain[0]}</div>
<mwc-button .domain=${domain[1]} @click=${this._handleOff}>
${this.hass.localize("state.default.off")}
</mwc-button>
<mwc-button .domain=${domain[1]} @click=${this._handleOn}>
${this.hass.localize("state.default.on")}
<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-paper-dialog>
</ha-dialog>
`;
}
private _openedChanged(ev: PolymerChangedEvent<boolean>): void {
// Closed dialog by clicking on the overlay
if (!ev.detail.value) {
this._params = undefined;
}
}
private _handleOff(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, false);
private _handleSwitch(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, ev.target.checked);
ev.currentTarget.blur();
}
private _handleOn(ev) {
this._params!.toggleDomain(ev.currentTarget.domain, true);
private _handleReset(ev) {
this._params!.resetDomain(ev.currentTarget.domain);
ev.currentTarget.blur();
}
@ -82,8 +91,8 @@ class DomainTogglerDialog extends LitElement {
return [
haStyleDialog,
css`
ha-paper-dialog {
max-width: 500px;
ha-dialog {
--mdc-dialog-max-width: 500px;
}
div {
display: grid;

View File

@ -2,7 +2,9 @@ import { fireEvent } from "../../common/dom/fire_event";
export interface HaDomainTogglerDialogParams {
domains: string[];
exposedDomains: string[] | null;
toggleDomain: (domain: string, turnOn: boolean) => void;
resetDomain: (domain: string) => void;
}
export const loadDomainTogglerDialog = () =>

View File

@ -1,14 +1,22 @@
import "../../../../components/ha-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import {
mdiCheckboxMarked,
mdiCheckboxMultipleMarked,
mdiCloseBox,
mdiCloseBoxMultiple,
} from "@mdi/js";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
@ -20,31 +28,28 @@ import {
} from "../../../../common/entity/entity_filter";
import { compare } 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 type { HaSwitch } from "../../../../components/ha-switch";
import { AlexaEntity, fetchCloudAlexaEntities } from "../../../../data/alexa";
import {
AlexaEntityConfig,
CloudPreferences,
CloudStatusLoggedIn,
updateCloudAlexaEntityConfig,
updateCloudPref,
} from "../../../../data/cloud";
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";
import "../../../../components/ha-formfield";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
const DEFAULT_CONFIG_EXPOSE = true;
const IGNORE_INTERFACES = ["Alexa.EndpointHealth"];
const configIsExposed = (config: AlexaEntityConfig) =>
config.should_expose === undefined
? DEFAULT_CONFIG_EXPOSE
: config.should_expose;
@customElement("cloud-alexa")
class CloudAlexa extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -100,7 +105,10 @@ class CloudAlexa extends LitElement {
const stateObj = this.hass.states[entity.entity_id];
const config = this._entityConfigs[entity.entity_id] || {};
const isExposed = emptyFilter
? configIsExposed(config)
? this._configIsExposed(entity.entity_id, config)
: filterFunc(entity.entity_id);
const isDomainExposed = emptyFilter
? this._configIsDomainExposed(entity.entity_id)
: filterFunc(entity.entity_id);
if (isExposed) {
selected++;
@ -117,33 +125,80 @@ class CloudAlexa extends LitElement {
target.push(html`
<ha-card>
<div class="card-content">
<state-info
.hass=${this.hass}
.stateObj=${stateObj}
secondary-line
@click=${this._showMoreInfo}
>
${entity.interfaces
.filter((ifc) => !IGNORE_INTERFACES.includes(ifc))
.map((ifc) =>
ifc.replace("Alexa.", "").replace("Controller", "")
)
.join(", ")}
</state-info>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.cloud.alexa.expose"
)}
.dir=${computeRTLDirection(this.hass!)}
>
<ha-switch
.entityId=${entity.entity_id}
.disabled=${!emptyFilter}
.checked=${isExposed}
@change=${this._exposeChanged}
<div class="top-line">
<state-info
.hass=${this.hass}
.stateObj=${stateObj}
secondary-line
@click=${this._showMoreInfo}
>
</ha-switch>
</ha-formfield>
${entity.interfaces
.filter((ifc) => !IGNORE_INTERFACES.includes(ifc))
.map((ifc) => ifc.replace(/(Alexa.|Controller)/g, ""))
.join(", ")}
</state-info>
<ha-button-menu
corner="BOTTOM_START"
.entityId=${stateObj.entity_id}
@action=${this._exposeChanged}
>
<mwc-icon-button
slot="trigger"
class=${classMap({
exposed: isExposed!,
"not-exposed": !isExposed,
})}
.title=${this.hass!.localize(
"ui.panel.config.cloud.alexa.expose"
)}
>
<ha-svg-icon
.path=${config.should_expose !== null
? isExposed
? mdiCheckboxMarked
: mdiCloseBox
: isDomainExposed
? mdiCheckboxMultipleMarked
: mdiCloseBoxMultiple}
></ha-svg-icon>
</mwc-icon-button>
<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>
`);
@ -157,17 +212,16 @@ class CloudAlexa extends LitElement {
<hass-subpage header="${this.hass!.localize(
"ui.panel.config.cloud.alexa.title"
)}">
<span slot="toolbar-icon">
${selected}${!this.narrow ? html` selected ` : ""}
</span>
${
emptyFilter
? html`
<ha-icon-button
<mwc-button
slot="toolbar-icon"
icon="hass:tune"
@click=${this._openDomainToggler}
></ha-icon-button>
>${this.hass!.localize(
"ui.panel.config.cloud.alexa.manage_domains"
)}</mwc-button
>
`
: ""
}
@ -183,11 +237,20 @@ class CloudAlexa extends LitElement {
${
exposedCards.length > 0
? html`
<h1>
${this.hass!.localize(
"ui.panel.config.cloud.alexa.exposed_entities"
)}
</h1>
<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>
`
: ""
@ -195,11 +258,20 @@ class CloudAlexa extends LitElement {
${
notExposedCards.length > 0
? html`
<h1>
${this.hass!.localize(
"ui.panel.config.cloud.alexa.not_exposed_entities"
)}
</h1>
<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>
`
: ""
@ -239,17 +311,37 @@ class CloudAlexa extends LitElement {
fireEvent(this, "hass-more-info", { entityId });
}
private async _exposeChanged(ev: Event) {
const entityId = (ev.currentTarget as any).entityId;
const newExposed = (ev.target as HaSwitch).checked;
await this._updateExposed(entityId, newExposed);
private _configIsDomainExposed(entityId: string) {
const domain = computeDomain(entityId);
return this.cloudStatus.prefs.alexa_default_expose
? this.cloudStatus.prefs.alexa_default_expose.includes(domain)
: DEFAULT_CONFIG_EXPOSE;
}
private async _updateExposed(entityId: string, newExposed: boolean) {
const curExposed = configIsExposed(this._entityConfigs[entityId] || {});
if (newExposed === curExposed) {
return;
private _configIsExposed(entityId: string, config: AlexaEntityConfig) {
return config.should_expose === null
? this._configIsDomainExposed(entityId)
: config.should_expose;
}
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,
});
@ -274,16 +366,46 @@ class CloudAlexa extends LitElement {
domains: this._entities!.map((entity) =>
computeDomain(entity.entity_id)
).filter((value, idx, self) => self.indexOf(value) === idx),
toggleDomain: (domain, turnOn) => {
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, turnOn);
this._updateExposed(entity.entity_id, null);
}
});
},
});
}
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;
@ -306,61 +428,75 @@ class CloudAlexa extends LitElement {
this._popstateSyncAttached = true;
// Cache parent because by the time popstate happens,
// this element is detached
// const parent = this.parentElement!;
window.addEventListener(
"popstate",
() => {
// We don't have anything yet.
// showToast(parent, { message: "Synchronizing changes to Google." });
// cloudSyncGoogleAssistant(this.hass);
},
{ once: true }
);
}
static get styles(): CSSResult {
return css`
.banner {
color: var(--primary-text-color);
background-color: var(
--ha-card-background,
var(--card-background-color, white)
);
padding: 16px 8px;
text-align: center;
}
h1 {
color: var(--primary-text-color);
font-size: 24px;
letter-spacing: -0.012em;
margin-bottom: 0;
padding: 0 8px;
}
.content {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
grid-gap: 8px 8px;
padding: 8px;
}
ha-switch {
clear: both;
}
.card-content {
padding-bottom: 12px;
}
state-info {
cursor: pointer;
}
ha-switch {
padding: 8px 0;
}
@media all and (max-width: 450px) {
ha-card {
max-width: 100%;
static get styles(): CSSResult[] {
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;
}
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%;
}
}
`,
];
}
}

View File

@ -1,14 +1,22 @@
import "../../../../components/ha-icon-button";
import { ActionDetail } from "@material/mwc-list/mwc-list-foundation";
import "@material/mwc-list/mwc-list-item";
import {
mdiCheckboxMarked,
mdiCheckboxMultipleMarked,
mdiCloseBox,
mdiCloseBoxMultiple,
} from "@mdi/js";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
internalProperty,
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDomain } from "../../../../common/entity/compute_domain";
@ -19,8 +27,12 @@ import {
isEmptyFilter,
} from "../../../../common/entity/entity_filter";
import { compare } 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 {
@ -29,6 +41,7 @@ import {
cloudSyncGoogleAssistant,
GoogleEntityConfig,
updateCloudGoogleEntityConfig,
updateCloudPref,
} from "../../../../data/cloud";
import {
fetchCloudGoogleEntities,
@ -37,18 +50,12 @@ import {
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";
import { showToast } from "../../../../util/toast";
import "../../../../components/ha-formfield";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
const DEFAULT_CONFIG_EXPOSE = true;
const configIsExposed = (config: GoogleEntityConfig) =>
config.should_expose === undefined
? DEFAULT_CONFIG_EXPOSE
: config.should_expose;
@customElement("cloud-google-assistant")
class CloudGoogleAssistant extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -104,7 +111,10 @@ class CloudGoogleAssistant extends LitElement {
const stateObj = this.hass.states[entity.entity_id];
const config = this._entityConfigs[entity.entity_id] || {};
const isExposed = emptyFilter
? configIsExposed(config)
? this._configIsExposed(entity.entity_id, config)
: filterFunc(entity.entity_id);
const isDomainExposed = emptyFilter
? this._configIsDomainExposed(entity.entity_id)
: filterFunc(entity.entity_id);
if (isExposed) {
selected++;
@ -121,31 +131,78 @@ class CloudGoogleAssistant extends LitElement {
target.push(html`
<ha-card>
<div class="card-content">
<state-info
.hass=${this.hass}
.stateObj=${stateObj}
secondary-line
@click=${this._showMoreInfo}
>
${entity.traits
.map((trait) => trait.substr(trait.lastIndexOf(".") + 1))
.join(", ")}
</state-info>
<div>
<ha-formfield
.label=${this.hass!.localize(
"ui.panel.config.cloud.google.expose"
)}
.dir=${dir}
<div class="top-line">
<state-info
.hass=${this.hass}
.stateObj=${stateObj}
secondary-line
@click=${this._showMoreInfo}
>
<ha-switch
.entityId=${entity.entity_id}
.disabled=${!emptyFilter}
.checked=${isExposed}
@change=${this._exposeChanged}
${entity.traits
.map((trait) => trait.substr(trait.lastIndexOf(".") + 1))
.join(", ")}
</state-info>
<ha-button-menu
corner="BOTTOM_START"
.entityId=${stateObj.entity_id}
@action=${this._exposeChanged}
>
<mwc-icon-button
slot="trigger"
class=${classMap({
exposed: isExposed!,
"not-exposed": !isExposed,
})}
.title=${this.hass!.localize(
"ui.panel.config.cloud.google.expose"
)}
>
</ha-switch>
</ha-formfield>
<ha-svg-icon
.path=${config.should_expose !== null
? isExposed
? mdiCheckboxMarked
: mdiCloseBox
: isDomainExposed
? mdiCheckboxMultipleMarked
: mdiCloseBoxMultiple}
></ha-svg-icon>
</mwc-icon-button>
<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`
@ -178,17 +235,16 @@ class CloudGoogleAssistant extends LitElement {
<hass-subpage header="${this.hass!.localize(
"ui.panel.config.cloud.google.title"
)}">
<span slot="toolbar-icon">
${selected}${!this.narrow ? html` selected ` : ""}
</span>
${
emptyFilter
? html`
<ha-icon-button
<mwc-button
slot="toolbar-icon"
icon="hass:tune"
@click=${this._openDomainToggler}
></ha-icon-button>
>${this.hass!.localize(
"ui.panel.config.cloud.google.manage_domains"
)}</mwc-button
>
`
: ""
}
@ -204,11 +260,20 @@ class CloudGoogleAssistant extends LitElement {
${
exposedCards.length > 0
? html`
<h1>
${this.hass!.localize(
"ui.panel.config.cloud.google.exposed_entities"
)}
</h1>
<div class="header">
<h3>
${this.hass!.localize(
"ui.panel.config.cloud.google.exposed_entities"
)}
</h3>
${!this.narrow
? this.hass!.localize(
"ui.panel.config.cloud.alexa.exposed",
"selected",
selected
)
: selected}
</div>
<div class="content">${exposedCards}</div>
`
: ""
@ -216,11 +281,20 @@ class CloudGoogleAssistant extends LitElement {
${
notExposedCards.length > 0
? html`
<h1>
${this.hass!.localize(
"ui.panel.config.cloud.google.not_exposed_entities"
)}
</h1>
<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.alexa.not_exposed",
"selected",
this._entities.length - selected
)
: this._entities.length - selected}
</div>
<div class="content">${notExposedCards}</div>
`
: ""
@ -242,6 +316,19 @@ class CloudGoogleAssistant extends LitElement {
}
}
private _configIsDomainExposed(entityId: string) {
const domain = computeDomain(entityId);
return this.cloudStatus.prefs.google_default_expose
? this.cloudStatus.prefs.google_default_expose.includes(domain)
: DEFAULT_CONFIG_EXPOSE;
}
private _configIsExposed(entityId: string, config: GoogleEntityConfig) {
return config.should_expose === null
? this._configIsDomainExposed(entityId)
: config.should_expose;
}
private async _fetchData() {
const entities = await fetchCloudGoogleEntities(this.hass);
entities.sort((a, b) => {
@ -260,17 +347,24 @@ class CloudGoogleAssistant extends LitElement {
fireEvent(this, "hass-more-info", { entityId });
}
private async _exposeChanged(ev: Event) {
private async _exposeChanged(ev: CustomEvent<ActionDetail>) {
const entityId = (ev.currentTarget as any).entityId;
const newExposed = (ev.target as HaSwitch).checked;
await this._updateExposed(entityId, newExposed);
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) {
const curExposed = configIsExposed(this._entityConfigs[entityId] || {});
if (newExposed === curExposed) {
return;
}
private async _updateExposed(entityId: string, newExposed: boolean | null) {
await this._updateConfig(entityId, {
should_expose: newExposed,
});
@ -309,16 +403,46 @@ class CloudGoogleAssistant extends LitElement {
domains: this._entities!.map((entity) =>
computeDomain(entity.entity_id)
).filter((value, idx, self) => self.indexOf(value) === idx),
toggleDomain: (domain, turnOn) => {
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, turnOn);
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;
@ -356,46 +480,66 @@ class CloudGoogleAssistant extends LitElement {
);
}
static get styles(): CSSResult {
return css`
.banner {
color: var(--primary-text-color);
background-color: var(
--ha-card-background,
var(--card-background-color, white)
);
padding: 16px 8px;
text-align: center;
}
h1 {
color: var(--primary-text-color);
font-size: 24px;
letter-spacing: -0.012em;
margin-bottom: 0;
padding: 0 8px;
}
.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;
}
@media all and (max-width: 450px) {
ha-card {
max-width: 100%;
static get styles(): CSSResult[] {
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;
}
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%;
}
}
`,
];
}
}

View File

@ -526,7 +526,8 @@
}
},
"domain_toggler": {
"title": "Toggle Domains"
"title": "Toggle Domains",
"reset_entities": "Reset Entities"
},
"mqtt_device_debug_info": {
"title": "{device} debug info",
@ -1383,7 +1384,13 @@
"title": "Alexa",
"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",
"not_exposed_entities": "Not exposed entities",
"manage_domains": "Manage domains",
"expose_entity": "Expose entity",
"dont_expose_entity": "Don't expose entity",
"follow_domain": "Follow domain",
"exposed": "{selected} exposed",
"not_exposed": "{selected} not exposed",
"expose": "Expose to Alexa"
},
"dialog_certificate": {
@ -1399,7 +1406,13 @@
"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",
"not_exposed_entities": "Not exposed entities",
"manage_domains": "Manage domains",
"expose_entity": "Expose entity",
"dont_expose_entity": "Don't expose entity",
"follow_domain": "Follow domain",
"exposed": "{selected} exposed",
"not_exposed": "{selected} not exposed",
"sync_to_google": "Synchronizing changes to Google."
},
"dialog_cloudhook": {