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"; import { areaByName, areaDevices, deviceByName, deviceEntities } from "card-tools/src/devices";
function match(pattern, value) { function match(pattern, value) {
if(typeof(value) === "string" && typeof(pattern) === "string") { if(typeof(value) === "string" && typeof(pattern) === "string") {
if((pattern.startsWith('/') && pattern.endsWith('/')) || pattern.indexOf('*') !== -1) { if((pattern.startsWith('/') && pattern.endsWith('/')) || pattern.indexOf('*') !== -1) {
if(!pattern.startsWith('/')) { // Convert globs to regex if(!pattern.startsWith('/')) { // Convert globs to regex
pattern = pattern pattern = pattern
.replace(/\./g, '\.') .replace(/\./g, '\.')
.replace(/\*/g, '.*'); .replace(/\*/g, '.*');
pattern = `/^${pattern}$/`; pattern = `/^${pattern}$/`;
} }
let regex = new RegExp(pattern.slice(1,-1)); let regex = new RegExp(pattern.slice(1,-1));
return regex.test(value); return regex.test(value);
}
} }
}
if(typeof(pattern) === "string") { if(typeof(pattern) === "string") {
// Comparisons assume numerical values // 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(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(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) { export function entity_filter(hass, filter) {
return function(e) { return function(e) {
const entity = typeof(e) === "string" const entity = typeof(e) === "string"
? hass.states[e] ? hass.states[e]
: hass.states[e.entity]; : hass.states[e.entity];
if(!e) return false; if(!e) return false;
for (const [key, value] of Object.entries(filter)) { for (const [key, value] of Object.entries(filter)) {
switch(key.split(" ")[0]) { switch(key.split(" ")[0]) {
case "options": case "options":
case "sort": case "sort":
break; break;
case "domain": case "domain":
if(!match(value, entity.entity_id.split('.')[0])) if(!match(value, entity.entity_id.split('.')[0]))
return false; return false;
break; break;
case "entity_id": case "entity_id":
if(!match(value, entity.entity_id)) if(!match(value, entity.entity_id))
return false; return false;
break; break;
case "state": case "state":
if(!match(value, entity.state)) if(!match(value, entity.state))
return false; return false;
break; break;
case "name": case "name":
if(!entity.attributes.friendly_name if(!entity.attributes.friendly_name
|| !match(value, entity.attributes.friendly_name)) || !match(value, entity.attributes.friendly_name))
return false; return false;
break; break;
case "group": case "group":
if(!value.startsWith("group.") if(!value.startsWith("group.")
|| !hass.states[value] || !hass.states[value]
|| !hass.states[value].attributes.entity_id || !hass.states[value].attributes.entity_id
|| !hass.states[value].attributes.entity_id.includes(entity.entity_id) || !hass.states[value].attributes.entity_id.includes(entity.entity_id)
) )
return false; return false;
break; break;
case "attributes": case "attributes":
for(const [k, v] of Object.entries(value)) { for(const [k, v] of Object.entries(value)) {
let attr = k.split(" ")[0]; let attr = k.split(" ")[0];
let entityAttribute = entity.attributes; let entityAttribute = entity.attributes;
while(attr && entityAttribute) { while(attr && entityAttribute) {
let step; let step;
[step, attr] = attr.split(":"); [step, attr] = attr.split(":");
entityAttribute = entityAttribute[step]; 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;
} }
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; return true;
} }
} }

View File

@ -1,174 +1,171 @@
import { LitElement, html, css } from "card-tools/src/lit-element"; import { LitElement, html, css } from "card-tools/src/lit-element";
import "card-tools/src/card-maker";
import { entity_filter } from "./filter"; import { entity_filter } from "./filter";
import { entity_sorter } from "./sort"; import { entity_sorter } from "./sort";
import { getData } from "card-tools/src/devices"; import { getData } from "card-tools/src/devices";
import { fireEvent } from "card-tools/src/event"; 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 { class AutoEntities extends LitElement {
static get properties() { static get properties() {
return { return {
hass: {}, hass: {},
}; };
}
setConfig(config) {
if(!config || !config.card) {
throw new Error("Invalid configuration");
} }
setConfig(config) { if(!this._config) {
if(!config || !config.card) { this._config = config;
throw new Error("Invalid configuration");
} this.hass = hass();
if(!this._config) { this._getEntities();
this._config = config; this.cardConfig = {entities: this.entities, ...config.card};
this.cardConfig = {entities: [], ...config.card}; this.card = createCard(this.cardConfig);
this.entities = []; } else {
} else { this._config = config;
this._config = config; this.hass = this.hass;
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 = []; this._entities = ent;
// Start with any entities added by the `entities` parameter this.cardConfig = {...this.cardConfig, entities: this._entities};
if(this._config.entities) if(ent.length === 0 && this._config.show_empty === false) {
entities = entities.concat(this._config.entities) this.style.display = "none";
.map((e) => { this.style.margin = "0";
if(typeof(e) === "string") } else {
return {entity: e}; this.style.display = null;
return e; this.style.margin = null;
}); }
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;
} }
}
get entities() {
return this._entities;
}
set entities(ent) { set cardConfig(cardConfig) {
function compare(a,b) { this._cardConfig = cardConfig;
if( a === b ) if(this.card)
return true; this.card.setConfig(cardConfig);
if( a == null || b == null) }
return false; get cardConfig() {
if(a.length != b.length) return this._cardConfig;
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) { updated(changedProperties) {
this._cardConfig = cardConfig; if(changedProperties.has("hass") && this.hass) {
if(this.querySelector("card-maker")) this.card.hass = this.hass;
this.querySelector("card-maker").config = cardConfig; // Run this in a timeout to improve performance
} setTimeout(() => this._getEntities(), 0);
get cardConfig() {
return this._cardConfig;
} }
}
firstUpdated() { createRenderRoot() {
this.cardConfig = this._cardConfig; return this;
} }
render() {
return html`
${this.card}`;
}
updated(changedProperties) { getCardSize() {
if(changedProperties.has("hass") && this.hass) { let len = 0;
// Run this in a timeout to improve performance if(this.card && this.card.getCardSize)
setTimeout(() => this._getEntities().then((e) => this.entities = e), 0); len = this.card.getCardSize();
} if(len === 1 && this.entities.length)
} len = this.entities.length;
if(len === 0 && this._config.filter && this._config.filter.include)
createRenderRoot() { len = Object.keys(this._config.filter.include).length;
return this; return len || 1;
} }
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;
}
} }
customElements.define('auto-entities', AutoEntities); customElements.define('auto-entities', AutoEntities);

View File

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