From 144b5b3ef53b46a2a17e3f563d767e983e1dc9e1 Mon Sep 17 00:00:00 2001 From: Max Chodorowski Date: Sat, 17 Feb 2024 02:32:17 +0000 Subject: [PATCH 1/2] Entity not available warning --- src/custom-elements/battery-state-entity.ts | 10 +++++++ src/entity-fields/battery-level.ts | 4 +-- src/entity-fields/get-name.ts | 4 +-- test/card/entity-list.test.ts | 24 +++++++++++++++++ test/helpers.ts | 27 +++++++++++++------ .../other/entity-fields/battery-level.test.ts | 12 --------- test/other/entity-fields/get-name.test.ts | 21 ++------------- 7 files changed, 59 insertions(+), 43 deletions(-) diff --git a/src/custom-elements/battery-state-entity.ts b/src/custom-elements/battery-state-entity.ts index 210b83df..3f678997 100644 --- a/src/custom-elements/battery-state-entity.ts +++ b/src/custom-elements/battery-state-entity.ts @@ -84,6 +84,16 @@ export class BatteryStateEntity extends LovelaceCard { } async internalUpdate() { + + if (!this.hass?.states[this.config.entity]) { + this.alert = { + type: "warning", + title: this.hass?.localize("ui.panel.lovelace.warning.entity_not_found", "entity", this.config.entity) || `Entity not available: ${this.config.entity}`, + } + + return; + } + this.entityData = { ...this.hass?.states[this.config.entity] }; diff --git a/src/entity-fields/battery-level.ts b/src/entity-fields/battery-level.ts index 4d475c79..a1f3db55 100644 --- a/src/entity-fields/battery-level.ts +++ b/src/entity-fields/battery-level.ts @@ -50,8 +50,8 @@ export const getBatteryLevel = (config: IBatteryEntityConfig, hass: HomeAssistan } else { const candidates: (string | number | undefined)[] = [ - config.non_battery_entity ? null: entityData.attributes?.battery_level, - config.non_battery_entity ? null: entityData.attributes?.battery, + config.non_battery_entity ? null: entityData.attributes.battery_level, + config.non_battery_entity ? null: entityData.attributes.battery, entityData.state ]; diff --git a/src/entity-fields/get-name.ts b/src/entity-fields/get-name.ts index a55c59c6..422a6e47 100644 --- a/src/entity-fields/get-name.ts +++ b/src/entity-fields/get-name.ts @@ -9,13 +9,13 @@ import { RichStringProcessor } from "../rich-string-processor"; * @param hass HomeAssistant state object * @returns Battery name */ -export const getName = (config: IBatteryEntityConfig, hass: HomeAssistant | undefined, entityData: IMap | undefined): string => { +export const getName = (config: IBatteryEntityConfig, hass: HomeAssistant | undefined, entityData: IMap): string => { if (config.name) { const proc = new RichStringProcessor(hass, entityData); return proc.process(config.name); } - let name = entityData?.attributes?.friendly_name; + let name = entityData.attributes.friendly_name; // when we have failed to get the name we just return entity id if (!name) { diff --git a/test/card/entity-list.test.ts b/test/card/entity-list.test.ts index 042a4729..6ef1941a 100644 --- a/test/card/entity-list.test.ts +++ b/test/card/entity-list.test.ts @@ -51,4 +51,28 @@ test("Entities as objects with custom settings", async () => { expect(card.itemsCount).toBe(2); expect(card.item(0).nameText).toBe("Entity 1"); expect(card.item(1).nameText).toBe("Entity 2"); +}); + +test("Missing entity", async () => { + const hass = new HomeAssistantMock(); + const motionSensor = hass.addEntity("Bedroom motion battery level", "90"); + + const cardElem = hass.addCard("battery-state-card", { + title: "Header", + entities: [ // array of entity IDs + { + entity: motionSensor.entity_id + "_missing", + }, + ] + }); + + // waiting for card to be updated/rendered + await cardElem.cardUpdated; + + const card = new CardElements(cardElem); + + expect(card.itemsCount).toBe(1); + expect(card.item(0).isAlert).toBeTruthy(); + expect(card.item(0).alertType).toBe("warning"); + expect(card.item(0).alertTitle).toBe("[ui.panel.lovelace.warning.entity_not_found, entity, bedroom_motion_battery_level_missing]"); }); \ No newline at end of file diff --git a/test/helpers.ts b/test/helpers.ts index 86141f3a..92c9b09d 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -72,6 +72,11 @@ export class EntityElements { private root: HTMLElement; constructor(private card: BatteryStateEntity, isShadowRoot: boolean = true) { + + if (isShadowRoot && !card.shadowRoot) { + throw Error("Missing shaddow root"); + } + this.root = isShadowRoot ? card.shadowRoot! : card; } @@ -97,6 +102,18 @@ export class EntityElements { ?.trim() .replace(String.fromCharCode(160), " "); // replace non breakable space } + + get isAlert() { + return !!this.root.querySelector("ha-alert"); + } + + get alertType() { + return this.root.querySelector("ha-alert")?.getAttribute("alert-type"); + } + + get alertTitle() { + return this.root.querySelector("ha-alert")?.getAttribute("title"); + } } export class GroupElement extends EntityElements { @@ -138,7 +155,7 @@ export class HomeAssistantMock> { public hass: HomeAssistantExt = { states: {}, - localize: jest.fn((key: string) => `[${key}]`), + localize: jest.fn((...data: string[]) => `[${data.join(", ")}]`), formatEntityState: jest.fn((entityData: any) => `${entityData.state} %`), }; @@ -195,12 +212,6 @@ export class HomeAssistantMock> { return entity; }, setAttributes: (attribs: IEntityAttributes) => { - - if (attribs === null) { - this.hass.states[entity.entity_id].attributes = undefined; - return entity; - } - this.hass.states[entity.entity_id].attributes = { ...this.hass.states[entity.entity_id].attributes, ...attribs @@ -248,7 +259,7 @@ interface IEntityMock { readonly entity_id: string; readonly state: string; setState(state: string): IEntityMock; - setAttributes(attribs: IEntityAttributes | null): IEntityMock; + setAttributes(attribs: IEntityAttributes): IEntityMock; setLastUpdated(val: string): void; setLastChanged(val: string): void; setProperty(name: K, val: HaEntityPropertyToTypeMap[K]): void; diff --git a/test/other/entity-fields/battery-level.test.ts b/test/other/entity-fields/battery-level.test.ts index 9bfd4848..b1e45fc6 100644 --- a/test/other/entity-fields/battery-level.test.ts +++ b/test/other/entity-fields/battery-level.test.ts @@ -12,18 +12,6 @@ describe("Battery level", () => { expect(unit).toBe("%"); }); - test("doen't throw exception when attributes are not set on entity", () => { - const hassMock = new HomeAssistantMock(true); - const entity = hassMock.addEntity("Mocked entity", "45", { battery_state: "45" }); - entity.setAttributes(null); - - const { state, level, unit } = getBatteryLevel({ entity: "mocked_entity" }, hassMock.hass, hassMock.hass.states["mocked_entity"]); - - expect(level).toBe(45); - expect(state).toBe("45"); - expect(unit).toBe("%") - }); - test("is 'Unknown' when entity not found and no localized string", () => { const hassMock = new HomeAssistantMock(true); hassMock.hass.localize = () => null; diff --git a/test/other/entity-fields/get-name.test.ts b/test/other/entity-fields/get-name.test.ts index 78db2da9..bf8a786e 100644 --- a/test/other/entity-fields/get-name.test.ts +++ b/test/other/entity-fields/get-name.test.ts @@ -9,22 +9,12 @@ describe("Get name", () => { expect(name).toBe("Entity name"); }); - test("returns entity id when name and hass is missing", () => { - let name = getName({ entity: "sensor.my_entity_id" }, undefined, {}) + test("returns entity id when friendly_name is missing", () => { + let name = getName({ entity: "sensor.my_entity_id" }, undefined, { attributes: {} }) expect(name).toBe("sensor.my_entity_id"); }); - test("doesn't throw exception when attributes property is missing", () => { - const hassMock = new HomeAssistantMock(true); - const entity = hassMock.addEntity("My entity", "45", { friendly_name: "My entity name" }); - entity.setAttributes(null); - - let name = getName({ entity: "my_entity" }, hassMock.hass, {}); - - expect(name).toBe("my_entity"); - }); - test("returns name from friendly_name attribute of the entity", () => { const hassMock = new HomeAssistantMock(true); const entity = hassMock.addEntity("My entity", "45", { friendly_name: "My entity name" }); @@ -34,13 +24,6 @@ describe("Get name", () => { expect(name).toBe("My entity name"); }); - test("returns entity id when entity not found in hass", () => { - const hassMock = new HomeAssistantMock(true); - let name = getName({ entity: "my_entity_missing" }, hassMock.hass, {}); - - expect(name).toBe("my_entity_missing"); - }); - test("returns entity id when entity doesn't have a friendly_name attribute", () => { const hassMock = new HomeAssistantMock(true); const entity = hassMock.addEntity("My entity", "45", { friendly_name: undefined }); From ba61aa2fa604f6d8dfe0aa80b243cb687eb79a6c Mon Sep 17 00:00:00 2001 From: Max Chodorowski Date: Sat, 17 Feb 2024 02:41:49 +0000 Subject: [PATCH 2/2] Made hass mandatory --- src/custom-elements/battery-state-entity.ts | 2 +- src/entity-fields/battery-level.ts | 2 +- src/entity-fields/charging-state.ts | 7 +------ src/entity-fields/get-icon.ts | 6 +++--- src/entity-fields/get-name.ts | 2 +- src/entity-fields/get-secondary-info.ts | 2 +- src/rich-string-processor.ts | 4 ++-- test/other/entity-fields/charging-state.test.ts | 9 --------- test/other/entity-fields/get-icon.test.ts | 8 ++++---- test/other/entity-fields/get-name.test.ts | 5 ++--- 10 files changed, 16 insertions(+), 31 deletions(-) diff --git a/src/custom-elements/battery-state-entity.ts b/src/custom-elements/battery-state-entity.ts index 3f678997..6c711086 100644 --- a/src/custom-elements/battery-state-entity.ts +++ b/src/custom-elements/battery-state-entity.ts @@ -95,7 +95,7 @@ export class BatteryStateEntity extends LovelaceCard { } this.entityData = { - ...this.hass?.states[this.config.entity] + ...this.hass.states[this.config.entity] }; if (this.config.extend_entity_data !== false) { diff --git a/src/entity-fields/battery-level.ts b/src/entity-fields/battery-level.ts index a1f3db55..0b8ac6df 100644 --- a/src/entity-fields/battery-level.ts +++ b/src/entity-fields/battery-level.ts @@ -19,7 +19,7 @@ const formattedStatePattern = /(-?[0-9,.]+)\s?(.*)/; * @param hass HomeAssistant state object * @returns Battery level */ -export const getBatteryLevel = (config: IBatteryEntityConfig, hass: HomeAssistantExt | undefined, entityData: IMap | undefined): IBatteryState => { +export const getBatteryLevel = (config: IBatteryEntityConfig, hass: HomeAssistantExt, entityData: IMap | undefined): IBatteryState => { const UnknownLevel = hass?.localize("state.default.unknown") || "Unknown"; let state: string; let unit: string | undefined; diff --git a/src/entity-fields/charging-state.ts b/src/entity-fields/charging-state.ts index e95f194d..4092e5bd 100644 --- a/src/entity-fields/charging-state.ts +++ b/src/entity-fields/charging-state.ts @@ -8,12 +8,7 @@ import { log, safeGetArray } from "../utils"; * @param hass HomeAssistant state object * @returns Whether battery is in chargin mode */ - export const getChargingState = (config: IBatteryEntityConfig, state: string, hass?: HomeAssistant): boolean => { - - if (!hass) { - return false; - } - + export const getChargingState = (config: IBatteryEntityConfig, state: string, hass: HomeAssistant): boolean => { const chargingConfig = config.charging_state; if (!chargingConfig) { return getDefaultChargingState(config, hass); diff --git a/src/entity-fields/get-icon.ts b/src/entity-fields/get-icon.ts index e1501f0d..190fe6e3 100644 --- a/src/entity-fields/get-icon.ts +++ b/src/entity-fields/get-icon.ts @@ -10,7 +10,7 @@ import { RichStringProcessor } from "../rich-string-processor"; * @param hass HomeAssistant state object * @returns Mdi icon string */ -export const getIcon = (config: IBatteryEntityConfig, level: number | undefined, isCharging: boolean, hass: HomeAssistant | undefined): string => { +export const getIcon = (config: IBatteryEntityConfig, level: number | undefined, isCharging: boolean, hass: HomeAssistant): string => { if (isCharging && config.charging_state?.icon) { return config.charging_state.icon; } @@ -18,7 +18,7 @@ export const getIcon = (config: IBatteryEntityConfig, level: number | undefined, if (config.icon) { const attribPrefix = "attribute."; // check if we should return the icon/string from the attribute value - if (hass && config.icon.startsWith(attribPrefix)) { + if (config.icon.startsWith(attribPrefix)) { const attribName = config.icon.substr(attribPrefix.length); const val = hass.states[config.entity].attributes[attribName] as string | undefined; if (!val) { @@ -29,7 +29,7 @@ export const getIcon = (config: IBatteryEntityConfig, level: number | undefined, return val; } - const processor = new RichStringProcessor(hass, { ...hass?.states[config.entity] }); + const processor = new RichStringProcessor(hass, { ...hass.states[config.entity] }); return processor.process(config.icon); } diff --git a/src/entity-fields/get-name.ts b/src/entity-fields/get-name.ts index 422a6e47..026b8297 100644 --- a/src/entity-fields/get-name.ts +++ b/src/entity-fields/get-name.ts @@ -9,7 +9,7 @@ import { RichStringProcessor } from "../rich-string-processor"; * @param hass HomeAssistant state object * @returns Battery name */ -export const getName = (config: IBatteryEntityConfig, hass: HomeAssistant | undefined, entityData: IMap): string => { +export const getName = (config: IBatteryEntityConfig, hass: HomeAssistant, entityData: IMap): string => { if (config.name) { const proc = new RichStringProcessor(hass, entityData); return proc.process(config.name); diff --git a/src/entity-fields/get-secondary-info.ts b/src/entity-fields/get-secondary-info.ts index addaea5d..5497d3fa 100644 --- a/src/entity-fields/get-secondary-info.ts +++ b/src/entity-fields/get-secondary-info.ts @@ -9,7 +9,7 @@ import { isNumber } from "../utils"; * @param entidyData Entity data * @returns Secondary info text */ -export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssistant | undefined, entityData: IMap | undefined): string => { +export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssistant, entityData: IMap | undefined): string => { if (config.secondary_info) { const processor = new RichStringProcessor(hass, entityData); diff --git a/src/rich-string-processor.ts b/src/rich-string-processor.ts index 3ab52ce5..b3b7b4cf 100644 --- a/src/rich-string-processor.ts +++ b/src/rich-string-processor.ts @@ -35,7 +35,7 @@ const validEntityDomains = [ */ export class RichStringProcessor { - constructor(private hass: HomeAssistant | undefined, private entityData: IMap | undefined) { + constructor(private hass: HomeAssistant, private entityData: IMap | undefined) { } /** @@ -87,7 +87,7 @@ const validEntityDomains = [ if (validEntityDomains.includes(chunks[0])) { data = { - ...this.hass?.states[chunks.splice(0, 2).join(".")] + ...this.hass.states[chunks.splice(0, 2).join(".")] }; } diff --git a/test/other/entity-fields/charging-state.test.ts b/test/other/entity-fields/charging-state.test.ts index 5098a04c..97145bb6 100644 --- a/test/other/entity-fields/charging-state.test.ts +++ b/test/other/entity-fields/charging-state.test.ts @@ -8,15 +8,6 @@ describe("Charging state", () => { const hassMock = new HomeAssistantMock(true); const isCharging = getChargingState({ entity: "any" }, "90", hassMock.hass); - expect(isCharging).toBe(false); - }) - - test("is false when there is no hass", () => { - const isCharging = getChargingState( - { entity: "sensor.my_entity", charging_state: { attribute: [ { name: "is_charging", value: "true" } ] } }, - "45", - undefined); - expect(isCharging).toBe(false); }) diff --git a/test/other/entity-fields/get-icon.test.ts b/test/other/entity-fields/get-icon.test.ts index ea670d87..99c9d09d 100644 --- a/test/other/entity-fields/get-icon.test.ts +++ b/test/other/entity-fields/get-icon.test.ts @@ -3,7 +3,7 @@ import { HomeAssistantMock } from "../../helpers"; describe("Get icon", () => { test("charging and charging icon set in config", () => { - let icon = getIcon({ entity: "", charging_state: { icon: "mdi:custom" } }, 20, true, undefined); + let icon = getIcon({ entity: "", charging_state: { icon: "mdi:custom" } }, 20, true, new HomeAssistantMock(true).hass); expect(icon).toBe("mdi:custom"); }); @@ -12,7 +12,7 @@ describe("Get icon", () => { [200], [NaN], ])("returns unknown state icon when invalid state passed", (invalidEntityState: number) => { - let icon = getIcon({ entity: "" }, invalidEntityState, false, undefined); + let icon = getIcon({ entity: "" }, invalidEntityState, false, new HomeAssistantMock(true).hass); expect(icon).toBe("mdi:battery-unknown"); }); @@ -38,12 +38,12 @@ describe("Get icon", () => { [95, true, "mdi:battery-charging-100"], [100, true, "mdi:battery-charging-100"], ])("returns correct state icon", (batteryLevel: number, isCharging: boolean, expectedIcon: string) => { - let icon = getIcon({ entity: "" }, batteryLevel, isCharging, undefined); + let icon = getIcon({ entity: "" }, batteryLevel, isCharging, new HomeAssistantMock(true).hass); expect(icon).toBe(expectedIcon); }); test("returns custom icon from config", () => { - let icon = getIcon({ entity: "", icon: "mdi:custom" }, 20, false, undefined); + let icon = getIcon({ entity: "", icon: "mdi:custom" }, 20, false, new HomeAssistantMock(true).hass); expect(icon).toBe("mdi:custom"); }); diff --git a/test/other/entity-fields/get-name.test.ts b/test/other/entity-fields/get-name.test.ts index bf8a786e..34bcacee 100644 --- a/test/other/entity-fields/get-name.test.ts +++ b/test/other/entity-fields/get-name.test.ts @@ -3,14 +3,13 @@ import { HomeAssistantMock } from "../../helpers"; describe("Get name", () => { test("returns name from the config", () => { - const hassMock = new HomeAssistantMock(true); - let name = getName({ entity: "test", name: "Entity name" }, hassMock.hass, {}) + let name = getName({ entity: "test", name: "Entity name" }, new HomeAssistantMock(true).hass, {}) expect(name).toBe("Entity name"); }); test("returns entity id when friendly_name is missing", () => { - let name = getName({ entity: "sensor.my_entity_id" }, undefined, { attributes: {} }) + let name = getName({ entity: "sensor.my_entity_id" }, new HomeAssistantMock(true).hass, { attributes: {} }) expect(name).toBe("sensor.my_entity_id"); });