Skip to content

Commit

Permalink
Added entity templating
Browse files Browse the repository at this point in the history
  • Loading branch information
marcokreeft87 committed Sep 27, 2022
1 parent 88c4957 commit 240a1e7
Show file tree
Hide file tree
Showing 13 changed files with 131 additions and 24 deletions.
5 changes: 5 additions & 0 deletions info.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@

### Features

{% if version_installed.replace("v", "").replace(".","") | int < 10600 %}
- Added `Notification when entity doest not have attribute given`
- Added `Entity templates to make yaml configs smaller and reduce duplications (https://github.com/marcokreeft87/room-card/wiki/Configuration#templates)`
{% endif %}

{% if version_installed.replace("v", "").replace(".","") | int < 10504 %}
- Fixed `When giving precision and unit error was thrown`
{% endif %}
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "room-card",
"version": "1.05.04",
"version": "1.06.00",
"description": "Show entities in Home Assistant's Lovelace UI",
"keywords": [
"home-assistant",
Expand Down
10 changes: 5 additions & 5 deletions room-card.js

Large diffs are not rendered by default.

Binary file modified room-card.js.gz
Binary file not shown.
8 changes: 1 addition & 7 deletions src/entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { ActionConfig, handleClick, HomeAssistant, NumberFormat } from 'custom-c
import { HomeAssistantEntity, EntityCondition, RoomCardEntity, RoomCardIcon, RoomCardConfig, EntityStyles } from './types/room-card-types';
import { html, HTMLTemplateResult, LitElement } from 'lit';
import { LAST_CHANGED, LAST_UPDATED, TIMESTAMP_FORMATS } from './lib/constants';
import { templateStyling } from './template';

export const checkConfig = (config: RoomCardConfig) => {
if (config.entities == undefined && config.entity == undefined && config.info_entities == undefined && config.rows == undefined) {
Expand Down Expand Up @@ -200,13 +201,6 @@ export const renderIcon = (stateObj: HomeAssistantEntity, config: RoomCardEntity
></state-badge>`;
}

// eslint-disable-next-line @typescript-eslint/ban-types
export const templateStyling = (stateObj: HomeAssistantEntity, config: RoomCardEntity | RoomCardConfig, hass: HomeAssistant) : Function => {
const icon = (config.icon as RoomCardIcon);

return icon?.template?.styles !== undefined ? evalTemplate(hass, stateObj, icon.template.styles) : null;
}

export const renderValue = (entity: RoomCardEntity, hass: HomeAssistant) => {
if (entity.toggle === true) {
return html`<ha-entity-toggle .stateObj="${entity.stateObj}" .hass="${hass}"></ha-entity-toggle>`;
Expand Down
6 changes: 3 additions & 3 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,12 +56,12 @@ export default class RoomCard extends LitElement {
this.entity = { ...this.config, stateObj: this.stateObj };
}

this.info_entities = this.config.info_entities?.map(entity => mapStateObject(entity, hass)) ?? [];
this.info_entities = this.config.info_entities?.map(entity => mapStateObject(entity, hass, this.config)) ?? [];

this.entities = this.config.entities?.map(entity => mapStateObject(entity, hass)) ?? [];
this.entities = this.config.entities?.map(entity => mapStateObject(entity, hass, this.config)) ?? [];
this.rows =
this.config.rows?.map((row) => {
const rowEntities = row.entities?.map(entity => mapStateObject(entity, hass));
const rowEntities = row.entities?.map(entity => mapStateObject(entity, hass, this.config));
return { entities: rowEntities };
}) ?? [];

Expand Down
23 changes: 23 additions & 0 deletions src/template.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { HomeAssistant } from "custom-card-helpers";
import { HomeAssistantEntity, RoomCardConfig, RoomCardEntity, RoomCardIcon } from "./types/room-card-types";
import { evalTemplate } from "./util";

// eslint-disable-next-line @typescript-eslint/ban-types
export const templateStyling = (stateObj: HomeAssistantEntity, config: RoomCardEntity | RoomCardConfig, hass: HomeAssistant) : Function => {
const icon = (config.icon as RoomCardIcon);

return icon?.template?.styles !== undefined ? evalTemplate(hass, stateObj, icon.template.styles) : null;
}

export const mapTemplate = (entity: RoomCardEntity, config: RoomCardConfig) => {
if(entity !== undefined && entity.template) {
const templatesWithMatchingName = config.templates.filter(template => template.name === entity.template);
if(templatesWithMatchingName.length > 0) {
const templateFromConfig = templatesWithMatchingName[0];

return { stateObj: entity.stateObj, ...entity, ...templateFromConfig.template };
}
}

return entity;
}
12 changes: 12 additions & 0 deletions src/types/room-card-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ export interface RoomCardEntity {
show_state?: boolean;
styles?: EntityStyles;
icon?: string | RoomCardIcon;
template?: string;
}

export interface EntityStyles {
Expand All @@ -41,6 +42,7 @@ export interface RoomCardConfig extends LovelaceCardConfig {
title?: string;
name?: string;
styles?: EntityStyles;
templates?: RoomCardTemplateContainer[]
}

export interface RoomCardRow {
Expand Down Expand Up @@ -82,4 +84,14 @@ export interface FormattingOptions {
maximumFractionDigits?: number;
style?: string;
currency?: string;
}

export interface RoomCardTemplateContainer {
name: string;
template: RoomCardTemplateDefinition;
}

export interface RoomCardTemplateDefinition {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
[key: string]: any;
}
12 changes: 10 additions & 2 deletions src/util.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { html, PropertyValues } from 'lit';
import { HassEntity } from 'home-assistant-js-websocket';
import { UNAVAILABLE_STATES } from './lib/constants';
import { HomeAssistantEntity, RoomCardConfig, RoomCardEntity, EntityCondition, HideIfConfig } from './types/room-card-types';
import { mapTemplate } from './template';

export const isObject = (obj: unknown) : boolean => typeof obj === 'object' && !Array.isArray(obj) && !!obj;

Expand All @@ -12,6 +13,10 @@ export const hideUnavailable = (entity: RoomCardEntity) : boolean =>
entity.hide_unavailable && isUnavailable(entity.stateObj);

export const getValue = (entity: RoomCardEntity) => {
if(entity.attribute && entity.stateObj.attributes[entity.attribute] === undefined) {
throw new Error(`Entity: '${entity.entity}' has no attribute named '${entity.attribute}'`);
}

return entity.attribute ? entity.stateObj.attributes[entity.attribute] : entity.stateObj.state;
}

Expand Down Expand Up @@ -78,8 +83,11 @@ export const checkConditionalValue = (item: EntityCondition, checkValue: unknown
}
}

export const mapStateObject = (entity: RoomCardEntity | string, hass: HomeAssistant) : RoomCardEntity => {
const conf = typeof entity === 'string' ? { entity: entity } : entity;
export const mapStateObject = (entity: RoomCardEntity | string, hass: HomeAssistant, config: RoomCardConfig) : RoomCardEntity => {
let conf = typeof entity === 'string' ? { entity: entity } : entity;

conf = mapTemplate(conf as RoomCardEntity, config);

return { ...conf, stateObj: hass.states[conf.entity] };
}

Expand Down
61 changes: 61 additions & 0 deletions tests/template/mapTemplate.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { createMock } from 'ts-auto-mock';
import { mapTemplate } from '../../src/template';
import { RoomCardConfig, RoomCardEntity } from '../../src/types/room-card-types';

describe('Testing template file function mapTemplate', () => {
const entity = createMock<RoomCardEntity>();
entity.template = 'test_template';

test('Passing entity and config with templates should map properties', () => {

const config: RoomCardConfig = {
entityIds: [],
type: '',
templates: [{
name: 'test_template',
template: {
'show_icon': true,
'show_state': true
}
}]
}

const expectedEntity: RoomCardEntity = { stateObj: entity.stateObj, show_icon: true, show_state: true };
expect(mapTemplate(entity, config)).toMatchObject(expectedEntity);
}),
test('Passing entity and config with complex templates should map properties', () => {

const config: RoomCardConfig = {
entityIds: [],
type: '',
templates: [{
name: 'test_template',
template: {
'show_icon': true,
'show_state': true,
'icon': {
conditions: [{
condition: 'above',
value: '1',
icon: 'mdi:test'
}]
}
}
}]
}

const expectedEntity: RoomCardEntity = {
stateObj: entity.stateObj,
show_icon: true,
show_state: true,
icon: {
conditions: [{
condition: 'above',
value: '1',
icon: 'mdi:test'
}]
}
};
expect(mapTemplate(entity, config)).toMatchObject(expectedEntity);
})
});
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import { HomeAssistant } from 'custom-card-helpers';
import { createMock } from 'ts-auto-mock';
import { HassEntity } from 'home-assistant-js-websocket';
import { templateStyling } from '../../src/entity';
import { templateStyling } from '../../src/template';
import { HomeAssistantEntity, RoomCardConfig } from '../../src/types/room-card-types';

describe('Testing entity file function templateStyling', () => {
describe('Testing template file function templateStyling', () => {
const hass = createMock<HomeAssistant>();
const stateObj = createMock<HomeAssistantEntity>();

Expand Down
2 changes: 1 addition & 1 deletion tests/util/getValue.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ describe('Testing util file function getValue', () => {
StubRoomCardEntity.stateObj.state = 'on';
StubRoomCardEntity.attribute = 'nonexisting';
StubRoomCardEntity.stateObj.attributes['testattribute'] = 'test'
expect(getValue(StubRoomCardEntity)).toBe(undefined);
expect(() => getValue(StubRoomCardEntity)).toThrowError(`Entity: '${StubRoomCardEntity.entity}' has no attribute named '${StubRoomCardEntity.attribute}'`);
})
})

10 changes: 7 additions & 3 deletions tests/util/mapStateObject.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,11 @@
import { createMock } from 'ts-auto-mock';
import { RoomCardConfig } from '../../src/types/room-card-types';
import { mapStateObject } from '../../src/util';
import { StubHassEntity, StubHomeAssistant, StubRoomCardEntity } from '../testdata';

describe('Testing util file function mapStateObject', () => {
const config = createMock<RoomCardConfig>();

test('Passing RoomCardEntity should return entity with stateObj', () => {
StubHassEntity.entity_id = 'sensor.test_entity';

Expand All @@ -10,7 +14,7 @@ describe('Testing util file function mapStateObject', () => {
'sensor.test_entity': StubHassEntity
};

expect(mapStateObject(StubRoomCardEntity, StubHomeAssistant)).toHaveProperty("stateObj", StubHassEntity);
expect(mapStateObject(StubRoomCardEntity, StubHomeAssistant, config)).toHaveProperty("stateObj", StubHassEntity);
}),
test('Passing RoomCardEntity should return entity with stateObj', () => {
StubHassEntity.entity_id = 'sensor.test_entity';
Expand All @@ -19,7 +23,7 @@ describe('Testing util file function mapStateObject', () => {
'sensor.test_entity': StubHassEntity
};

expect(mapStateObject(StubRoomCardEntity, StubHomeAssistant)).toHaveProperty("stateObj", StubHassEntity);
expect(mapStateObject(StubRoomCardEntity, StubHomeAssistant, config)).toHaveProperty("stateObj", StubHassEntity);
}),
test('Passing RoomCardEntity should return entity with stateObj', () => {
StubHassEntity.entity_id = 'sensor.test_entity';
Expand All @@ -28,7 +32,7 @@ describe('Testing util file function mapStateObject', () => {
'sensor.test_entity': StubHassEntity
};

expect(mapStateObject('sensor.test_entity', StubHomeAssistant)).toHaveProperty("stateObj", StubHassEntity);
expect(mapStateObject('sensor.test_entity', StubHomeAssistant, config)).toHaveProperty("stateObj", StubHassEntity);
})
})

0 comments on commit 240a1e7

Please sign in to comment.