Add entity source API (#12149)

This commit is contained in:
Paulus Schoutsen 2022-03-29 17:09:51 -07:00 committed by GitHub
parent 2a12172eeb
commit 00cbd1d9e6
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
5 changed files with 195 additions and 59 deletions

View File

@ -0,0 +1,53 @@
import { HomeAssistant } from "../../types";
interface ResultCache<T> {
[entityId: string]: Promise<T> | undefined;
}
/**
* Call a function with result caching per entity.
* @param cacheKey key to store the cache on hass object
* @param cacheTime time to cache the results
* @param func function to fetch the data
* @param hass Home Assistant object
* @param entityId entity to fetch data for
* @param args extra arguments to pass to the function to fetch the data
* @returns
*/
export const timeCacheEntityPromiseFunc = async <T>(
cacheKey: string,
cacheTime: number,
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>,
hass: HomeAssistant,
entityId: string,
...args: any[]
): Promise<T> => {
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey];
if (!cache) {
cache = hass[cacheKey] = {};
}
const lastResult = cache[entityId];
if (lastResult) {
return lastResult;
}
const result = func(hass, entityId, ...args);
cache[entityId] = result;
result.then(
// When successful, set timer to clear cache
() =>
setTimeout(() => {
cache![entityId] = undefined;
}, cacheTime),
// On failure, clear cache right away
() => {
cache![entityId] = undefined;
}
);
return result;
};

View File

@ -1,43 +1,80 @@
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
interface ResultCache<T> { interface CacheResult<T> {
[entityId: string]: Promise<T> | undefined; result: T;
cacheKey: any;
} }
/**
* Caches a result of a promise for X time. Allows optional extra validation
* check to invalidate the cache.
* @param cacheKey the key to store the cache
* @param cacheTime the time to cache the result
* @param func the function to fetch the data
* @param generateCacheKey optional function to generate a cache key based on current hass + cached result. Cache is invalid if generates a different cache key.
* @param hass Home Assistant object
* @param args extra arguments to pass to the function to fetch the data
* @returns
*/
export const timeCachePromiseFunc = async <T>( export const timeCachePromiseFunc = async <T>(
cacheKey: string, cacheKey: string,
cacheTime: number, cacheTime: number,
func: (hass: HomeAssistant, entityId: string, ...args: any[]) => Promise<T>, func: (hass: HomeAssistant, ...args: any[]) => Promise<T>,
generateCacheKey:
| ((hass: HomeAssistant, lastResult: T) => unknown)
| undefined,
hass: HomeAssistant, hass: HomeAssistant,
entityId: string,
...args: any[] ...args: any[]
): Promise<T> => { ): Promise<T> => {
let cache: ResultCache<T> | undefined = (hass as any)[cacheKey]; const anyHass = hass as any;
const lastResult: Promise<CacheResult<T>> | CacheResult<T> | undefined =
anyHass[cacheKey];
if (!cache) { const checkCachedResult = (result: CacheResult<T>): T | Promise<T> => {
cache = hass[cacheKey] = {}; if (
!generateCacheKey ||
generateCacheKey(hass, result.result) === result.cacheKey
) {
return result.result;
} }
const lastResult = cache[entityId]; anyHass[cacheKey] = undefined;
return timeCachePromiseFunc(
cacheKey,
cacheTime,
func,
generateCacheKey,
hass,
...args
);
};
// If we have a cached result, return it if it's still valid
if (lastResult) { if (lastResult) {
return lastResult; return lastResult instanceof Promise
? lastResult.then(checkCachedResult)
: checkCachedResult(lastResult);
} }
const result = func(hass, entityId, ...args); const resultPromise = func(hass, ...args);
cache[entityId] = result; anyHass[cacheKey] = resultPromise;
result.then( resultPromise.then(
// When successful, set timer to clear cache // When successful, set timer to clear cache
() => (result) => {
anyHass[cacheKey] = {
result,
cacheKey: generateCacheKey?.(hass, result),
};
setTimeout(() => { setTimeout(() => {
cache![entityId] = undefined; anyHass[cacheKey] = undefined;
}, cacheTime), }, cacheTime);
},
// On failure, clear cache right away // On failure, clear cache right away
() => { () => {
cache![entityId] = undefined; anyHass[cacheKey] = undefined;
} }
); );
return result; return resultPromise;
}; };

View File

