diff --git a/package.json b/package.json index 3c9fdbd..c6251b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "battery-state-card", - "version": "3.1.3", + "version": "3.1.4", "description": "Battery State card for Home Assistant", "main": "dist/battery-state-card.js", "author": "Max Chodorowski", diff --git a/src/custom-elements/battery-state-card.views.ts b/src/custom-elements/battery-state-card.views.ts index cfaa33d..b9cb838 100644 --- a/src/custom-elements/battery-state-card.views.ts +++ b/src/custom-elements/battery-state-card.views.ts @@ -26,7 +26,7 @@ export const collapsableWrapper = (model: IBatteryGroup, batteries: IBatteryColl
-
+
${model.batteryIds.map(id => batteryWrapper(batteries[id]))}
diff --git a/src/grouping.ts b/src/grouping.ts index 174c303..91b53c4 100644 --- a/src/grouping.ts +++ b/src/grouping.ts @@ -1,6 +1,5 @@ -import { log } from "./utils"; +import { log, toNumber } from "./utils"; import { IBatteryCollection, IBatteryCollectionItem } from "./battery-provider"; -import { BatteryStateEntity } from "./custom-elements/battery-state-entity"; export interface IBatteryGroup { title?: string; @@ -87,7 +86,7 @@ const getGroupIndex = (config: IGroupConfig[], battery: IBatteryCollectionItem, return false } - const level = isNaN(Number(battery.state)) ? 0 : Number(battery.state); + const level = isNaN(toNumber(battery.state)) ? 0 : toNumber(battery.state); return level >= group.min! && level <= group.max!; }); @@ -152,14 +151,14 @@ const getEnrichedText = (text: string, group: IBatteryGroup, batteries: IBattery text = text.replace(/\{[a-z]+\}/g, keyword => { switch (keyword) { case "{min}": - return group.batteryIds.reduce((agg, id) => agg > Number(batteries[id].state) ? Number(batteries[id].state) : agg, 100).toString(); + return group.batteryIds.reduce((agg, id) => agg > toNumber(batteries[id].state) ? toNumber(batteries[id].state) : agg, 100).toString(); case "{max}": - return group.batteryIds.reduce((agg, id) => agg < Number(batteries[id].state) ? Number(batteries[id].state) : agg, 0).toString(); + return group.batteryIds.reduce((agg, id) => agg < toNumber(batteries[id].state) ? toNumber(batteries[id].state) : agg, 0).toString(); case "{count}": return group.batteryIds.length.toString(); case "{range}": - const min = group.batteryIds.reduce((agg, id) => agg > Number(batteries[id].state) ? Number(batteries[id].state) : agg, 100).toString(); - const max = group.batteryIds.reduce((agg, id) => agg < Number(batteries[id].state) ? Number(batteries[id].state) : agg, 0).toString(); + const min = group.batteryIds.reduce((agg, id) => agg > toNumber(batteries[id].state) ? toNumber(batteries[id].state) : agg, 100).toString(); + const max = group.batteryIds.reduce((agg, id) => agg < toNumber(batteries[id].state) ? toNumber(batteries[id].state) : agg, 0).toString(); return min == max ? min : min + "-" + max; default: return keyword; diff --git a/test/card/grouping.test.ts b/test/card/grouping.test.ts new file mode 100644 index 0000000..d19bd8a --- /dev/null +++ b/test/card/grouping.test.ts @@ -0,0 +1,78 @@ +import { BatteryStateCard } from "../../src/custom-elements/battery-state-card"; +import { CardElements, HomeAssistantMock } from "../helpers"; + +describe("Grouping", () => { + test.each([ + [["10", "24", "25", "26", "50"], 25, "10 %|24 %", "25 %|26 %|50 %"], + [["10.1", "24.2", "25.3", "26.4", "50.5"], 25, "10,1 %|24,2 %", "25,3 %|26,4 %|50,5 %", ","], + [["10.1", "24.2", "25.3", "26.4", "50.5"], 25, "10.1 %|24.2 %", "25.3 %|26.4 %|50.5 %", "."], + ])("works with 'min' setting", async (entityStates: string[], min: number, ungrouped: string, inGroup: string, decimalPoint = ".") => { + + const hass = new HomeAssistantMock(); + const entities = entityStates.map((state, i) => { + const batt = hass.addEntity(`Batt ${i + 1}`, state); + return batt.entity_id; + }); + const groupEntity = hass.addEntity("My group", "30", { entity_id: entities }, "group"); + + hass.mockFunc("formatEntityState", (entityData: any) => `${entityData.state.replace(".", decimalPoint)} %`); + + const cardElem = hass.addCard("battery-state-card", { + title: "Header", + entities: [], + //sort: "state", + collapse: [ + { + group_id: groupEntity.entity_id, + min + } + ] + }); + + // waiting for card to be updated/rendered + await cardElem.cardUpdated; + + const card = new CardElements(cardElem); + + const ungroupedStates = card.items.map(e => e.stateText).join("|"); + expect(ungroupedStates).toBe(ungrouped); + + expect(card.groupsCount).toBe(1); + + const groupStates = card.group(0).items.map(e => e.stateText).join("|"); + expect(groupStates).toBe(inGroup); + }); + + test.each([ + [["10", "24", "25", "26", "50"], "Min {min}, Max {max}, Range {range}, Count {count}", "Min 25, Max 50, Range 25-50, Count 3"], + ])("secondary info keywords", async (entityStates: string[], secondaryInfo: string, expectedSecondaryInfo: string) => { + + const hass = new HomeAssistantMock(); + const entities = entityStates.map((state, i) => { + const batt = hass.addEntity(`Batt ${i + 1}`, state); + return batt.entity_id; + }); + const groupEntity = hass.addEntity("My group", "30", { entity_id: entities }, "group"); + + const cardElem = hass.addCard("battery-state-card", { + title: "Header", + entities: [], + //sort: "state", + collapse: [ + { + group_id: groupEntity.entity_id, + min: 25, + secondary_info: secondaryInfo + } + ] + }); + + // waiting for card to be updated/rendered + await cardElem.cardUpdated; + + const card = new CardElements(cardElem); + + expect(card.groupsCount).toBe(1); + expect(card.group(0).secondaryInfoText).toBe(expectedSecondaryInfo); + }); +}); \ No newline at end of file diff --git a/test/helpers.ts b/test/helpers.ts index 593f95f..43c09d9 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -26,6 +26,10 @@ export class CardElements { return this.card.shadowRoot!.querySelectorAll(".card-content > * > battery-state-entity").length; } + get groupsCount() { + return this.card.shadowRoot!.querySelectorAll(".card-content > .expandWrapper").length; + } + get items(): EntityElements[] { const result: EntityElements[] = []; for (let index = 0; index < this.itemsCount; index++) { @@ -35,6 +39,15 @@ export class CardElements { return result; } + get groups(): GroupElement[] { + const result: GroupElement[] = []; + for (let index = 0; index < this.groupsCount; index++) { + result.push(this.group(index)); + } + + return result; + } + item(index: number) { const entity = this.card.shadowRoot!.querySelectorAll(".card-content > * > battery-state-entity")[index]; if (!entity) { @@ -43,23 +56,35 @@ export class CardElements { return new EntityElements(entity); } + + group(index: number) { + const group = this.card.shadowRoot!.querySelectorAll(".card-content > .expandWrapper")[index]; + if (!group) { + throw new Error("Group element not found: " + index); + } + + return new GroupElement(group); + } } export class EntityElements { - constructor(private card: BatteryStateEntity) { + private root: HTMLElement; + + constructor(private card: BatteryStateEntity, isShadowRoot: boolean = true) { + this.root = isShadowRoot ? card.shadowRoot! : card; } get iconName() { - return this.card.shadowRoot?.querySelector("ha-icon")?.getAttribute("icon") + return this.root.querySelector("ha-icon")?.getAttribute("icon"); } get nameText() { - return this.card.shadowRoot?.querySelector(".name")?.textContent?.trim(); + return this.root.querySelector(".name")?.textContent?.trim(); } get secondaryInfo() { - return this.card.shadowRoot?.querySelector(".secondary"); + return this.root.querySelector(".secondary"); } get secondaryInfoText() { @@ -67,13 +92,45 @@ export class EntityElements { } get stateText() { - return this.card.shadowRoot?.querySelector(".state") + return this.root.querySelector(".state") ?.textContent ?.trim() .replace(String.fromCharCode(160), " "); // replace non breakable space } } +export class GroupElement extends EntityElements { + constructor(private elem: HTMLElement) { + super(elem.querySelector(".toggler"), false); + } + + private get batteryNodes(): NodeListOf { + return this.elem.querySelectorAll(".groupItems > * > battery-state-entity"); + } + + get itemsCount() { + return this.batteryNodes.length; + } + + get items(): EntityElements[] { + const result: EntityElements[] = []; + for (let index = 0; index < this.itemsCount; index++) { + result.push(this.item(index)); + } + + return result; + } + + item(index: number): EntityElements { + const entity = this.batteryNodes[index]; + if (!entity) { + throw new Error("Card element not found: " + index); + } + + return new EntityElements(entity); + } +} + export class HomeAssistantMock> {