diff --git a/src/filter.ts b/src/filter.ts index 95a0498..66504be 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -3,17 +3,17 @@ import { getRegexFromString, getValueFromObject, isNumber, log, toNumber } from /** * Functions to check if filter condition is met */ -const operatorHandlers: { [key in FilterOperator]: (val: string | number | undefined, expectedVal: string | number | undefined) => boolean } = { +const operatorHandlers: { [key in FilterOperator]: (val: FilterValueType, expectedVal: FilterValueType) => boolean } = { "exists": val => val !== undefined, "not_exists": val => val === undefined, - "contains": (val, searchString) => val !== undefined && val.toString().indexOf(searchString!.toString()) != -1, + "contains": (val, searchString) => val !== undefined && val !== null && val.toString().indexOf(searchString!.toString()) != -1, "=": (val, expectedVal) => isNumber(val) || isNumber(expectedVal) ? toNumber(val) == toNumber(expectedVal) : val == expectedVal, ">": (val, expectedVal) => toNumber(val) > toNumber(expectedVal), "<": (val, expectedVal) => toNumber(val) < toNumber(expectedVal), ">=": (val, expectedVal) => toNumber(val) >= toNumber(expectedVal), "<=": (val, expectedVal) => toNumber(val) <= toNumber(expectedVal), "matches": (val, pattern) => { - if (val === undefined) { + if (val === undefined || val === null) { return false; } @@ -62,7 +62,7 @@ export class Filter { * @param entityData Hass entity data * @param state State override - battery state/level */ - private getValue(entityData: any, state?: string): string | undefined { + private getValue(entityData: any, state?: string): FilterValueType { if (!this.config.name) { log("Missing filter 'name' property"); return; @@ -79,13 +79,16 @@ export class Filter { * Checks whether value meets the filter conditions. * @param val Value to validate */ - private meetsExpectations(val: string | number | undefined): boolean { + private meetsExpectations(val: FilterValueType): boolean { let operator = this.config.operator; if (!operator) { if (this.config.value === undefined) { operator = "exists"; } + else if (this.config.value === null) { + operator = "="; + } else { const expectedVal = this.config.value.toString(); const regex = getRegexFromString(expectedVal); diff --git a/src/typings.d.ts b/src/typings.d.ts index c88aac5..cc204de 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -153,6 +153,11 @@ type FilterGroups = "exclude" | "include"; */ type FilterOperator = "exists" | "not_exists" | "=" | ">" | "<" | ">=" | "<=" | "contains" | "matches"; +/** + * Allowed filter value types + */ +type FilterValueType = string | number | boolean | null | undefined; + /** * Filter object */ @@ -170,7 +175,7 @@ interface IFilter { /** * Value to compare with the extracted one */ - value?: string | number; + value?: FilterValueType; } interface IBatteryEntityConfig { diff --git a/src/utils.ts b/src/utils.ts index 644cf71..9494e99 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -17,8 +17,8 @@ export const log = (message: string, level: "warn" | "error" = "warn") => { * Checks whether given value is a number * @param val String value to check */ -export const isNumber = (value: string | number | undefined): boolean => { - if (value === undefined) { +export const isNumber = (value: string | number | boolean | null | undefined): boolean => { + if (value === undefined || value === null || typeof value === "boolean") { return false; } @@ -27,7 +27,7 @@ export const isNumber = (value: string | number | undefined): boolean => { value = value.replace(",", "."); } - return (value!== undefined && value !== null && value !== '' && !isNaN(Number(value))) + return value !== '' && !isNaN(Number(value)); } /** @@ -35,7 +35,7 @@ export const isNumber = (value: string | number | undefined): boolean => { * @param value String value to convert * @returns Result number */ -export const toNumber = (value: string | number | undefined): number => { +export const toNumber = (value: string | number | boolean | null | undefined): number => { if (typeof(value) == "string") { // trying to solve decimal number formatting in some langs value = value.replace(",", "."); @@ -153,7 +153,7 @@ export const getRegexFromString = (ruleVal: string): RegExp | null => { * @param path Path to the value * @returns Value from the path */ -export const getValueFromObject = (dataObject: any, path: string): string | undefined => { +export const getValueFromObject = (dataObject: any, path: string): string | number | boolean | null | undefined => { const chunks = path.split("."); for (let i = 0; i < chunks.length; i++) { @@ -167,5 +167,5 @@ export const getValueFromObject = (dataObject: any, path: string): string | unde dataObject = JSON.stringify(dataObject); } - return dataObject === undefined ? undefined : dataObject.toString(); + return dataObject; } \ No newline at end of file diff --git a/test/other/filter.test.ts b/test/other/filter.test.ts index d45c8b6..ba111fe 100644 --- a/test/other/filter.test.ts +++ b/test/other/filter.test.ts @@ -118,6 +118,25 @@ describe("Filter", () => { expect(isValid).toBe(expectedIsVlid); }) + test.each([ + [44, "<", "44,1", true], + [44, ">", "44.1", false], + [true, "=", "false", false], + [true, "=", "true", false], + [true, "=", true, true], + [true, undefined, true, true], + [false, undefined, true, false], + [true, undefined, false, false], + ])("non mixed types of values", (attributeValue: FilterValueType, operator: FilterOperator | undefined, value: FilterValueType, expectedIsVlid: boolean) => { + const hassMock = new HomeAssistantMock(); + + const entity = hassMock.addEntity("Entity name", "ok", { entity_attrib: attributeValue }); + + const filter = new Filter({ name: "attributes.entity_attrib", operator, value }); + const isValid = filter.isValid(entity); + + expect(isValid).toBe(expectedIsVlid); + }) test.each([ [{ state: "45", device: { name: "Device name" } }, "path.missing", "Device name", false],