@ -1,21 +1,23 @@
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement } from "lit"; import { html, LitElement, PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeStateDomain } from "../../common/entity/compute_state_domain"; import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { subscribeEntityRegistry } from "../../data/entity_registry"; import {
EntitySources,
fetchEntitySourcesWithCache,
} from "../../data/entity_sources";
import { EntitySelector } from "../../data/selector"; import { EntitySelector } from "../../data/selector";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../entity/ha-entities-picker"; import "../entity/ha-entities-picker";
import "../entity/ha-entity-picker"; import "../entity/ha-entity-picker";
@customElement("ha-selector-entity") @customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) { export class HaEntitySelector extends LitElement {
@property() public hass!: HomeAssistant; @property() public hass!: HomeAssistant;
@property() public selector!: EntitySelector; @property() public selector!: EntitySelector;
@state() private _entityPlaformLookup?: Record<string, string>; @state() private _entitySources?: EntitySources;
@property() public value?: any; @property() public value?: any;
@ -49,50 +51,48 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
`; `;
} }
public hassSubscribe(): UnsubscribeFunc[] { protected updated(changedProps: PropertyValues): void {
return [ super.updated(changedProps);
subscribeEntityRegistry(this.hass.connection!, (entities) => { if (
const entityLookup = {}; changedProps.has("selector") &&
for (const confEnt of entities) { this.selector.entity.integration &&
if (!confEnt.platform) { !this._entitySources
continue; ) {
fetchEntitySourcesWithCache(this.hass).then((sources) => {
this._entitySources = sources;
});
} }
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entityPlaformLookup = entityLookup;
}),
];
} }
private _filterEntities = (entity: HassEntity): boolean => { private _filterEntities = (entity: HassEntity): boolean => {
if (this.selector.entity?.domain) { const {
const filterDomain = this.selector.entity.domain; domain: filterDomain,
const filterDomainIsArray = Array.isArray(filterDomain); device_class: filterDeviceClass,
integration: filterIntegration,
} = this.selector.entity;
if (filterDomain) {
const entityDomain = computeStateDomain(entity); const entityDomain = computeStateDomain(entity);
if ( if (
(filterDomainIsArray && !filterDomain.includes(entityDomain)) || Array.isArray(filterDomain)
(!filterDomainIsArray && entityDomain !== filterDomain) ? !filterDomain.includes(entityDomain)
: entityDomain !== filterDomain
) { ) {
return false; return false;
} }
} }
if (this.selector.entity?.device_class) {
if ( if (
!entity.attributes.device_class || filterDeviceClass &&
entity.attributes.device_class !== this.selector.entity.device_class entity.attributes.device_class !== filterDeviceClass
) { ) {
return false; return false;
} }
}
if (this.selector.entity?.integration) {
if ( if (
!this._entityPlaformLookup || filterIntegration &&
this._entityPlaformLookup[entity.entity_id] !== this._entitySources?.[entity.entity_id]?.domain !== filterIntegration
this.selector.entity.integration
) { ) {
return false; return false;
} }
}
return true; return true;
}; };
} }

View File

@ -2,7 +2,7 @@ import {
HassEntityAttributeBase, HassEntityAttributeBase,
HassEntityBase, HassEntityBase,
} from "home-assistant-js-websocket"; } from "home-assistant-js-websocket";
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; import { timeCacheEntityPromiseFunc } from "../common/util/time-cache-entity-promise-func";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import { getSignedPath } from "./auth"; import { getSignedPath } from "./auth";
@ -50,7 +50,7 @@ export const fetchThumbnailUrlWithCache = async (
width: number, width: number,
height: number height: number
) => { ) => {
const base_url = await timeCachePromiseFunc( const base_url = await timeCacheEntityPromiseFunc(
"_cameraTmbUrl", "_cameraTmbUrl",
9000, 9000,
fetchThumbnailUrl, fetchThumbnailUrl,

View File

@ -0,0 +1,46 @@
import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise";
import { HomeAssistant } from "../types";
interface EntitySourceConfigEntry {
source: "config_entry";
domain: string;
custom_component: boolean;
config_entry: string;
}
interface EntitySourcePlatformConfig {
source: "platform_config";
domain: string;
custom_component: boolean;
}
export type EntitySources = Record<
string,
EntitySourceConfigEntry | EntitySourcePlatformConfig
>;
const fetchEntitySources = (
hass: HomeAssistant,
entity_id?: string
): Promise<EntitySources> =>
hass.callWS({
type: "entity/source",
entity_id,
});
export const fetchEntitySourcesWithCache = (
hass: HomeAssistant,
entity_id?: string
): Promise<EntitySources> =>
entity_id
? fetchEntitySources(hass, entity_id)
: timeCachePromiseFunc(
"_entitySources",
// cache for 30 seconds
30000,
fetchEntitySources,
// We base the cache on number of states. If number of states
// changes we force a refresh
(hass2) => Object.keys(hass2.states).length,
hass
);