From f18a97ffb971107f24cb76da2374b10d0ea3cf9d Mon Sep 17 00:00:00 2001 From: jharris4 Date: Mon, 3 Jun 2024 12:03:19 -0400 Subject: [PATCH 1/2] add names config option for name transforms --- src/main.ts | 161 +++++++++++++++++++++++++++++++++++++++++++++++++++ src/types.ts | 41 +++++++++++++ 2 files changed, 202 insertions(+) diff --git a/src/main.ts b/src/main.ts index 9e2664b..2e0ff6a 100644 --- a/src/main.ts +++ b/src/main.ts @@ -5,6 +5,7 @@ import { bind_template, unbind_template } from "./templates"; import { filter_entity } from "./filter"; import { get_sorter } from "./sort"; import { + EntityNameConfig, AutoEntitiesConfig, EntityList, HuiErrorCard, @@ -59,6 +60,9 @@ class AutoEntities extends LitElement { if (!config.filter && !config.entities) { throw new Error("No filters specified."); } + if (config.names && !config.names.transforms) { + throw new Error("No names transforms specified."); + } config = JSON.parse(JSON.stringify(config)); this._config = config; @@ -276,6 +280,163 @@ class AutoEntities extends LitElement { } } + if (this._config.names) { + const { transforms } = this._config.names; + if (transforms && transforms.length > 0) { + const ents = await getEntities(this.hass); + const devs = await getDevices(this.hass); + const areas = await getAreas(this.hass); + + const findEnt = entity => ents.find((e) => e.entity_id === entity.entity); + const findDev = ent => devs.find((e) => e.id === ent.device_id); + const findArea = ent => areas.find((a) => a.area_id === ent.area_id); + + const getEntName = (entity: LovelaceRowConfig, ent: any) => { + let name = ""; + if (entity.name) { + name = entity.name; + } else if (ent.friendly_name) { + name = ent.friendly_name; + } else if (ent.name) { + name = ent.name; + } else if (ent.device_id) { + const device = findDev(ent); + if (device && device.name) { + if (ent.original_name) { + if (ent.original_name !== device.name) { + if (ent.original_name.startsWith(device.name)) { + name = ent.original_name; + } else { + name = device.name + " " + ent.original_name; + } + } else { + name = device.name; + } + } else { + name = device.name; + } + } else if (ent.original_name) { + name = ent.original_name; + } + } else if (ent.original_name) { + name = ent.original_name; + } + return name; + }; + + const getEntAreaName = ent => { + let area; + if (ent.area_id) { + area = findArea(ent); + } + if (!area && ent.device_id !== undefined) { + const device = findDev(ent); + if (device) { + area = findArea(device); + } + } + if (area && area.name) { + return area.name; + } + return ""; + }; + + const getEntGroupName = group => { + const groupEnt = this.hass.states[group]; + if (groupEnt && groupEnt.attributes.friendly_name) { + return groupEnt.attributes.friendly_name; + } + return ""; + }; + + const getEntDeviceManufacturerName = ent => { + if (ent.device_id !== undefined) { + const device = findDev(ent); + if (device && device.manufacturer) { + return device.manufacturer; + } + } + return ""; + }; + + const getEntDeviceModelName = ent => { + if (ent.device_id !== undefined) { + const device = findDev(ent); + if (device && device.model) { + return device.model; + } + } + return ""; + }; + + const getEntNameForConfig = (ent: any, config: EntityNameConfig) => { + const { text, area, group, device_manufacturer, device_model } = config; + if (text) { + return text; + } else if (area) { + return getEntAreaName(ent); + } else if (group) { + return getEntGroupName(group); + } else if (device_manufacturer) { + return getEntDeviceManufacturerName(ent); + } else if (device_model) { + return getEntDeviceModelName(ent); + } + }; + + const newEntities = entities.map(e => ({...e})); + + for (let transform of transforms) { + const { type, trim } = transform; + if (type === "set" || type === "prefix" || type === "suffix") { + const { value } = transform; + if (value) { + let getEntityName = (oldName, newName) => oldName; + if (type === "set") { + getEntityName = (oldName, newName) => newName; + } else if (type === "prefix") { + getEntityName = (oldName, newName) => newName + oldName; + } else if (type === "suffix") { + getEntityName = (oldName, newName) => oldName + newName; + } + for (let entity of newEntities) { + const ent = findEnt(entity); + if (ent) { + const oldName = getEntName(entity, ent); + const newName = getEntNameForConfig(ent, value); + if (oldName && newName) { + entity.name = getEntityName(oldName, newName); + if (trim) { + entity.name = entity.name.trim(); + } + } + } + } + } + } else if (type === "replace") { + const { match, replacement } = transform; + if (match) { + for (let entity of newEntities) { + const ent = findEnt(entity); + if (ent) { + const oldName = getEntName(entity, ent); + const matchName = getEntNameForConfig(ent, match); + if (oldName && matchName) { + const replaceName = replacement ? getEntNameForConfig(ent, replacement) : ""; + entity.name = oldName.replace(matchName, replaceName); + if (trim) { + entity.name = entity.name.trim(); + } + } + } + } + } + } + } + entities = newEntities; + } + } + if (this._config.sort) { entities = entities.sort(get_sorter(this.hass, this._config.sort)); if (this._config.sort.count) { diff --git a/src/types.ts b/src/types.ts index 3f18b7b..53aef0c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,6 +39,45 @@ interface FilterConfig { type?: string; } +export interface EntityNameConfig { + text?: string; + group?: string; + area?: boolean; + device_manufacturer?: boolean; + device_model?: boolean; +} + +interface NamesTransformSetConfig { + type: "set"; + value: EntityNameConfig; + trim?: boolean; +} + +interface NamesTransformPrefixConfig { + type: "prefix"; + value: EntityNameConfig; + trim?: boolean; +} + +interface NamesTransformSuffixConfig { + type: "suffix"; + value: EntityNameConfig; + trim?: boolean; +} + +interface NamesTransformReplaceConfig { + type: "replace"; + match: EntityNameConfig; + replacement?: EntityNameConfig; + trim?: boolean; +} + +export type NamesTransformConfig = NamesTransformSetConfig | NamesTransformPrefixConfig | NamesTransformSuffixConfig | NamesTransformReplaceConfig; + +export interface NamesConfig { + transforms: NamesTransformConfig[]; +} + export interface AutoEntitiesConfig { card: any; entities: Array; @@ -47,6 +86,7 @@ export interface AutoEntitiesConfig { include?: FilterConfig[]; exclude?: FilterConfig[]; }; + names?: NamesConfig; card_param?: string; @@ -61,6 +101,7 @@ export interface AutoEntitiesConfig { export interface LovelaceRowConfig { entity?: string; type?: string; + name?: string; } export interface LovelaceCard extends HTMLElement { hass: any; From ac229e1af5d4fcfa589f8290db8d20764fc4f450 Mon Sep 17 00:00:00 2001 From: jharris4 Date: Mon, 3 Jun 2024 15:14:56 -0400 Subject: [PATCH 2/2] add docs for names option to README --- README.md | 98 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/README.md b/README.md index b784d08..c7d0b7e 100644 --- a/README.md +++ b/README.md @@ -37,6 +37,8 @@ sort: - `template:` A jinja2 template evaluating to a list of entries to include - `include:` A list of filters specifying which entities to add to the card - `exclude:` A list of filters specifying which entities to remove from the card +- `names:` + - `transforms:` A list of transforms to be applied to all entity names - `show_empty:` Whether to display the card if it has no entities. Default: `true`. Note that any filter with a `type` option will be ignored. - `else:` Card to display if main card has no entities. Overrides `show_empty`. - `unique:` Whether to remove duplicate values after filtering and sorting. Set to `true` to remove exact duplicate entries. Set to `entity` to remove entries with the same entity id. Default: `false`. @@ -79,6 +81,102 @@ Special options: The filter section `template` takes a jinja2 template which evaluates to a list of entities or entity objects. +### Names + +Names `transforms` have the following options: + +- `type:` The type of the transform. Supported types are `set`, `prefix`, `suffix` and `replace`. +- `value:` For `set`, `prefix` and `suffix` types this specifies the new name value to use. +- `match:` For `replace` type, this specifies the name value to be replaced. +- `replacement:` For `replace` type, this specifies the name value to be used as the replacement (Defaults to empty string if not specified). +- `trim:` Whether or not to trim whitespace from the name after performing a transform on it. + +The `value` or `match` and `replacement` options are all in term composed of the following sub options which are all optional although one must be specified: + +- `text:` A plain text string value for a name. +- `group:` The entity id of a group whose name to use. +- `area:` Whether to use the area name of the entity. +- `device_manufacturer:` Whether to use the device manufacturer of the entity. +- `device_model:` Whether to use the device manufacturer of the entity. + +Show a list of lights and set their name to a group: + +```yaml +filter: + include: + domain: light +names: + transforms: + - type: set + value: + group: light.my_light_group +``` + +Show a list of lights and set their name to their area: + +```yaml +filter: + include: + domain: light +names: + transforms: + - type: set + value: + area: true +``` + +Remove the room name from the names of a list of lights: + +```yaml +filter: + include: + domain: light +names: + transforms: + - type: replace + match: + area: true + # this is the same as: + # match: + # area: true + # replace: + # text: "" +``` + +Replace the room name with device model from the names of a list of lights: + +```yaml +filter: + include: + domain: light +names: + transforms: + - type: replace + match: + area: true + replace: + device_model: true +``` + +Remove area and then some text from the names of a list of lights: + +```yaml +filter: + include: + domain: light +names: + transforms: + - type: replace + match: + area: true + - type: replace + match: + text: Lights + - type: replace + match: + text: Light +``` + ## How it works `auto-entities` creates a list of entities by: