From c098c93abe3d067e4a32411ef718de0e2fd2430a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Thomas=20Lov=C3=A9n?= Date: Sat, 26 Jan 2019 23:14:45 +0100 Subject: [PATCH] Initial commit. --- README.md | 183 +++++++++++++++++++++++++++++++++++++++++++++++ auto-entities.js | 159 ++++++++++++++++++++++++++++++++++++++++ 2 files changed, 342 insertions(+) create mode 100644 README.md create mode 100644 auto-entities.js diff --git a/README.md b/README.md new file mode 100644 index 0000000..f854a1a --- /dev/null +++ b/README.md @@ -0,0 +1,183 @@ +auto-entities +============= + +**Kinda-sorta-experimental-ish** + +This plugin can automatically populate the `entities:` list of a card or entity-row with entities matching a filter. + +> If you've been around the custom lovelace stuff scene for a while, +> this function probably feels familliar to you. This plugin is a +> reimplementation of the fantastic [`monster-card`](https://github.com/ciotlosm/custom-lovelace/tree/master/monster-card) +> by Marius Ciotlos. Differences are outlined below. + +## Installation instructions + +This plugin requires [card-tools](https://github.com/thomasloven/lovelace-card-tools) to be installed. + +For installation instructions [see this guide](https://github.com/thomasloven/hass-config/wiki/Lovelace-Plugins). + +## Options + +| Name | Type | Default | Description +| ---- | ---- | ------- | ----------- +| type | string | **Required** | `custom:auto-entities` +| card | object | **Required** | The card to display +| filter | object | None | Filters for including and excluding entities +| entities | list | None | Enties to include +| show\_empty | boolean | true | Show/hide empty card + +### filters +The `filter` options has two sections, `include` and `exclude`. Each section contains a list of filters. + +Each section has the following options. +All options are optional, and filters will match any entity matching **ALL** options. + +| Name | Description +| ---- | ----------- +| domain | Match entity domain (e.g. `light`, `binary_sensor`, `media_player`) +| state | Match entity state (e.g. "on", "off", 3.14) +| entity\_id | Match entity id (e.g. `light.bed_light`, `binary_sensor.weekdays_only`, `media_player.kitchen`) +| name | Match friendly name attribute (e.g. "Kitchen lights", "Front door") +| group | Match entities in given group +| attributes | Match attributes. **See below** +| options | Additional options to attach to entities matching this filter (only makes sense in `include`) + +The attributes option takes an object with `attribute: value` combinations and matches any entity which matches all of those attributes. + +## How it works +`auto-entities` creates a list of entities by: +1. Including every entity given in `entities:` (this allows nesting of `auto-entities` if you'd want to do that for some reason...) +2. Include all entities that matches **ALL** options of **ANY** filter in the `filter.include` section. +3. Remove all entities that matches **ALL** options of **ANY** filter in the `filter.exclude` section. + +It then creates a card based on the configuration given in `card:` but adds the `entities:` option populated with the entities from above. + +### Matching rules +Any filter option can use `*` as a wildcard for string comparison. Remember to quote your strings when doing this: +```yaml +filter: + include: + - name: "Bedroom *" + - entity_id: "sensor.temperature_*_max" +``` + +Any filter option can use [javascript Regular Expressions](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Guide/Regular_Expressions) for string comparison. To do this, enclose the regex in `/`. Also make sure to quote the string: +```yaml +filter: + include: + - name: "/Bedroom .*/" + - entity_id: "/sensor.temperature_4[abd]/" +``` + +Any filter option dealing with numerical quantities can use comparison operators if specified as a string (must be quoted): +```yaml +filter: + include: + - attributes: + battery: "<= 50" # Attribute battery_level is 50 or less + - state: "> 25" # State is greater than 25 + - attributes: + count: "! 2" # Attribute count is not equal to 2 + - state: "= 12" # State is exactly 12 (also matches "12", "12.0" etc.) + - state: 12 # State is exactly 12 but not "12" +``` + +Any option can be used more than once by appending a number or string to the option name: +```yaml +filter: + include: + - state 1: "> 100" + state 2: "< 200" +``` +The filter above matches entities where the state is above 100 **AND** below 200. Compare to the following: +```yaml +filter: + include: + - state: "< 100" + - state: "> 200" +``` +The two filters above together match entities where the state is below 100 **OR** above 200. + +*Advanced stuff:* You can drill into attributes that are object using keys or indexes separated by `:`: +```yaml +filter: + include: + - attributes: + hs_color: "1:> 30" +``` +The example above matches lights with a `hs_color` saturation value greater than 30. + +## Examples + +Show all with some exceptions +```yaml +type: custom:auto-entities +card: + type: glance +filter: + include: [{}] + exclude: + - entity_id: "*yweather*" + - domain: group + - domain: zone +``` + +Show all in `device_tracker` with battery less than 50: +```yaml +type: custom:auto-entities +card: + type: entities + title: Battery warning +filter: + include: + - domain: device_tracker + options: + secondary_info: last-changed + attributes: + battery: "< 50" + source_type: gps +``` + +Show all lights that are on: +```yaml +type: custom:auto-entities +show_empty: false +card: + type: glance + title: Lights on +filter: + include: + - domain: light + state: "on" # Remember that "on" and "off" are magic in yaml, and must always be quoted + options: + tap_action: + action: toggle +``` +Also show all lights that are on: +```yaml +type: custom:auto-entities +show_empty: false +card: + type: entities + title: Lights on + show_header_toggle: false +filter: + include: + - domain: light + exclude: + - state: "off" + - state: "unavailable" +``` + + +## About monster-card +This card works very much like [`monster-card`](https://github.com/ciotlosm/custom-lovelace/tree/master/monster-card) with the following exceptions: + +- `auto-entities` has no `when` option. Hiding the card based on the state of an entity is better done with [`conditional`](https://www.home-assistant.io/lovelace/conditional/). +- `auto-entities` supports Regular Expressions. +- `auto-entities` supports comparison operators for states as well as attributes. +- `auto-entities` can add all entities from a group +- `auto-entities` works with custom cards. + +![custom-cards-and-stuff](https://user-images.githubusercontent.com/1299821/51793369-aa3b6480-21bf-11e9-9a00-e7b7b85ba0a2.png) + diff --git a/auto-entities.js b/auto-entities.js new file mode 100644 index 0000000..64f4f04 --- /dev/null +++ b/auto-entities.js @@ -0,0 +1,159 @@ +customElements.whenDefined('card-tools').then(() => { +class AutoEntities extends cardTools.litElement() { + + setConfig(config) { + if(!config || !config.card) + throw new Error("Invalid configuration"); + + this._config = config; + + this.entities = this.get_entities() || []; + this.card = cardTools.createCard({entities: this.entities, ...config.card}); + } + + match(pattern, str){ + if (typeof(str) === "string" && typeof(pattern) === "string") { + if((pattern.startsWith('/') && pattern.endsWith('/')) || pattern.indexOf('*') !== -1) { + if(pattern[0] !== '/') + pattern = `/${pattern.replace(/\*/g, '.*')}/`; + var regex = new RegExp(pattern.substr(1).slice(0,-1)); + return regex.test(str); + } + } else if(typeof(pattern) === "string") { + if(pattern.indexOf(":") !== -1 && typeof(str) === "object") { + while(pattern.indexOf(":") !== -1) + { + str = str[pattern.split(":")[0]]; + pattern = pattern.substr(pattern.indexOf(":")+1, pattern.length); + } + } + if(pattern.startsWith("<=")) return str <= parseFloat(pattern.substr(2)); + if(pattern.startsWith(">=")) return str >= parseFloat(pattern.substr(2)); + if(pattern.startsWith("<")) return str < parseFloat(pattern.substr(1)); + if(pattern.startsWith(">")) return str > parseFloat(pattern.substr(1)); + if(pattern.startsWith("!")) return str != parseFloat(pattern.substr(1)); + if(pattern.startsWith("=")) return str == parseFloat(pattern.substr(1)); + } + return str === pattern; + } + + match_filter(hass, entities, filter) { + let retval = []; + let count = -1; + entities.forEach((i) => { + count++; + if(!hass.states) return; + const e = (typeof(i) === "string")?hass.states[i]:hass.states[i.entity]; + if(!e) return; + + let unmatched = false; + Object.keys(filter).forEach((filterKey) => { + const key = filterKey.split(" ")[0]; + const value = filter[filterKey]; + switch(key) { + case "options": + break; + case "domain": + if(!this.match(value, e.entity_id.split('.')[0])) + unmatched = true; + break; + case "state": + if(!this.match(value, e.state)) + unmatched = true; + break; + case "entity_id": + if(!this.match(value, e.entity_id)) + unmatched = true; + break; + case "name": + if(!e.attributes.friendly_name + || !this.match(value, e.attributes.friendly_name) + ) + unmatched = true; + break; + case "group": + if(!value.startsWith("group.") + || !hass.states[value] + || !hass.states[value].attributes.entity_id + || !hass.states[value].attributes.entity_id.includes(e.entity_id) + ) + unmatched = true; + break; + case "attributes": + Object.keys(value).forEach((entityKey) => { + const k = entityKey.split(" ")[0]; + const v = value[entityKey]; + if(!e.attributes[k] + || !this.match(v, e.attributes[k]) + ) + unmatched = true; + }); + break; + } + }); + if(!unmatched) retval.push(count); + }); + return retval; + } + + get_entities() + { + let entities = []; + if(this._config.entities) + this._config.entities.forEach((e) => entities.push(e)); + + if(this._hass && this._config.filter) { + + if(this._config.filter.include){ + this._config.filter.include.forEach((f) => { + const add = this.match_filter(this._hass, Object.keys(this._hass.states), f) + add.forEach((i) => { + entities.push({entity: Object.keys(this._hass.states)[i], ...f.options}); + }); + }); + } + + if(this._config.filter.exclude) { + this._config.filter.exclude.forEach((f) => { + const remove = this.match_filter(this._hass, entities, f); + for(var i = remove.length-1; i >= 0; i--) + { + entities.splice(remove[i],1); + } + }); + } + } + return entities; + } + + render() { + if(this.entities.length === 0 && this._config.show_empty === false) + return cardTools.litHtml()``; + return cardTools.litHtml()` + ${this.card} + `; + } + + set hass(hass) { + this._hass = hass; + const oldlen = this.entities.length; + this.entities = this.get_entities() || []; + if(this.card) + { + this.card.hass = this._hass; + this.card.setConfig({entities: this.entities, ...this._config.card}); + } + if(this.entities.length != oldlen) this.requestUpdate(); + } + +} + +customElements.define('auto-entities', AutoEntities); +}); + +window.setTimeout(() => { + if(customElements.get('card-tools')) return; + customElements.define('auto-entities', class extends HTMLElement{ + setConfig() { throw new Error("Can't find card-tools. See https://github.com/thomasloven/lovelace-card-tools");} + }); +}, 2000);