Less clever, more stable. Really fix #37

This commit is contained in:
Thomas Lovén 2019-11-15 01:00:24 +01:00
parent 52b2a77bfa
commit bc73013f6d
4 changed files with 323 additions and 324 deletions

File diff suppressed because one or more lines are too long

View File

@ -1,122 +1,126 @@
import { areaByName, areaDevices, deviceByName, deviceEntities } from "card-tools/src/devices";
function match(pattern, value) {
if(typeof(value) === "string" && typeof(pattern) === "string") {
if((pattern.startsWith('/') && pattern.endsWith('/')) || pattern.indexOf('*') !== -1) {
if(!pattern.startsWith('/')) { // Convert globs to regex
pattern = pattern
.replace(/\./g, '\.')
.replace(/\*/g, '.*');
pattern = `/^${pattern}$/`;
}
let regex = new RegExp(pattern.slice(1,-1));
return regex.test(value);
}
if(typeof(value) === "string" && typeof(pattern) === "string") {
if((pattern.startsWith('/') && pattern.endsWith('/')) || pattern.indexOf('*') !== -1) {
if(!pattern.startsWith('/')) { // Convert globs to regex
pattern = pattern
.replace(/\./g, '\.')
.replace(/\*/g, '.*');
pattern = `/^${pattern}$/`;
}
let regex = new RegExp(pattern.slice(1,-1));
return regex.test(value);
}
}
if(typeof(pattern) === "string") {
// Comparisons assume numerical values
if(pattern.startsWith("<=")) return parseFloat(value) <= parseFloat(pattern.substr(2));
if(pattern.startsWith(">=")) return parseFloat(value) >= parseFloat(pattern.substr(2));
if(pattern.startsWith("<")) return parseFloat(value) < parseFloat(pattern.substr(1));
if(pattern.startsWith(">")) return parseFloat(value) > parseFloat(pattern.substr(1));
if(pattern.startsWith("!")) return parseFloat(value) != parseFloat(pattern.substr(1));
if(pattern.startsWith("=")) return parseFloat(value) == parseFloat(pattern.substr(1));
}
if(typeof(pattern) === "string") {
// Comparisons assume numerical values
if(pattern.startsWith("<=")) return parseFloat(value) <= parseFloat(pattern.substr(2));
if(pattern.startsWith(">=")) return parseFloat(value) >= parseFloat(pattern.substr(2));
if(pattern.startsWith("<")) return parseFloat(value) < parseFloat(pattern.substr(1));
if(pattern.startsWith(">")) return parseFloat(value) > parseFloat(pattern.substr(1));
if(pattern.startsWith("!")) return parseFloat(value) != parseFloat(pattern.substr(1));
if(pattern.startsWith("=")) return parseFloat(value) == parseFloat(pattern.substr(1));
}
return pattern === value;
return pattern === value;
}
export function entity_filter(hass, filter) {
return function(e) {
const entity = typeof(e) === "string"
? hass.states[e]
: hass.states[e.entity];
if(!e) return false;
for (const [key, value] of Object.entries(filter)) {
switch(key.split(" ")[0]) {
case "options":
case "sort":
break;
return function(e) {
const entity = typeof(e) === "string"
? hass.states[e]
: hass.states[e.entity];
if(!e) return false;
for (const [key, value] of Object.entries(filter)) {
switch(key.split(" ")[0]) {
case "options":
case "sort":
break;
case "domain":
if(!match(value, entity.entity_id.split('.')[0]))
return false;
break;
case "domain":
if(!match(value, entity.entity_id.split('.')[0]))
return false;
break;
case "entity_id":
if(!match(value, entity.entity_id))
return false;
break;
case "entity_id":
if(!match(value, entity.entity_id))
return false;
break;
case "state":
if(!match(value, entity.state))
return false;
break;
case "state":
if(!match(value, entity.state))
return false;
break;
case "name":
if(!entity.attributes.friendly_name
|| !match(value, entity.attributes.friendly_name))
return false;
break;
case "name":
if(!entity.attributes.friendly_name
|| !match(value, entity.attributes.friendly_name))
return false;
break;
case "group":
if(!value.startsWith("group.")
|| !hass.states[value]
|| !hass.states[value].attributes.entity_id
|| !hass.states[value].attributes.entity_id.includes(entity.entity_id)
)
return false;
break;
case "group":
if(!value.startsWith("group.")
|| !hass.states[value]
|| !hass.states[value].attributes.entity_id
|| !hass.states[value].attributes.entity_id.includes(entity.entity_id)
)
return false;
break;
case "attributes":
for(const [k, v] of Object.entries(value)) {
let attr = k.split(" ")[0];
let entityAttribute = entity.attributes;
while(attr && entityAttribute) {
let step;
[step, attr] = attr.split(":");
entityAttribute = entityAttribute[step];
}
if(entityAttribute === undefined
|| (v && !match(v, entityAttribute))
)
return false;
continue;
}
break;
case "not":
if(entity_filter(hass,value)(e))
return false;
break;
case "device":
let _deviceMatch = false;
for(const d of window.cardToolsData.devices) {
if (match(value, d.name)){
if(deviceEntities(d).includes(entity.entity_id))
_deviceMatch = true;
}
}
if(!_deviceMatch) return false;
break;
case "area":
let _areaMatch = false;
for (const a of window.cardToolsData.areas) {
if(match(value, a.name)) {
if(areaDevices(a).flatMap(deviceEntities).includes(entity.entity_id))
_areaMatch = true;
}
}
if(!_areaMatch) return false;
break;
default:
return false;
case "attributes":
for(const [k, v] of Object.entries(value)) {
let attr = k.split(" ")[0];
let entityAttribute = entity.attributes;
while(attr && entityAttribute) {
let step;
[step, attr] = attr.split(":");
entityAttribute = entityAttribute[step];
}
if(entityAttribute === undefined
|| (v && !match(v, entityAttribute))
)
return false;
continue;
}
break;
case "not":
if(entity_filter(hass,value)(e))
return false;
break;
case "device":
if(!window.cardToolsData || !window.cardToolsData.devices)
return false;
let _deviceMatch = false;
for(const d of window.cardToolsData.devices) {
if (match(value, d.name)){
if(deviceEntities(d).includes(entity.entity_id))
_deviceMatch = true;
}
}
if(!_deviceMatch) return false;
break;
case "area":
if(!window.cardToolsData || !window.cardToolsData.areas)
return false;
let _areaMatch = false;
for (const a of window.cardToolsData.areas) {
if(match(value, a.name)) {
if(areaDevices(a).flatMap(deviceEntities).includes(entity.entity_id))
_areaMatch = true;
}
}
if(!_areaMatch) return false;
break;
default:
return false;
}
}
return true;
}
}
}

View File

@ -1,174 +1,171 @@
import { LitElement, html, css } from "card-tools/src/lit-element";
import "card-tools/src/card-maker";
import { entity_filter } from "./filter";
import { entity_sorter } from "./sort";
import { getData } from "card-tools/src/devices";
import { fireEvent } from "card-tools/src/event";
import { createCard } from "card-tools/src/lovelace-element";
import { hass } from "card-tools/src/hass";
class AutoEntities extends LitElement {
static get properties() {
return {
hass: {},
};
static get properties() {
return {
hass: {},
};
}
setConfig(config) {
if(!config || !config.card) {
throw new Error("Invalid configuration");
}
setConfig(config) {
if(!config || !config.card) {
throw new Error("Invalid configuration");
}
if(!this._config) {
this._config = config;
this.cardConfig = {entities: [], ...config.card};
this.entities = [];
} else {
this._config = config;
this.hass = this.hass;
}
if(!this._config) {
this._config = config;
this.hass = hass();
this._getEntities();
this.cardConfig = {entities: this.entities, ...config.card};
this.card = createCard(this.cardConfig);
} else {
this._config = config;
this.hass = this.hass;
}
async _getEntities()
// Reevaluate all filters once areas have been loaded
getData().then(() => this._getEntities());
}
_getEntities()
{
let entities = [];
// Start with any entities added by the `entities` parameter
if(this._config.entities)
entities = entities.concat(this._config.entities)
.map((e) => {
if(typeof(e) === "string")
return {entity: e};
return e;
});
if(!this.hass || !this._config.filter) return entities;
if(this._config.filter.include) {
const all_entities = Object.keys(this.hass.states)
.map((e) => new Object({entity: e}));
for(const f of this._config.filter.include) {
if(f.type !== undefined) {
// If the filter has a type, it's a special entry
entities.push(f);
continue;
}
let add = all_entities.filter(entity_filter(this.hass, f))
.map((e) => new Object({...e, ...f.options}));
if(f.sort !== undefined) {
// Sort per filter
add = add.sort(entity_sorter(this.hass, f.sort));
}
entities = entities.concat(add);
}
}
if(this._config.filter.exclude) {
for(const f of this._config.filter.exclude) {
entities = entities.filter((e) => {
// Don't exclude special entries
if(typeof(e) !== "string" && e.entity === undefined) return true;
return !entity_filter(this.hass,f)(e)
});
}
}
if(this._config.sort) {
// Sort everything
entities = entities.sort(entity_sorter(this.hass, this._config.sort));
}
if(this._config.unique) {
function compare(a,b) {
if(typeof(a) !== typeof(b)) return false;
if(typeof(a) !== "object") return a===b;
if(Object.keys(a).some((k) => !Object.keys(b).includes(k))) return false;
return Object.keys(a).every((k) => compare(a[k], b[k]));
}
let newEntities = [];
for(const e of entities) {
if(newEntities.some((i) => compare(i,e))) continue;
newEntities.push(e);
}
entities = newEntities;
}
this.entities = entities;
}
set entities(ent) {
function compare(a,b) {
if( a === b )
return true;
if( a == null || b == null)
return false;
if(a.length != b.length)
return false;
for(var i = 0; i < a.length; i++)
if(JSON.stringify(a[i]) !== JSON.stringify(b[i]))
return false;
return true;
}
if(!compare(ent, this._entities))
{
let entities = [];
// Start with any entities added by the `entities` parameter
if(this._config.entities)
entities = entities.concat(this._config.entities)
.map((e) => {
if(typeof(e) === "string")
return {entity: e};
return e;
});
if(!this.hass || !this._config.filter) return entities;
if(this._config.filter.include) {
const all_entities = Object.keys(this.hass.states)
.map((e) => new Object({entity: e}));
for(const f of this._config.filter.include) {
if(f.type !== undefined) {
// If the filter has a type, it's a special entry
entities.push(f);
continue;
}
if(f.device || f.area) {
await getData();
}
let add = all_entities.filter(entity_filter(this.hass, f))
.map((e) => new Object({...e, ...f.options}));
if(f.sort !== undefined) {
// Sort per filter
add = add.sort(entity_sorter(this.hass, f.sort));
}
entities = entities.concat(add);
}
}
if(this._config.filter.exclude) {
for(const f of this._config.filter.exclude) {
entities = entities.filter((e) => {
// Don't exclude special entries
if(typeof(e) !== "string" && e.entity === undefined) return true;
return !entity_filter(this.hass,f)(e)
});
}
}
if(this._config.sort) {
// Sort everything
entities = entities.sort(entity_sorter(this.hass, this._config.sort));
}
if(this._config.unique) {
function compare(a,b) {
if(typeof(a) !== typeof(b)) return false;
if(typeof(a) !== "object") return a===b;
if(Object.keys(a).some((k) => !Object.keys(b).includes(k))) return false;
return Object.keys(a).every((k) => compare(a[k], b[k]));
}
let newEntities = [];
for(const e of entities) {
if(newEntities.some((i) => compare(i,e))) continue;
newEntities.push(e);
}
entities = newEntities;
}
return entities;
this._entities = ent;
this.cardConfig = {...this.cardConfig, entities: this._entities};
if(ent.length === 0 && this._config.show_empty === false) {
this.style.display = "none";
this.style.margin = "0";
} else {
this.style.display = null;
this.style.margin = null;
}
}
}
get entities() {
return this._entities;
}
set entities(ent) {
function compare(a,b) {
if( a === b )
return true;
if( a == null || b == null)
return false;
if(a.length != b.length)
return false;
for(var i = 0; i < a.length; i++)
if(JSON.stringify(a[i]) !== JSON.stringify(b[i]))
return false;
return true;
}
if(!compare(ent, this._entities))
{
this._entities = ent;
this.cardConfig = {...this.cardConfig, entities: this._entities};
if(ent.length === 0 && this._config.show_empty === false) {
this.style.display = "none";
this.style.margin = "0";
} else {
this.style.display = null;
this.style.margin = null;
}
this.requestUpdate();
}
}
get entities() {
return this._entities;
}
set cardConfig(cardConfig) {
this._cardConfig = cardConfig;
if(this.card)
this.card.setConfig(cardConfig);
}
get cardConfig() {
return this._cardConfig;
}
set cardConfig(cardConfig) {
this._cardConfig = cardConfig;
if(this.querySelector("card-maker"))
this.querySelector("card-maker").config = cardConfig;
}
get cardConfig() {
return this._cardConfig;
updated(changedProperties) {
if(changedProperties.has("hass") && this.hass) {
this.card.hass = this.hass;
// Run this in a timeout to improve performance
setTimeout(() => this._getEntities(), 0);
}
}
firstUpdated() {
this.cardConfig = this._cardConfig;
}
createRenderRoot() {
return this;
}
render() {
return html`
${this.card}`;
}
updated(changedProperties) {
if(changedProperties.has("hass") && this.hass) {
// Run this in a timeout to improve performance
setTimeout(() => this._getEntities().then((e) => this.entities = e), 0);
}
}
createRenderRoot() {
return this;
}
render() {
return html`
<card-maker
.hass=${this.hass}
></card-maker>`;
}
getCardSize() {
let len = 0;
if(this.querySelector("card-maker") && this.querySelector("card-maker").getCardSize)
len = this.querySelector("card-maker").getCardSize();
if(len === 1 && this.entities.length)
len = this.entities.length;
if(len === 0 && this._config.filter && this._config.filter.include)
return Object.keys(this._config.filter.include).length;
return len || 1;
}
getCardSize() {
let len = 0;
if(this.card && this.card.getCardSize)
len = this.card.getCardSize();
if(len === 1 && this.entities.length)
len = this.entities.length;
if(len === 0 && this._config.filter && this._config.filter.include)
len = Object.keys(this._config.filter.include).length;
return len || 1;
}
}
customElements.define('auto-entities', AutoEntities);

View File

@ -1,66 +1,66 @@
export function entity_sorter(hass, method) {
if(typeof(method) === "string") {
method = {method};
if(typeof(method) === "string") {
method = {method};
}
return function(a, b) {
const entityA = typeof(a) === "string"
? hass.states[a]
: hass.states[a.entity];
const entityB = typeof(b) === "string"
? hass.states[b]
: hass.states[b.entity];
if(entityA === undefined || entityB === undefined) return 0;
const [lt, gt] = method.reverse ? [-1, 1] : [1, -1];
function compare(_a, _b) {
if(method.ignore_case && _a.toLowerCase) _a = _a.toLowerCase();
if(method.ignore_case && _b.toLowerCase) _b = _b.toLowerCase();
if(_a === undefined && _b === undefined) return 0;
if(_a === undefined) return lt;
if(_b === undefined) return gt;
if(_a < _b) return gt;
if(_a > _b) return lt;
return 0;
}
return function(a, b) {
const entityA = typeof(a) === "string"
? hass.states[a]
: hass.states[a.entity];
const entityB = typeof(b) === "string"
? hass.states[b]
: hass.states[b.entity];
if(entityA === undefined || entityB === undefined) return 0;
const [lt, gt] = method.reverse ? [-1, 1] : [1, -1];
function compare(_a, _b) {
if(method.ignore_case && _a.toLowerCase) _a = _a.toLowerCase();
if(method.ignore_case && _b.toLowerCase) _b = _b.toLowerCase();
if(_a === undefined && _b === undefined) return 0;
if(_a === undefined) return lt;
if(_b === undefined) return gt;
if(_a < _b) return gt;
if(_a > _b) return lt;
return 0;
}
switch(method.method) {
case "domain":
return compare(
entityA.entity_id.split(".")[0],
entityB.entity_id.split(".")[0]
);
case "entity_id":
return compare(
entityA.entity_id,
entityB.entity_id
);
case "friendly_name":
case "name":
return compare(
entityA.attributes.friendly_name || entityA.entity_id.split(".")[1],
entityB.attributes.friendly_name || entityB.entity_id.split(".")[1]
);
switch(method.method) {
case "domain":
return compare(
entityA.entity_id.split(".")[0],
entityB.entity_id.split(".")[0]
);
case "entity_id":
return compare(
entityA.entity_id,
entityB.entity_id
);
case "friendly_name":
case "name":
return compare(
entityA.attributes.friendly_name || entityA.entity_id.split(".")[1],
entityB.attributes.friendly_name || entityB.entity_id.split(".")[1]
);
case "state":
return compare(
entityA.state,
entityB.state
);
case "attribute":
let _a = entityA.attributes;
let _b = entityB.attributes;
let attr = method.attribute;
while(attr) {
let k;
[k, attr] = attr.split(":");
_a = _a[k];
_b = _b[k];
if(_a === undefined && _b === undefined) return 0;
if(_a === undefined) return lt;
if(_b === undefined) return gt;
}
return compare(_a, _b);
default:
return 0;
return compare(
entityA.state,
entityB.state
);
case "attribute":
let _a = entityA.attributes;
let _b = entityB.attributes;
let attr = method.attribute;
while(attr) {
let k;
[k, attr] = attr.split(":");
_a = _a[k];
_b = _b[k];
if(_a === undefined && _b === undefined) return 0;
if(_a === undefined) return lt;
if(_b === undefined) return gt;
}
return compare(_a, _b);
default:
return 0;
}
}
}
}
}