Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Entity not available warning #701

Merged
merged 2 commits into from
Feb 17, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading