mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-26 02:36:37 +00:00
Expose entities for Google/Alexa (#680)
* Add entity filter * Show exposed entities on cloud panel * Fix tests * Revert some testing changes * Cursor: pointer * Fix * Update tests to TS
This commit is contained in:
parent
54e43758d3
commit
e2b9893b17
64
src/common/entity/entity_filter.ts
Normal file
64
src/common/entity/entity_filter.ts
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import computeDomain from "./compute_domain";
|
||||||
|
|
||||||
|
export type FilterFunc = (entityId: string) => boolean;
|
||||||
|
|
||||||
|
export const generateFilter = (
|
||||||
|
includeDomains?: string[],
|
||||||
|
includeEntities?: string[],
|
||||||
|
excludeDomains?: string[],
|
||||||
|
excludeEntities?: string[]
|
||||||
|
): FilterFunc => {
|
||||||
|
const includeDomainsSet = new Set(includeDomains);
|
||||||
|
const includeEntitiesSet = new Set(includeEntities);
|
||||||
|
const excludeDomainsSet = new Set(excludeDomains);
|
||||||
|
const excludeEntitiesSet = new Set(excludeEntities);
|
||||||
|
|
||||||
|
const haveInclude = includeDomainsSet.size > 0 || includeEntitiesSet.size > 0;
|
||||||
|
const haveExclude = excludeDomainsSet.size > 0 || excludeEntitiesSet.size > 0;
|
||||||
|
|
||||||
|
// Case 1 - no includes or excludes - pass all entities
|
||||||
|
if (!haveInclude && !haveExclude) {
|
||||||
|
return () => true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 2 - includes, no excludes - only include specified entities
|
||||||
|
if (haveInclude && !haveExclude) {
|
||||||
|
return (entityId) =>
|
||||||
|
includeEntitiesSet.has(entityId) ||
|
||||||
|
includeDomainsSet.has(computeDomain(entityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 3 - excludes, no includes - only exclude specified entities
|
||||||
|
if (!haveInclude && haveExclude) {
|
||||||
|
return (entityId) =>
|
||||||
|
!excludeEntitiesSet.has(entityId) &&
|
||||||
|
!excludeDomainsSet.has(computeDomain(entityId));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 4 - both includes and excludes specified
|
||||||
|
// Case 4a - include domain specified
|
||||||
|
// - if domain is included, pass if entity not excluded
|
||||||
|
// - if domain is not included, pass if entity is included
|
||||||
|
// note: if both include and exclude domains specified,
|
||||||
|
// the exclude domains are ignored
|
||||||
|
if (includeDomainsSet.size) {
|
||||||
|
return (entityId) =>
|
||||||
|
includeDomainsSet.has(computeDomain(entityId))
|
||||||
|
? !excludeEntitiesSet.has(entityId)
|
||||||
|
: includeEntitiesSet.has(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 4b - exclude domain specified
|
||||||
|
// - if domain is excluded, pass if entity is included
|
||||||
|
// - if domain is not excluded, pass if entity not excluded
|
||||||
|
if (excludeDomainsSet.size) {
|
||||||
|
return (entityId) =>
|
||||||
|
excludeDomainsSet.has(computeDomain(entityId))
|
||||||
|
? includeEntitiesSet.has(entityId)
|
||||||
|
: !excludeEntitiesSet.has(entityId);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Case 4c - neither include or exclude domain specified
|
||||||
|
// - Only pass if entity is included. Ignore entity excludes.
|
||||||
|
return (entityId) => includeEntitiesSet.has(entityId);
|
||||||
|
};
|
@ -10,6 +10,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
|||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { updatePref } from "./data";
|
import { updatePref } from "./data";
|
||||||
import { CloudStatusLoggedIn } from "./types";
|
import { CloudStatusLoggedIn } from "./types";
|
||||||
|
import "./cloud-exposed-entities";
|
||||||
|
|
||||||
export class CloudAlexaPref extends LitElement {
|
export class CloudAlexaPref extends LitElement {
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
@ -23,11 +24,13 @@ export class CloudAlexaPref extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
const enabled = this.cloudStatus!.alexa_enabled;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
<paper-card heading="Alexa">
|
<paper-card heading="Alexa">
|
||||||
<paper-toggle-button
|
<paper-toggle-button
|
||||||
.checked="${this.cloudStatus!.alexa_enabled}"
|
.checked="${enabled}"
|
||||||
@change="${this._toggleChanged}"
|
@change="${this._toggleChanged}"
|
||||||
></paper-toggle-button>
|
></paper-toggle-button>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
@ -43,6 +46,18 @@ export class CloudAlexaPref extends LitElement {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<em>This integration requires an Alexa-enabled device like the Amazon Echo.</em>
|
<em>This integration requires an Alexa-enabled device like the Amazon Echo.</em>
|
||||||
|
${
|
||||||
|
enabled
|
||||||
|
? html`
|
||||||
|
<p>Exposed entities:</p>
|
||||||
|
<cloud-exposed-entities
|
||||||
|
.hass="${this.hass}"
|
||||||
|
.filter="${this.cloudStatus!.alexa_entities}"
|
||||||
|
.supportedDomains="${this.cloudStatus!.alexa_domains}"
|
||||||
|
></cloud-exposed-entities>
|
||||||
|
`
|
||||||
|
: ""
|
||||||
|
}
|
||||||
</div>
|
</div>
|
||||||
</paper-card>
|
</paper-card>
|
||||||
`;
|
`;
|
||||||
|
116
src/panels/config/cloud/cloud-exposed-entities.ts
Normal file
116
src/panels/config/cloud/cloud-exposed-entities.ts
Normal file
@ -0,0 +1,116 @@
|
|||||||
|
import {
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
PropertyDeclarations,
|
||||||
|
PropertyValues,
|
||||||
|
} from "@polymer/lit-element";
|
||||||
|
import { TemplateResult } from "lit-html";
|
||||||
|
import { repeat } from "lit-html/directives/repeat";
|
||||||
|
import "@polymer/paper-tooltip/paper-tooltip";
|
||||||
|
import { HassEntityBase } from "home-assistant-js-websocket";
|
||||||
|
import "../../../components/entity/ha-state-icon";
|
||||||
|
|
||||||
|
import { fireEvent } from "../../../common/dom/fire_event";
|
||||||
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { EntityFilter } from "./types";
|
||||||
|
import computeStateName from "../../../common/entity/compute_state_name";
|
||||||
|
import {
|
||||||
|
FilterFunc,
|
||||||
|
generateFilter,
|
||||||
|
} from "../../../common/entity/entity_filter";
|
||||||
|
|
||||||
|
export class CloudExposedEntities extends LitElement {
|
||||||
|
public hass?: HomeAssistant;
|
||||||
|
public filter?: EntityFilter;
|
||||||
|
public supportedDomains?: string[];
|
||||||
|
private _filterFunc?: FilterFunc;
|
||||||
|
|
||||||
|
static get properties(): PropertyDeclarations {
|
||||||
|
return {
|
||||||
|
hass: {},
|
||||||
|
filter: {},
|
||||||
|
supportedDomains: {},
|
||||||
|
_filterFunc: {},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this._filterFunc) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
|
||||||
|
const states: Array<[string, HassEntityBase]> = [];
|
||||||
|
|
||||||
|
Object.keys(this.hass!.states).forEach((entityId) => {
|
||||||
|
if (this._filterFunc!(entityId)) {
|
||||||
|
const stateObj = this.hass!.states[entityId];
|
||||||
|
states.push([computeStateName(stateObj), stateObj]);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
states.sort();
|
||||||
|
|
||||||
|
return html`
|
||||||
|
${this.renderStyle()}
|
||||||
|
${repeat(
|
||||||
|
states!,
|
||||||
|
(stateInfo) => stateInfo[1].entity_id,
|
||||||
|
(stateInfo) => html`
|
||||||
|
<span>
|
||||||
|
<ha-state-icon
|
||||||
|
.stateObj='${stateInfo[1]}'
|
||||||
|
@click='${this._handleMoreInfo}'
|
||||||
|
></ha-state-icon>
|
||||||
|
<paper-tooltip
|
||||||
|
position="bottom"
|
||||||
|
>${stateInfo[0]}</paper-tooltip>
|
||||||
|
</span>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProperties: PropertyValues) {
|
||||||
|
if (
|
||||||
|
changedProperties.has("filter") &&
|
||||||
|
changedProperties.get("filter") !== this.filter
|
||||||
|
) {
|
||||||
|
const filter = this.filter!;
|
||||||
|
const filterFunc = generateFilter(
|
||||||
|
filter.include_domains,
|
||||||
|
filter.include_entities,
|
||||||
|
filter.exclude_domains,
|
||||||
|
filter.exclude_entities
|
||||||
|
);
|
||||||
|
const domains = new Set(this.supportedDomains);
|
||||||
|
this._filterFunc = (entityId: string) => {
|
||||||
|
const domain = entityId.split(".")[0];
|
||||||
|
return domains.has(domain) && filterFunc(entityId);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private _handleMoreInfo(ev: MouseEvent) {
|
||||||
|
fireEvent(this, "hass-more-info", {
|
||||||
|
entityId: (ev.currentTarget as any).stateObj.entity_id,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderStyle(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<style>
|
||||||
|
ha-state-icon {
|
||||||
|
color: var(--primary-text-color);
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"cloud-exposed-entities": CloudExposedEntities;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
customElements.define("cloud-exposed-entities", CloudExposedEntities);
|
@ -11,6 +11,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
|||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import { updatePref } from "./data";
|
import { updatePref } from "./data";
|
||||||
import { CloudStatusLoggedIn } from "./types";
|
import { CloudStatusLoggedIn } from "./types";
|
||||||
|
import "./cloud-exposed-entities";
|
||||||
|
|
||||||
export class CloudGooglePref extends LitElement {
|
export class CloudGooglePref extends LitElement {
|
||||||
public hass?: HomeAssistant;
|
public hass?: HomeAssistant;
|
||||||
@ -24,11 +25,13 @@ export class CloudGooglePref extends LitElement {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
|
const enabled = this.cloudStatus!.google_enabled;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
${this.renderStyle()}
|
${this.renderStyle()}
|
||||||
<paper-card heading="Google Assistant">
|
<paper-card heading="Google Assistant">
|
||||||
<paper-toggle-button
|
<paper-toggle-button
|
||||||
.checked="${this.cloudStatus!.google_enabled}"
|
.checked="${enabled}"
|
||||||
@change="${this._toggleChanged}"
|
@change="${this._toggleChanged}"
|
||||||
></paper-toggle-button>
|
></paper-toggle-button>
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
@ -46,11 +49,23 @@ export class CloudGooglePref extends LitElement {
|
|||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
<em>This integration requires a Google Assistant-enabled device like the Google Home or Android phone.</em>
|
<em>This integration requires a Google Assistant-enabled device like the Google Home or Android phone.</em>
|
||||||
|
${
|
||||||
|
enabled
|
||||||
|
? html`
|
||||||
|
<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>
|
||||||
<div class="card-actions">
|
<div class="card-actions">
|
||||||
<ha-call-api-button
|
<ha-call-api-button
|
||||||
.hass="${this.hass}"
|
.hass="${this.hass}"
|
||||||
.disabled="${!this.cloudStatus!.google_enabled}"
|
.disabled="${!enabled}"
|
||||||
path="cloud/google_actions/sync"
|
path="cloud/google_actions/sync"
|
||||||
>Sync devices</ha-call-api-button>
|
>Sync devices</ha-call-api-button>
|
||||||
</div>
|
</div>
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
interface EntityFilter {
|
export interface EntityFilter {
|
||||||
include_domains: string[];
|
include_domains: string[];
|
||||||
include_entities: string[];
|
include_entities: string[];
|
||||||
exclude_domains: string[];
|
exclude_domains: string[];
|
||||||
@ -13,8 +13,10 @@ export type CloudStatusLoggedIn = CloudStatusBase & {
|
|||||||
email: string;
|
email: string;
|
||||||
google_enabled: boolean;
|
google_enabled: boolean;
|
||||||
google_entities: EntityFilter;
|
google_entities: EntityFilter;
|
||||||
|
google_domains: string[];
|
||||||
alexa_enabled: boolean;
|
alexa_enabled: boolean;
|
||||||
alexa_entities: EntityFilter;
|
alexa_entities: EntityFilter;
|
||||||
|
alexa_domains: string[];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
|
export type CloudStatus = CloudStatusBase | CloudStatusLoggedIn;
|
||||||
|
97
test-mocha/common/entity/entity_filter.ts
Normal file
97
test-mocha/common/entity/entity_filter.ts
Normal file
@ -0,0 +1,97 @@
|
|||||||
|
import { generateFilter } from "../../../src/common/entity/entity_filter";
|
||||||
|
|
||||||
|
import * as assert from "assert";
|
||||||
|
|
||||||
|
describe("EntityFilter", () => {
|
||||||
|
// case 1
|
||||||
|
it("passes all when no filters passed in", () => {
|
||||||
|
const filter = generateFilter();
|
||||||
|
|
||||||
|
assert(filter("sensor.test"));
|
||||||
|
assert(filter("sun.sun"));
|
||||||
|
assert(filter("light.test"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// case 2
|
||||||
|
it("allows whitelisting entities by entity id", () => {
|
||||||
|
const filter = generateFilter(undefined, ["light.kitchen"]);
|
||||||
|
|
||||||
|
assert(filter("light.kitchen"));
|
||||||
|
assert(!filter("light.living_room"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows whitelisting entities by domain", () => {
|
||||||
|
const filter = generateFilter(["switch"]);
|
||||||
|
|
||||||
|
assert(filter("switch.bla"));
|
||||||
|
assert(!filter("light.kitchen"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// case 3
|
||||||
|
it("allows blacklisting entities by entity id", () => {
|
||||||
|
const filter = generateFilter(undefined, undefined, undefined, [
|
||||||
|
"light.kitchen",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert(!filter("light.kitchen"));
|
||||||
|
assert(filter("light.living_room"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows blacklisting entities by domain", () => {
|
||||||
|
const filter = generateFilter(undefined, undefined, ["switch"]);
|
||||||
|
|
||||||
|
assert(!filter("switch.bla"));
|
||||||
|
assert(filter("light.kitchen"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// case 4a
|
||||||
|
it("allows whitelisting domain and blacklisting entity", () => {
|
||||||
|
const filter = generateFilter(["switch"], undefined, undefined, [
|
||||||
|
"switch.kitchen",
|
||||||
|
]);
|
||||||
|
|
||||||
|
assert(filter("switch.living_room"));
|
||||||
|
assert(!filter("switch.kitchen"));
|
||||||
|
assert(!filter("sensor.bla"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows whitelisting entity while whitelisting other domains", () => {
|
||||||
|
const filter = generateFilter(["switch"], ["light.kitchen"]);
|
||||||
|
|
||||||
|
assert(filter("switch.living_room"));
|
||||||
|
assert(filter("light.kitchen"));
|
||||||
|
assert(!filter("sensor.bla"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// case 4b
|
||||||
|
it("allows blacklisting domain and whitelisting entity", () => {
|
||||||
|
const filter = generateFilter(undefined, ["switch.kitchen"], ["switch"]);
|
||||||
|
|
||||||
|
assert(filter("switch.kitchen"));
|
||||||
|
assert(!filter("switch.living_room"));
|
||||||
|
assert(filter("sensor.bla"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("allows blacklisting domain and excluding entities", () => {
|
||||||
|
const filter = generateFilter(
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
["switch"],
|
||||||
|
["light.kitchen"]
|
||||||
|
);
|
||||||
|
|
||||||
|
assert(!filter("switch.living_room"));
|
||||||
|
assert(!filter("light.kitchen"));
|
||||||
|
assert(filter("sensor.bla"));
|
||||||
|
});
|
||||||
|
|
||||||
|
// case 4c
|
||||||
|
it("allows whitelisting entities", () => {
|
||||||
|
const filter = generateFilter(undefined, ["light.kitchen"]);
|
||||||
|
|
||||||
|
assert(filter("light.kitchen"));
|
||||||
|
assert(!filter("switch.living_room"));
|
||||||
|
assert(!filter("light.living_room"));
|
||||||
|
assert(!filter("sensor.bla"));
|
||||||
|
});
|
||||||
|
});
|
Loading…
x
Reference in New Issue
Block a user