mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 11:46:42 +00:00
Add UI to manage Google Entities exposed to Cloud (#3224)
* Add UI to manage Google Entities exposed to Cloud * Add selected count
This commit is contained in:
parent
b2b18cb814
commit
1ad9d2e54c
@ -2,6 +2,20 @@ import computeDomain from "./compute_domain";
|
|||||||
|
|
||||||
export type FilterFunc = (entityId: string) => boolean;
|
export type FilterFunc = (entityId: string) => boolean;
|
||||||
|
|
||||||
|
export interface EntityFilter {
|
||||||
|
include_domains: string[];
|
||||||
|
include_entities: string[];
|
||||||
|
exclude_domains: string[];
|
||||||
|
exclude_entities: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export const isEmptyFilter = (filter: EntityFilter) =>
|
||||||
|
filter.include_domains.length +
|
||||||
|
filter.include_entities.length +
|
||||||
|
filter.exclude_domains.length +
|
||||||
|
filter.exclude_entities.length ===
|
||||||
|
0;
|
||||||
|
|
||||||
export const generateFilter = (
|
export const generateFilter = (
|
||||||
includeDomains?: string[],
|
includeDomains?: string[],
|
||||||
includeEntities?: string[],
|
includeEntities?: string[],
|
||||||
|
@ -91,13 +91,12 @@ class StateInfo extends PolymerElement {
|
|||||||
|
|
||||||
static get properties() {
|
static get properties() {
|
||||||
return {
|
return {
|
||||||
detailed: {
|
|
||||||
type: Boolean,
|
|
||||||
value: false,
|
|
||||||
},
|
|
||||||
hass: Object,
|
hass: Object,
|
||||||
stateObj: Object,
|
stateObj: Object,
|
||||||
inDialog: Boolean,
|
inDialog: {
|
||||||
|
type: Boolean,
|
||||||
|
value: () => false,
|
||||||
|
},
|
||||||
rtl: {
|
rtl: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflectToAttribute: true,
|
reflectToAttribute: true,
|
||||||
|
@ -1,27 +1,33 @@
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
import { EntityFilter } from "../common/entity/entity_filter";
|
||||||
|
|
||||||
export interface EntityFilter {
|
|
||||||
include_domains: string[];
|
|
||||||
include_entities: string[];
|
|
||||||
exclude_domains: string[];
|
|
||||||
exclude_entities: string[];
|
|
||||||
}
|
|
||||||
interface CloudStatusBase {
|
interface CloudStatusBase {
|
||||||
logged_in: boolean;
|
logged_in: boolean;
|
||||||
cloud: "disconnected" | "connecting" | "connected";
|
cloud: "disconnected" | "connecting" | "connected";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GoogleEntityConfig {
|
||||||
|
should_expose?: boolean;
|
||||||
|
override_name?: string;
|
||||||
|
aliases?: string[];
|
||||||
|
disable_2fa?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export interface CertificateInformation {
|
export interface CertificateInformation {
|
||||||
common_name: string;
|
common_name: string;
|
||||||
expire_date: string;
|
expire_date: string;
|
||||||
fingerprint: string;
|
fingerprint: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface CloudPreferences {
|
export interface CloudPreferences {
|
||||||
google_enabled: boolean;
|
google_enabled: boolean;
|
||||||
alexa_enabled: boolean;
|
alexa_enabled: boolean;
|
||||||
|
remote_enabled: boolean;
|
||||||
google_secure_devices_pin: string | undefined;
|
google_secure_devices_pin: string | undefined;
|
||||||
cloudhooks: { [webhookId: string]: CloudWebhook };
|
cloudhooks: { [webhookId: string]: CloudWebhook };
|
||||||
|
google_entity_configs: {
|
||||||
|
[entityId: string]: GoogleEntityConfig;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type CloudStatusLoggedIn = CloudStatusBase & {
|
export type CloudStatusLoggedIn = CloudStatusBase & {
|
||||||
@ -49,6 +55,12 @@ export interface CloudWebhook {
|
|||||||
managed?: boolean;
|
managed?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface GoogleEntity {
|
||||||
|
entity_id: string;
|
||||||
|
traits: string[];
|
||||||
|
might_2fa: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
export const fetchCloudStatus = (hass: HomeAssistant) =>
|
export const fetchCloudStatus = (hass: HomeAssistant) =>
|
||||||
hass.callWS<CloudStatus>({ type: "cloud/status" });
|
hass.callWS<CloudStatus>({ type: "cloud/status" });
|
||||||
|
|
||||||
@ -89,3 +101,20 @@ export const updateCloudPref = (
|
|||||||
type: "cloud/update_prefs",
|
type: "cloud/update_prefs",
|
||||||
...prefs,
|
...prefs,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const fetchCloudGoogleEntities = (hass: HomeAssistant) =>
|
||||||
|
hass.callWS<GoogleEntity[]>({ type: "cloud/google_assistant/entities" });
|
||||||
|
|
||||||
|
export const updateCloudGoogleEntityConfig = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
entityId: string,
|
||||||
|
values: GoogleEntityConfig
|
||||||
|
) =>
|
||||||
|
hass.callWS<GoogleEntityConfig>({
|
||||||
|
type: "cloud/google_assistant/entities/update",
|
||||||
|
entity_id: entityId,
|
||||||
|
...values,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const cloudSyncGoogleAssistant = (hass: HomeAssistant) =>
|
||||||
|
hass.callApi("POST", "cloud/google_actions/sync");
|
||||||
|
@ -16,8 +16,8 @@ import computeStateName from "../../../common/entity/compute_state_name";
|
|||||||
import {
|
import {
|
||||||
FilterFunc,
|
FilterFunc,
|
||||||
generateFilter,
|
generateFilter,
|
||||||
|
EntityFilter,
|
||||||
} from "../../../common/entity/entity_filter";
|
} from "../../../common/entity/entity_filter";
|
||||||
import { EntityFilter } from "../../../data/cloud";
|
|
||||||
|
|
||||||
export class CloudExposedEntities extends LitElement {
|
export class CloudExposedEntities extends LitElement {
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
|
@ -16,7 +16,6 @@ import "../../../components/ha-card";
|
|||||||
|
|
||||||
import { fireEvent } from "../../../common/dom/fire_event";
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import "./cloud-exposed-entities";
|
|
||||||
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
|
import { CloudStatusLoggedIn, updateCloudPref } from "../../../data/cloud";
|
||||||
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
import { PaperInputElement } from "@polymer/paper-input/paper-input";
|
||||||
|
|
||||||
@ -89,12 +88,6 @@ export class CloudGooglePref extends LitElement {
|
|||||||
@change="${this._pinChanged}"
|
@change="${this._pinChanged}"
|
||||||
></paper-input>
|
></paper-input>
|
||||||
</div>
|
</div>
|
||||||
<p>Exposed entities:</p>
|
|
||||||
<cloud-exposed-entities
|
|
||||||
.hass="${this.hass}"
|
|
||||||
.filter="${this.cloudStatus!.google_entities}"
|
|
||||||
.supportedDomains="${this.cloudStatus!.google_domains}"
|
|
||||||
></cloud-exposed-entities>
|
|
||||||
`
|
`
|
||||||
: ""}
|
: ""}
|
||||||
</div>
|
</div>
|
||||||
@ -103,8 +96,12 @@ export class CloudGooglePref extends LitElement {
|
|||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.disabled="${!google_enabled}"
|
.disabled="${!google_enabled}"
|
||||||
path="cloud/google_actions/sync"
|
path="cloud/google_actions/sync"
|
||||||
>Sync devices</ha-call-api-button
|
|
||||||
>
|
>
|
||||||
|
Sync entities to Google
|
||||||
|
</ha-call-api-button>
|
||||||
|
<a href="/config/cloud/google-assistant">
|
||||||
|
<mwc-button>Manage Entities</mwc-button>
|
||||||
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
@ -154,6 +151,12 @@ export class CloudGooglePref extends LitElement {
|
|||||||
paper-input {
|
paper-input {
|
||||||
width: 200px;
|
width: 200px;
|
||||||
}
|
}
|
||||||
|
.card-actions a {
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
.card-actions mwc-button {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
275
src/panels/config/cloud/ha-config-cloud-google-assistant.ts
Normal file
275
src/panels/config/cloud/ha-config-cloud-google-assistant.ts
Normal file
@ -0,0 +1,275 @@
|
|||||||
|
import {
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
html,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
|
customElement,
|
||||||
|
property,
|
||||||
|
} from "lit-element";
|
||||||
|
import "@polymer/paper-toggle-button";
|
||||||
|
import "../../../layouts/hass-subpage";
|
||||||
|
import "../../../layouts/hass-loading-screen";
|
||||||
|
import "../../../components/ha-card";
|
||||||
|
import "../../../components/entity/state-info";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import {
|
||||||
|
GoogleEntity,
|
||||||
|
fetchCloudGoogleEntities,
|
||||||
|
CloudStatusLoggedIn,
|
||||||
|
CloudPreferences,
|
||||||
|
updateCloudGoogleEntityConfig,
|
||||||
|
cloudSyncGoogleAssistant,
|
||||||
|
GoogleEntityConfig,
|
||||||
|
} from "../../../data/cloud";
|
||||||
|
import memoizeOne from "memoize-one";
|
||||||
|
import {
|
||||||
|
generateFilter,
|
||||||
|
isEmptyFilter,
|
||||||
|
EntityFilter,
|
||||||
|
} from "../../../common/entity/entity_filter";
|
||||||
|
import { compare } from "../../../common/string/compare";
|
||||||
|
import computeStateName from "../../../common/entity/compute_state_name";
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { showToast } from "../../../util/toast";
|
||||||
|
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||||
|
|
||||||
|
@customElement("ha-config-cloud-google-assistant")
|
||||||
|
class CloudGoogleAssistant extends LitElement {
|
||||||
|
@property() public hass!: HomeAssistant;
|
||||||
|
@property() public cloudStatus!: CloudStatusLoggedIn;
|
||||||
|
@property() public isWide!: boolean;
|
||||||
|
@property() private _entities?: GoogleEntity[];
|
||||||
|
@property()
|
||||||
|
private _entityConfigs: CloudPreferences["google_entity_configs"] = {};
|
||||||
|
private _popstateSyncAttached = false;
|
||||||
|
private _popstateReloadStatusAttached = false;
|
||||||
|
|
||||||
|
private _getEntityFilterFunc = memoizeOne((filter: EntityFilter) =>
|
||||||
|
generateFilter(
|
||||||
|
filter.include_domains,
|
||||||
|
filter.include_entities,
|
||||||
|
filter.exclude_domains,
|
||||||
|
filter.exclude_entities
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render(): TemplateResult | void {
|
||||||
|
if (this._entities === undefined) {
|
||||||
|
return html`
|
||||||
|
<hass-loading-screen></hass-loading-screen>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
const emptyFilter = true || isEmptyFilter(this.cloudStatus.google_entities);
|
||||||
|
const filterFunc = this._getEntityFilterFunc(
|
||||||
|
this.cloudStatus.google_entities
|
||||||
|
);
|
||||||
|
let selected = 0;
|
||||||
|
const cards = this._entities.map((entity) => {
|
||||||
|
const stateObj = this.hass.states[entity.entity_id];
|
||||||
|
const config = this._entityConfigs[entity.entity_id] || {};
|
||||||
|
const isExposed = emptyFilter
|
||||||
|
? Boolean(config.should_expose)
|
||||||
|
: filterFunc(entity.entity_id);
|
||||||
|
if (isExposed) {
|
||||||
|
selected++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 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>
|
||||||
|
<paper-toggle-button
|
||||||
|
.entityId=${entity.entity_id}
|
||||||
|
.disabled=${!emptyFilter}
|
||||||
|
.checked=${isExposed}
|
||||||
|
@checked-changed=${this._exposeChanged}
|
||||||
|
>
|
||||||
|
Expose to Google Assistant
|
||||||
|
</paper-toggle-button>
|
||||||
|
${entity.might_2fa
|
||||||
|
? html`
|
||||||
|
<paper-toggle-button
|
||||||
|
.entityId=${entity.entity_id}
|
||||||
|
.checked=${Boolean(config.disable_2fa)}
|
||||||
|
@checked-changed=${this._disable2FAChanged}
|
||||||
|
>
|
||||||
|
Disable two factor authentication
|
||||||
|
</paper-toggle-button>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`;
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<hass-subpage header="Google Assistant">
|
||||||
|
<span slot="toolbar-icon">${selected} selected</span>
|
||||||
|
${!emptyFilter
|
||||||
|
? html`
|
||||||
|
<div class="banner">
|
||||||
|
Editing which entities are exposed via this UI is disabled
|
||||||
|
because you have configured entity filters in
|
||||||
|
configuration.yaml.
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
<div class="content">
|
||||||
|
${cards}
|
||||||
|
</div>
|
||||||
|
</hass-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
this._fetchData();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
if (changedProps.has("cloudStatus")) {
|
||||||
|
this._entityConfigs = this.cloudStatus.prefs.google_entity_configs;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _fetchData() {
|
||||||
|
const entities = await fetchCloudGoogleEntities(this.hass);
|
||||||
|
entities.sort((a, b) => {
|
||||||
|
const stateA = this.hass.states[a.entity_id];
|
||||||
|
const stateB = this.hass.states[b.entity_id];
|
||||||
|
return compare(
|
||||||
|
stateA ? computeStateName(stateA) : a.entity_id,
|
||||||
|
stateB ? computeStateName(stateB) : b.entity_id
|
||||||
|
);
|
||||||
|
});
|
||||||
|
this._entities = entities;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _showMoreInfo(ev) {
|
||||||
|
const entityId = ev.currentTarget.stateObj.entity_id;
|
||||||
|
fireEvent(this, "hass-more-info", { entityId });
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _exposeChanged(ev: PolymerChangedEvent<boolean>) {
|
||||||
|
const entityId = (ev.currentTarget as any).entityId;
|
||||||
|
const newExposed = ev.detail.value;
|
||||||
|
const curExposed = Boolean(
|
||||||
|
(this._entityConfigs[entityId] || {}).should_expose
|
||||||
|
);
|
||||||
|
if (newExposed === curExposed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this._updateConfig(entityId, {
|
||||||
|
should_expose: newExposed,
|
||||||
|
});
|
||||||
|
this._ensureEntitySync();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _disable2FAChanged(ev: PolymerChangedEvent<boolean>) {
|
||||||
|
const entityId = (ev.currentTarget as any).entityId;
|
||||||
|
const newDisable2FA = ev.detail.value;
|
||||||
|
const curDisable2FA = Boolean(
|
||||||
|
(this._entityConfigs[entityId] || {}).disable_2fa
|
||||||
|
);
|
||||||
|
if (newDisable2FA === curDisable2FA) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this._updateConfig(entityId, {
|
||||||
|
disable_2fa: newDisable2FA,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _updateConfig(entityId: string, values: GoogleEntityConfig) {
|
||||||
|
const updatedConfig = await updateCloudGoogleEntityConfig(
|
||||||
|
this.hass,
|
||||||
|
entityId,
|
||||||
|
values
|
||||||
|
);
|
||||||
|
this._entityConfigs = {
|
||||||
|
...this._entityConfigs,
|
||||||
|
[entityId]: updatedConfig,
|
||||||
|
};
|
||||||
|
this._ensureStatusReload();
|
||||||
|
}
|
||||||
|
|
||||||
|
private _ensureStatusReload() {
|
||||||
|
if (this._popstateReloadStatusAttached) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._popstateReloadStatusAttached = true;
|
||||||
|
// Cache parent because by the time popstate happens,
|
||||||
|
// this element is detached
|
||||||
|
const parent = this.parentElement!;
|
||||||
|
this.addEventListener(
|
||||||
|
"popstate",
|
||||||
|
() => fireEvent(parent, "ha-refresh-cloud-status"),
|
||||||
|
{ once: true }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private _ensureEntitySync() {
|
||||||
|
if (this._popstateSyncAttached) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this._popstateSyncAttached = true;
|
||||||
|
// Cache parent because by the time popstate happens,
|
||||||
|
// this element is detached
|
||||||
|
const parent = this.parentElement!;
|
||||||
|
window.addEventListener(
|
||||||
|
"popstate",
|
||||||
|
() => {
|
||||||
|
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(--card-background-color);
|
||||||
|
padding: 16px 8px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
padding: 4px;
|
||||||
|
--paper-toggle-button-label-spacing: 16px;
|
||||||
|
}
|
||||||
|
ha-card {
|
||||||
|
margin: 4px;
|
||||||
|
width: 100%;
|
||||||
|
max-width: 300px;
|
||||||
|
}
|
||||||
|
.card-content {
|
||||||
|
padding-bottom: 12px;
|
||||||
|
}
|
||||||
|
state-info {
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
paper-toggle-button {
|
||||||
|
padding: 8px 0;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-config-cloud-google-assistant": CloudGoogleAssistant;
|
||||||
|
}
|
||||||
|
}
|
@ -11,7 +11,7 @@ import { CloudStatus } from "../../../data/cloud";
|
|||||||
import { PolymerChangedEvent } from "../../../polymer-types";
|
import { PolymerChangedEvent } from "../../../polymer-types";
|
||||||
import { PolymerElement } from "@polymer/polymer";
|
import { PolymerElement } from "@polymer/polymer";
|
||||||
|
|
||||||
const LOGGED_IN_URLS = ["account"];
|
const LOGGED_IN_URLS = ["account", "google-assistant"];
|
||||||
const NOT_LOGGED_IN_URLS = ["login", "register", "forgot-password"];
|
const NOT_LOGGED_IN_URLS = ["login", "register", "forgot-password"];
|
||||||
|
|
||||||
@customElement("ha-config-cloud")
|
@customElement("ha-config-cloud")
|
||||||
@ -53,6 +53,10 @@ class HaConfigCloud extends HassRouterPage {
|
|||||||
account: {
|
account: {
|
||||||
tag: "ha-config-cloud-account",
|
tag: "ha-config-cloud-account",
|
||||||
},
|
},
|
||||||
|
"google-assistant": {
|
||||||
|
tag: "ha-config-cloud-google-assistant",
|
||||||
|
load: () => import("./ha-config-cloud-google-assistant"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
Loading…
x
Reference in New Issue
Block a user