Total overhaul!

This commit is contained in:
2019-11-08 00:38:06 +01:00
parent 8057bdfd51
commit 05a3d0a9c5
12 changed files with 4539 additions and 325 deletions

113
src/filter.js Normal file
View File

@@ -0,0 +1,113 @@
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(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;
}
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;
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 "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 "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":
const _deviceEntities = deviceEntities(deviceByName(value));
if(!_deviceEntities.includes(entity.entity_id))
return false;
break;
case "area":
const _areaDevices = areaDevices(areaByName(value));
const _areaEntities = _areaDevices.flatMap(deviceEntities);
if(!_areaEntities.includes(entity.entity_id))
return false;
break;
default:
return false;
}
}
return true;
}
}

132
src/main.js Normal file
View File

@@ -0,0 +1,132 @@
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";
class AutoEntities extends LitElement {
static get properties() {
return {
hass: {},
cardConfig: {},
entities: {},
};
}
async setConfig(config) {
if(!config || !config.card) {
throw new Error("Invalid configuration");
}
this._config = config;
this.entities = [];
this.cardConfig = {entities: this.entities, ...config.card};
}
async _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;
}
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));
}
return entities;
}
async updated(changedProperties) {
if(changedProperties.has("hass") && this.hass) {
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(a[i] !== b[i])
return false;
return true;
}
const oldEntities = this.entities;
const newEntities = await this._getEntities();
if(!compare(oldEntities, newEntities)) {
this.entities = newEntities;
this.cardConfig = {
...this.cardConfig,
entities: newEntities,
};
}
}
}
createRenderRoot() {
return this;
}
render() {
if(this.entities.length === 0 && this._config.show_empty === false) {
return html``;
}
return html`
<card-maker
.config=${this.cardConfig}
.hass=${this.hass}
></card-maker>`;
}
getCardSize() {
if(this.querySelector("card-maker") && this.querySelector("card-maker").getCardSize)
return this.querySelector("card-maker").getCardSize();
if(this.entities.length)
return this.entities.length;
if(this._config.filter && this._config.filter.include)
return Object.keys(this._config.filter.include).length;
return 1
}
}
customElements.define('auto-entities', AutoEntities)

66
src/sort.js Normal file
View File

@@ -0,0 +1,66 @@
export function entity_sorter(hass, 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;
}
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;
}
}
}