From 21723de3ce1e556299e139147c444450a509cabf Mon Sep 17 00:00:00 2001 From: Max Chodorowski Date: Fri, 29 Dec 2023 17:02:45 +0000 Subject: [PATCH] Added not_exists filter aperator --- README.md | 15 ++++--- src/filter.ts | 7 +-- src/typings.d.ts | 4 +- test/other/filter.test.ts | 89 ++++++++++++++++++++++++++++++++++++++- 4 files changed, 102 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index a862f811..b518d51d 100644 --- a/README.md +++ b/README.md @@ -200,14 +200,15 @@ Operator is an optional property. If operator is not specified it depends on `va * if `value` starts and ends with shalsh "`/`" or if it contains wildcard "`*`" the operator is `matches` * if `value` property is set but above conditions are not met the operator is "`=`" -| Name | Type | +| Name | Since | Type | |:-----|:-----| -| `"exists"` | It just checks if value is present (e.g. to match entities having particular attribute regardless of the attribute value). It doesn't require `value` to be specified. -| `"="` | If value equals the one specified in `value` property. -| `">"` | If value is greater than one specified in `value` property. Possible variant: `">="`. Value must be numeric type. -| `"<"` | If value is lower than one specified in `value` property. Possible variant: `"<="`. Value must be numeric type. -| `"contains"` | If value contains the one specified in `value` property -| `"matches"` | If value matches the one specified in `value` property. You can use wildcards (e.g. `"*_battery_level"`) or regular expression (must be prefixed and followed by slash e.g. `"/[a-z_]+_battery_level/"`) +| `"exists"` | v1.3.0 | It checks if field is present (e.g. to match entities having particular attribute regardless of the attribute value). It doesn't require `value` to be specified. +| `"not_exists"` | v3.1.0 | It checks if field is not present (e.g. to match entities without particular attribute). It doesn't require `value` to be specified. +| `"="` | v1.3.0 | If value equals the one specified in `value` property. +| `">"` | v1.3.0 | If value is greater than one specified in `value` property. Possible variant: `">="`. Value must be numeric type. +| `"<"` | v1.3.0 | If value is lower than one specified in `value` property. Possible variant: `"<="`. Value must be numeric type. +| `"contains"` | v1.3.0 | If value contains the one specified in `value` property +| `"matches"` | v1.3.0 | If value matches the one specified in `value` property. You can use wildcards (e.g. `"*_battery_level"`) or regular expression (must be prefixed and followed by slash e.g. `"/[a-z_]+_battery_level/"`) ### Tap-Action diff --git a/src/filter.ts b/src/filter.ts index a21bcd48..4c582a5c 100644 --- a/src/filter.ts +++ b/src/filter.ts @@ -3,9 +3,10 @@ import { getRegexFromString, log } from "./utils"; /** * Functions to check if filter condition is met */ -const operatorHandlers: { [key in FilterOperator]: (val: string | number | undefined, expectedVal: string | number) => boolean } = { +const operatorHandlers: { [key in FilterOperator]: (val: string | number | undefined, expectedVal: string | number | undefined) => boolean } = { "exists": val => val !== undefined, - "contains": (val, searchString) => val !== undefined && val.toString().indexOf(searchString.toString()) != -1, + "not_exists": val => val === undefined, + "contains": (val, searchString) => val !== undefined && val.toString().indexOf(searchString!.toString()) != -1, "=": (val, expectedVal) => val == expectedVal, ">": (val, expectedVal) => Number(val) > Number(expectedVal), "<": (val, expectedVal) => Number(val) < Number(expectedVal), @@ -16,7 +17,7 @@ const operatorHandlers: { [key in FilterOperator]: (val: string | number | undef return false; } - pattern = pattern.toString() + pattern = pattern!.toString() let exp = getRegexFromString(pattern); if (!exp && pattern.includes("*")) { diff --git a/src/typings.d.ts b/src/typings.d.ts index 96c2f423..145c52c1 100644 --- a/src/typings.d.ts +++ b/src/typings.d.ts @@ -151,7 +151,7 @@ type FilterGroups = "exclude" | "include"; /** * Supprted filter operators */ -type FilterOperator = "exists" | "=" | ">" | "<" | ">=" | "<=" | "contains" | "matches"; +type FilterOperator = "exists" | "not_exists" | "=" | ">" | "<" | ">=" | "<=" | "contains" | "matches"; /** * Filter object @@ -170,7 +170,7 @@ interface IFilter { /** * Value to compare with the extracted one */ - value: string | number; + value?: string | number; } interface IBatteryEntityConfig { diff --git a/test/other/filter.test.ts b/test/other/filter.test.ts index 32681839..99ec0f2d 100644 --- a/test/other/filter.test.ts +++ b/test/other/filter.test.ts @@ -2,6 +2,46 @@ import { Filter } from "../../src/filter"; import { HomeAssistantMock } from "../helpers"; describe("Filter", () => { + + test("unsupported operator", () => { + const hassMock = new HomeAssistantMock(); + + const entity = hassMock.addEntity("Entity name", "90", { battery_level: "45" }); + + const filter = new Filter({ name: "attributes.battery_level", operator: "unsupported" }); + const isValid = filter.isValid(entity); + + expect(isValid).toBe(false); + }) + + test.each([ + [""], + [undefined], + ])("filter name missing", (filterName: string | undefined) => { + const hassMock = new HomeAssistantMock(); + + const entity = hassMock.addEntity("Entity name", "90", { battery_level: "45" }); + + const filter = new Filter({ name: filterName }); + const isValid = filter.isValid(entity); + + expect(isValid).toBe(false); + }) + + test.each([ + ["45", true], + ["90", false], + ])("filter based on state - state coming from custom source", (filterValue: string, expectedIsValid: boolean) => { + const hassMock = new HomeAssistantMock(); + + const entity = hassMock.addEntity("Entity name", "90"); + + const filter = new Filter({ name: "state", value: filterValue }); + const isValid = filter.isValid(entity, "45"); + + expect(isValid).toBe(expectedIsValid); + }) + test.each([ ["Bedroom motion battery level", "*_battery_level", true], ["Bedroom motion battery level", "/_battery_level$/", true], @@ -10,7 +50,7 @@ describe("Filter", () => { ["Bedroom motion", "*_battery_level", false], ["Bedroom motion", "/BEDroom_motion/", false], ["Bedroom motion", "/BEDroom_motion/i", true], - ])("returns correct validity status (matches func)", (entityName: string, filterValue: string, expectedIsVlid: boolean) => { + ])("matches func returns correct results", (entityName: string, filterValue: string, expectedIsVlid: boolean) => { const hassMock = new HomeAssistantMock(); const entity = hassMock.addEntity(entityName, "90"); @@ -21,4 +61,51 @@ describe("Filter", () => { expect(filter.is_permanent).toBeTruthy(); expect(isValid).toBe(expectedIsVlid); }) + + test.each([ + ["attributes.battery_level", { battery_level: "45" }, true, "exists"], + ["attributes.battery_level", { battery_level: "45" }, true, undefined], + ["attributes.battery_state", { battery_level: "45" }, false, "exists"], + ["attributes.battery_level", { battery_level: "45" }, false, "not_exists"], + ["attributes.battery_state", { battery_level: "45" }, true, "not_exists"], + ])("exists/not_exists func returns correct results", (fileterName: string, attribs: IMap, expectedIsVlid: boolean, operator: FilterOperator | undefined) => { + const hassMock = new HomeAssistantMock(); + + const entity = hassMock.addEntity("Entity name", "90", attribs); + + const filter = new Filter({ name: fileterName, operator }); + const isValid = filter.isValid(entity); + + expect(filter.is_permanent).toBeTruthy(); + expect(isValid).toBe(expectedIsVlid); + }) + + test.each([ + ["45", "matches", "45", true], + ["45", "matches", "55", false], + [undefined, "matches", "55", false], + ["45", "=", "45", true], + ["45", "=", "55", false], + ["45", ">", "44", true], + ["45", ">", "45", false], + ["45", ">=", "45", true], + ["45", ">=", "44", true], + ["45", ">=", "46", false], + ["45", "<", "45", false], + ["45", "<", "46", true], + ["45", "<=", "45", true], + ["45", "<=", "44", false], + ["45", "<=", "46", true], + ["some longer text", "contains", "longer", true], + ["some longer text", "contains", "loonger", false], + ])("matching functions return correct results", (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 = new Filter({ name: "attributes.battery_level", operator, value }); + const isValid = filter.isValid(entity); + + expect(isValid).toBe(expectedIsVlid); + }) }); \ No newline at end of file