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

Fixed min/max setting on groups #648

Merged
merged 2 commits into from
Jan 12, 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
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion src/custom-elements/battery-state-card.views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ export const collapsableWrapper = (model: IBatteryGroup, batteries: IBatteryColl
</div>
<div class="chevron">&lsaquo;</div>
</div>
<div style="max-height: ${Object.keys(batteries).length * 50}px">
<div style="max-height: ${Object.keys(batteries).length * 50}px" class="groupItems">
${model.batteryIds.map(id => batteryWrapper(batteries[id]))}
</div>
</div>
Expand Down
13 changes: 6 additions & 7 deletions src/grouping.ts
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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!;
});
Expand Down Expand Up @@ -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;
Expand Down
78 changes: 78 additions & 0 deletions test/card/grouping.test.ts
Original file line number Diff line number Diff line change
@@ -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<BatteryStateCard>();
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<BatteryStateCard>();
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);
});
});
67 changes: 62 additions & 5 deletions test/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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++) {
Expand All @@ -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<BatteryStateEntity>(".card-content > * > battery-state-entity")[index];
if (!entity) {
Expand All @@ -43,37 +56,81 @@ export class CardElements {

return new EntityElements(entity);
}

group(index: number) {
const group = this.card.shadowRoot!.querySelectorAll<BatteryStateEntity>(".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 ? <any>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() {
return this.secondaryInfo?.textContent?.trim();
}

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(<BatteryStateEntity>elem.querySelector(".toggler"), false);
}

private get batteryNodes(): NodeListOf<BatteryStateEntity> {
return this.elem.querySelectorAll<BatteryStateEntity>(".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<T extends LovelaceCard<any>> {

Expand Down
Loading