Skip to content

Commit

Permalink
Merge pull request #701 from maxwroc/MissingEntityAlert
Browse files Browse the repository at this point in the history
Entity not available warning
  • Loading branch information
maxwroc committed Feb 17, 2024
2 parents a89e108 + ba61aa2 commit 4d50880
Show file tree
Hide file tree
Showing 13 changed files with 73 additions and 72 deletions.
12 changes: 11 additions & 1 deletion src/custom-elements/battery-state-entity.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,18 @@ export class BatteryStateEntity extends LovelaceCard<IBatteryEntityConfig> {
}

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 = <any>{
...this.hass?.states[this.config.entity]
...this.hass.states[this.config.entity]
};

if (this.config.extend_entity_data !== false) {
Expand Down
6 changes: 3 additions & 3 deletions src/entity-fields/battery-level.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> | undefined): IBatteryState => {
export const getBatteryLevel = (config: IBatteryEntityConfig, hass: HomeAssistantExt, entityData: IMap<any> | undefined): IBatteryState => {
const UnknownLevel = hass?.localize("state.default.unknown") || "Unknown";
let state: string;
let unit: string | undefined;
Expand Down Expand Up @@ -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
];

Expand Down
7 changes: 1 addition & 6 deletions src/entity-fields/charging-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
6 changes: 3 additions & 3 deletions src/entity-fields/get-icon.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,15 @@ 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;
}

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) {
Expand All @@ -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);
}

Expand Down
4 changes: 2 additions & 2 deletions src/entity-fields/get-name.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> | undefined): string => {
export const getName = (config: IBatteryEntityConfig, hass: HomeAssistant, entityData: IMap<any>): 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) {
Expand Down
2 changes: 1 addition & 1 deletion src/entity-fields/get-secondary-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<any> | undefined): string => {
export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssistant, entityData: IMap<any> | undefined): string => {
if (config.secondary_info) {
const processor = new RichStringProcessor(hass, entityData);

Expand Down
4 changes: 2 additions & 2 deletions src/rich-string-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ const validEntityDomains = [
*/
export class RichStringProcessor {

constructor(private hass: HomeAssistant | undefined, private entityData: IMap<any> | undefined) {
constructor(private hass: HomeAssistant, private entityData: IMap<any> | undefined) {
}

/**
Expand Down Expand Up @@ -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(".")]
};
}

Expand Down
24 changes: 24 additions & 0 deletions test/card/entity-list.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<BatteryStateCard>();
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]");
});
27 changes: 19 additions & 8 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ? <any>card.shadowRoot! : card;
}

Expand All @@ -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 {
Expand Down Expand Up @@ -138,7 +155,7 @@ export class HomeAssistantMock<T extends LovelaceCard<any>> {

public hass: HomeAssistantExt = <any>{
states: {},
localize: jest.fn((key: string) => `[${key}]`),
localize: jest.fn((...data: string[]) => `[${data.join(", ")}]`),
formatEntityState: jest.fn((entityData: any) => `${entityData.state} %`),
};

Expand Down Expand Up @@ -195,12 +212,6 @@ export class HomeAssistantMock<T extends LovelaceCard<any>> {
return entity;
},
setAttributes: (attribs: IEntityAttributes) => {

if (attribs === null) {
this.hass.states[entity.entity_id].attributes = <any>undefined;
return entity;
}

this.hass.states[entity.entity_id].attributes = {
...this.hass.states[entity.entity_id].attributes,
...attribs
Expand Down Expand Up @@ -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<K extends keyof HaEntityPropertyToTypeMap>(name: K, val: HaEntityPropertyToTypeMap[K]): void;
Expand Down
12 changes: 0 additions & 12 deletions test/other/entity-fields/battery-level.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = () => <string><unknown>null;
Expand Down
9 changes: 0 additions & 9 deletions test/other/entity-fields/charging-state.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
})

Expand Down
8 changes: 4 additions & 4 deletions test/other/entity-fields/get-icon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});

Expand All @@ -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");
});

Expand All @@ -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");
});

Expand Down
24 changes: 3 additions & 21 deletions test/other/entity-fields/get-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,28 +3,17 @@ 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 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" }, new HomeAssistantMock(true).hass, { 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" });
Expand All @@ -34,13 +23,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 });
Expand Down

0 comments on commit 4d50880

Please sign in to comment.