Skip to content

Commit

Permalink
Merge pull request #585 from maxwroc/BatchRenRegex
Browse files Browse the repository at this point in the history
Support regex on bulk_rename
  • Loading branch information
maxwroc authored Oct 24, 2023
2 parents f83aa09 + bb6c506 commit 9963573
Show file tree
Hide file tree
Showing 6 changed files with 177 additions and 118 deletions.
116 changes: 2 additions & 114 deletions src/battery-provider.ts
Original file line number Diff line number Diff line change
@@ -1,125 +1,13 @@
import { log, safeGetConfigArrayOfObjects } from "./utils";
import { getRegexFromString, log, safeGetConfigArrayOfObjects } from "./utils";
import { HomeAssistant } from "custom-card-helpers";
import { BatteryStateEntity } from "./custom-elements/battery-state-entity";
import { Filter } from "./filter";

/**
* Properties which should be copied over to individual entities from the card
*/
const entititesGlobalProps: (keyof IBatteryEntityConfig)[] = [ "tap_action", "state_map", "charging_state", "secondary_info", "colors", "bulk_rename", "icon", "round", "unit", "value_override", "non_battery_entity" ];

const regExpPattern = /\/([^/]+)\/([igmsuy]*)/;

/**
* Functions to check if filter condition is met
*/
const operatorHandlers: { [key in FilterOperator]: (val: string | number | undefined, expectedVal: string | number) => boolean } = {
"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),
">=": (val, expectedVal) => Number(val) >= Number(expectedVal),
"<=": (val, expectedVal) => Number(val) <= Number(expectedVal),
"matches": (val, pattern) => {
if (val === undefined) {
return false;
}

pattern = pattern.toString();

let exp: RegExp | undefined;
const regexpMatch = pattern.match(regExpPattern);
if (regexpMatch) {
// create regexp after removing slashes
exp = new RegExp(regexpMatch[1], regexpMatch[2]);
} else if (pattern.indexOf("*") != -1) {
exp = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
}

return exp ? exp.test(val.toString()) : val === pattern;
}
}

/**
* Filter class
*/
class Filter {

/**
* Whether filter is permanent.
*
* Permanent filters removes entities/batteries from collections permanently
* instead of making them hidden.
*/
get is_permanent(): boolean {
return this.config.name != "state";
}

constructor(private config: IFilter) {

}

/**
* Checks whether entity meets the filter conditions.
* @param entity Hass entity
* @param state State override - battery state/level
*/
isValid(entity: any, state?: string): boolean {
const val = this.getValue(entity, state);
return this.meetsExpectations(val);
}

/**
* Gets the value to validate.
* @param entity Hass entity
* @param state State override - battery state/level
*/
private getValue(entity: any, state?: string): string | undefined {
if (!this.config.name) {
log("Missing filter 'name' property");
return;
}

if (this.config.name.indexOf("attributes.") == 0) {
return entity.attributes[this.config.name.substr(11)];
}

if (this.config.name == "state" && state !== undefined) {
return state;
}

return (<any>entity)[this.config.name];
}

/**
* Checks whether value meets the filter conditions.
* @param val Value to validate
*/
private meetsExpectations(val: string | number | undefined): boolean {

let operator = this.config.operator;
if (!operator) {
if (this.config.value === undefined) {
operator = "exists";
}
else {
const expectedVal = this.config.value.toString();
operator = expectedVal.indexOf("*") != -1 || (expectedVal[0] == "/" && expectedVal[expectedVal.length - 1] == "/") ?
"matches" :
"=";
}
}

const func = operatorHandlers[operator];
if (!func) {
log(`Operator '${this.config.operator}' not supported. Supported operators: ${Object.keys(operatorHandlers).join(", ")}`);
return false;
}

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

/**
* Class responsible for intializing Battery view models based on given configuration.
*/
Expand Down
10 changes: 6 additions & 4 deletions src/entity-fields/get-name.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { HomeAssistant } from "custom-card-helpers";
import { safeGetArray } from "../utils";
import { getRegexFromString, safeGetArray } from "../utils";


/**
Expand All @@ -21,14 +21,16 @@ export const getName = (config: IBatteryEntityConfig, hass: HomeAssistant | unde

const renameRules = safeGetArray(config.bulk_rename)
renameRules.forEach(r => {
if (r.from[0] == "/" && r.from[r.from.length - 1] == "/") {
const regex = getRegexFromString(r.from);
if (regex) {
// create regexp after removing slashes
name = name.replace(new RegExp(r.from.substr(1, r.from.length - 2)), r.to || "");
name = name.replace(regex, r.to || "");
}
else {
name = name.replace(r.from, r.to || "");
}
});

return name;
}
}

109 changes: 109 additions & 0 deletions src/filter.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
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 } = {
"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),
">=": (val, expectedVal) => Number(val) >= Number(expectedVal),
"<=": (val, expectedVal) => Number(val) <= Number(expectedVal),
"matches": (val, pattern) => {
if (val === undefined) {
return false;
}

pattern = pattern.toString()

let exp = getRegexFromString(pattern);
if (!exp && pattern.includes("*")) {
exp = new RegExp("^" + pattern.replace(/\*/g, ".*") + "$");
}

