Skip to content
Open
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
6 changes: 3 additions & 3 deletions src/battery-provider.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { log, safeGetConfigArrayOfObjects } from "./utils";
import { HomeAssistant } from "custom-card-helpers";
import { BatteryStateEntity } from "./custom-elements/battery-state-entity";
import { Filter } from "./filter";
import { createFilter, Filter } from "./filter";
import { EntityRegistryDisplayEntry, HomeAssistantExt } from "./type-extensions";

/**
Expand Down Expand Up @@ -60,8 +60,8 @@ export class BatteryProvider {
private initialized: boolean = false;

constructor(private config: IBatteryCardConfig) {
this.include = config.filter?.include?.map(f => new Filter(f));
this.exclude = config.filter?.exclude?.map(f => new Filter(f));
this.include = config.filter?.include?.map(createFilter);
this.exclude = config.filter?.exclude?.map(createFilter);

if (!this.include) {
this.initialized = false;
Expand Down
75 changes: 73 additions & 2 deletions src/filter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,66 @@ const operatorHandlers: { [key in FilterOperator]: (val: FilterValueType, expect
/**
* Filter class
*/
export class Filter {
export abstract class Filter {
/**
* Whether filter is permanent.
*
* Permanent filters removes entities/batteries from collections permanently
* instead of making them hidden.
*/
abstract get is_permanent(): boolean;

/**
* Checks whether entity meets the filter conditions.
* @param entityData Hass entity data
* @param state State override - battery state/level
*/
abstract isValid(entityData: any, state?: string): boolean;
}

export class NotFilter extends Filter {
constructor(private filter: Filter) {
super();
}

override get is_permanent(): boolean {
return this.filter.is_permanent;
}

override isValid(entityData: any, state?: string): boolean {
return !this.filter.isValid(entityData, state);
}
}

export class AndFilter extends Filter {
constructor(private filters: Filter[]) {
super();
}

override get is_permanent(): boolean {
return this.filters.every(filter => filter.is_permanent);
}

override isValid(entityData: any, state?: string): boolean {
return this.filters.every(filter => filter.isValid(entityData, state));
}
}

export class OrFilter extends Filter {
constructor(private filters: Filter[]) {
super();
}

override get is_permanent(): boolean {
return this.filters.every(filter => filter.is_permanent);
}

override isValid(entityData: any, state?: string): boolean {
return this.filters.some(filter => filter.isValid(entityData, state));
}
}

export class FieldFilter extends Filter {
/**
* Whether filter is permanent.
*
Expand All @@ -44,7 +102,7 @@ export class Filter {
}

constructor(private config: IFilter) {

super();
}

/**
Expand Down Expand Up @@ -106,4 +164,17 @@ export class Filter {

return func(val, this.config.value);
}
}

export function createFilter(config: FilterSpec): Filter {
if ("not" in config) {
return new NotFilter(createFilter(config.not));
}
if ("and" in config) {
return new AndFilter(config.and.map(createFilter));
}
if ("or" in config) {
return new OrFilter(config.or.map(createFilter));
}
return new FieldFilter(config);
}
4 changes: 3 additions & 1 deletion src/typings.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,8 @@ interface IFilter {
value?: FilterValueType;
}

type FilterSpec = IFilter | { not: FilterSpec } | { and: FilterSpec[] } | { or: FilterSpec[] }

interface IBatteryEntityConfig {

/**
Expand Down Expand Up @@ -295,7 +297,7 @@ interface IBatteryCardConfig {
/**
* Filters for auto adding or removing entities
*/
filter?: { [key in FilterGroups]: IFilter[] };
filter?: { [key in FilterGroups]: FilterSpec[] };
}

/**
Expand Down
74 changes: 65 additions & 9 deletions test/other/filter.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { Filter } from "../../src/filter";
import { createFilter } from "../../src/filter";
import { HomeAssistantMock } from "../helpers";

describe("Filter", () => {
Expand All @@ -8,7 +8,7 @@ describe("Filter", () => {

const entity = hassMock.addEntity("Entity name", "90", { battery_level: "45" });

const filter = new Filter({ name: "attributes.battery_level", operator: <any>"unsupported" });
const filter = createFilter({ name: "attributes.battery_level", operator: <any>"unsupported" });
const isValid = filter.isValid(entity);

expect(isValid).toBe(false);
Expand All @@ -22,7 +22,7 @@ describe("Filter", () => {

const entity = hassMock.addEntity("Entity name", "90", { battery_level: "45" });

const filter = new Filter({ name: <any>filterName });
const filter = createFilter({ name: <any>filterName });
const isValid = filter.isValid(entity);

expect(isValid).toBe(false);
Expand All @@ -36,7 +36,7 @@ describe("Filter", () => {

const entity = hassMock.addEntity("Entity name", "90");

const filter = new Filter({ name: "state", value: filterValue });
const filter = createFilter({ name: "state", value: filterValue });
const isValid = filter.isValid(entity, "45");

expect(isValid).toBe(expectedIsValid);
Expand All @@ -56,7 +56,7 @@ describe("Filter", () => {

const entity = hassMock.addEntity(entityName, "90");

const filter = new Filter({ name: "entity_id", value: filterValue });
const filter = createFilter({ name: "entity_id", value: filterValue });
const isValid = filter.isValid(entity);

expect(filter.is_permanent).toBeTruthy();
Expand All @@ -74,7 +74,7 @@ describe("Filter", () => {

const entity = hassMock.addEntity("Entity name", "90", attribs);

const filter = new Filter({ name: fileterName, operator });
const filter = createFilter({ name: fileterName, operator });
const isValid = filter.isValid(entity);

expect(filter.is_permanent).toBeTruthy();
Expand Down Expand Up @@ -113,7 +113,7 @@ describe("Filter", () => {

const entity = hassMock.addEntity("Entity name", "ok", { battery_level: state });

const filter = new Filter({ name: "attributes.battery_level", operator, value });
const filter = createFilter({ name: "attributes.battery_level", operator, value });
const isValid = filter.isValid(entity);

expect(isValid).toBe(expectedIsVlid);
Expand All @@ -134,7 +134,7 @@ describe("Filter", () => {

const entity = hassMock.addEntity("Entity name", "ok", { entity_attrib: attributeValue });

const filter = new Filter({ name: "attributes.entity_attrib", operator, value });
const filter = createFilter({ name: "attributes.entity_attrib", operator, value });
const isValid = filter.isValid(entity);

expect(isValid).toBe(expectedIsVlid);
Expand All @@ -149,9 +149,65 @@ describe("Filter", () => {
])("filter based on nested entity data", (entityData: any, filterName: string, filterValue: string, expectedIsValid: boolean) => {
const hassMock = new HomeAssistantMock();

const filter = new Filter({ name: filterName, value: filterValue });
const filter = createFilter({ name: filterName, value: filterValue });
const isValid = filter.isValid(entityData, "45");

expect(isValid).toBe(expectedIsValid);
})

test.each([
["45", <FilterOperator>"=", "45", false],
["45", <FilterOperator>"=", "55", true],
])("not negates the underlying filter", (state: string | undefined, operator: FilterOperator | undefined, value: string | number, expectedIsVlid: boolean) => {
const hassMock = new HomeAssistantMock();

const entity = hassMock.addEntity("Entity name", "ok", { battery_level: state });

const filter = createFilter({ not: { name: "attributes.battery_level", operator, value } });
const isValid = filter.isValid(entity);

expect(isValid).toBe(expectedIsVlid);
})

test.each([
["Charging", "45", true],
["Charging", "55", false],
["45", "45", false],
["55", "55", false],
])("combining filters using and", (state: string, battery_level: string, expectedIsValid: boolean ) => {
const hassMock = new HomeAssistantMock();

const entity = hassMock.addEntity("Entity name", state, { battery_level });

const filter = createFilter({
and: [
{ name: "attributes.battery_level", operator: "<", value: "50" },
{ name: "state", operator: "=", value: "Charging" },
]
});
const isValid = filter.isValid(entity);

expect(isValid).toBe(expectedIsValid);
})

test.each([
["Charging", "45", true],
["Charging", "55", true],
["45", "45", true],
["55", "55", false],
])("combining filters using or", (state: string, battery_level: string, expectedIsValid: boolean ) => {
const hassMock = new HomeAssistantMock();

const entity = hassMock.addEntity("Entity name", state, { battery_level });

const filter = createFilter({
or: [
{ name: "attributes.battery_level", operator: "<", value: "50" },
{ name: "state", operator: "=", value: "Charging" },
]
});
const isValid = filter.isValid(entity);

expect(isValid).toBe(expectedIsValid);
})
});
Loading