mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 09:16:38 +00:00
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:
parent
994a397231
commit
5292119e6e
@ -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"];
|
||||
}
|
||||
) =>
|
||||
|
@ -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;
|
||||
|
@ -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 = () =>
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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%;
|
||||
}
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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": {
|
||||
|
Loading…
x
Reference in New Issue
Block a user