return exp ? exp.test(val.toString()) : val === pattern;
}
}

/**
* Filter class
*/
export class Filter {

/**
* Whether filter is permanent.
*
* Permanent filters removes entities/batteries from collections permanently
* instead of making them hidden.
*/
get is_permanent(): boolean {
return this.config.name != "state";
}

constructor(private config: IFilter) {

}

/**
* Checks whether entity meets the filter conditions.
* @param entity Hass entity
* @param state State override - battery state/level
*/
isValid(entity: any, state?: string): boolean {
const val = this.getValue(entity, state);
return this.meetsExpectations(val);
}

/**
* Gets the value to validate.
* @param entity Hass entity
* @param state State override - battery state/level
*/
private getValue(entity: any, state?: string): string | undefined {
if (!this.config.name) {
log("Missing filter 'name' property");
return;
}

if (this.config.name.indexOf("attributes.") == 0) {
return entity.attributes[this.config.name.substr(11)];
}

if (this.config.name == "state" && state !== undefined) {
return state;
}

return (<any>entity)[this.config.name];
}

/**
* Checks whether value meets the filter conditions.
* @param val Value to validate
*/
private meetsExpectations(val: string | number | undefined): boolean {

let operator = this.config.operator;
if (!operator) {
if (this.config.value === undefined) {
operator = "exists";
}
else {
const expectedVal = this.config.value.toString();
const regex = getRegexFromString(expectedVal);
operator = expectedVal.indexOf("*") != -1 || regex ?
"matches" :
"=";
}
}

const func = operatorHandlers[operator];
if (!func) {
log(`Operator '${this.config.operator}' not supported. Supported operators: ${Object.keys(operatorHandlers).join(", ")}`);
return false;
}

return func(val, this.config.value);
}
}
21 changes: 21 additions & 0 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -102,3 +102,24 @@ export const throttledCall = function <T extends Function>(func: T, throttleMs:
timeoutHook = setTimeout(() => func.apply(null, args), 100);
})) as T
}


const regexPattern = /\/(.*?)\/([igm]{1,3})/
/**
* Extracts regex from the given string
* @param ruleVal Value to process
* @returns Parsed regex
*/
export const getRegexFromString = (ruleVal: string): RegExp | null => {
if (ruleVal[0] == "/" && ruleVal[ruleVal.length - 1] == "/") {
return new RegExp(ruleVal.substr(1, ruleVal.length - 2));
}
else {
let matches = ruleVal.match(regexPattern)
if (matches && matches.length == 3) {
return new RegExp(matches[1], matches[2]);
}
}

return null;
}
15 changes: 15 additions & 0 deletions test/other/entity-fields/get-name.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -54,4 +54,19 @@ describe("Get name", () => {

expect(name).toBe(expectedResult);
});

test.each(
[
["Kitchen Battery", { from: "/ Battery/", to: "" }, "Kitchen"],
["Kitchen Battery", { from: "/ battery/i", to: "" }, "Kitchen"],
["Kitchen battery temperature battery", [{ from: "/\\sbattery/ig", to: "" }], "Kitchen temperature"],
]
)("regex", (entityName: string, renameRules: IConvert | IConvert[], expectedResult: string) => {
const hassMock = new HomeAssistantMock(true);
hassMock.addEntity("My entity", "45", { friendly_name: entityName });

let name = getName({ entity: "my_entity", bulk_rename: renameRules }, hassMock.hass);

expect(name).toBe(expectedResult);
});
});
24 changes: 24 additions & 0 deletions test/other/filter.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { Filter } from "../../src/filter";
import { HomeAssistantMock } from "../helpers";

describe("Filter", () => {
test.each([
["Bedroom motion battery level", "*_battery_level", true],
["Bedroom motion battery level", "/_battery_level$/", true],
["Bedroom motion battery level", "*_battery_*", true],
["Bedroom motion battery level", "*_battery_", false],
["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) => {
const hassMock = new HomeAssistantMock();

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

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

expect(filter.is_permanent).toBeTruthy();
expect(isValid).toBe(expectedIsVlid);
})
});

0 comments on commit 9963573

Please sign in to comment.