diff --git a/README.md b/README.md index faf9c376..cceca401 100644 --- a/README.md +++ b/README.md @@ -57,6 +57,7 @@ This card was inspired by [another great card](https://github.com/cbulock/lovela | icon | string | | v1.6.0 | Icon override (if you want to set a static custom one). You can provide entity attribute name which contains icon class (e.g. `attributes.battery_icon` - it has to be prefixed with "attributes.") | attribute | string | | v0.9.0 | Name of attribute (override) to extract the value from. By default we look for values in the following attributes: `battery_level`, `battery`. If they are not present we take entity state. | multiplier | number | `1` | v0.9.0 | If the value is not in 0-100 range we can adjust it by specifying multiplier. E.g. if the values are in 0-10 range you can make them working by putting `10` as multiplier. +| value_override | [KString](#keyword-string-kstring) | | v3.0.0 | Allows to override the battery level value. Note: when used the `multiplier`, `round`, `state_map` setting is ignored +[common options](#common-options) (if specified they will override the card-level ones) @@ -90,8 +91,13 @@ Keywords support simple functions to convert the values |:-----|:-----|:-----| | round(\[number\]) | `"{state\|round(2)}"` | Rounds the value to number of fractional digits. E.g. if state is 20.617 the output will be 20.62. | replace(\[old_string\]=\[new_string\]) | `"{attributes.friendly_name\|replace(Battery level=)}"` | Simple replace. E.g. if name contains "Battery level" string then it will be removed +| multiply(\[number\]) | `"{state\|multiply(10)}"` | Multiplies the value by given number +| greaterthan(\[threshold_number\],\[result_value\]) | `"{state\|greaterthan(10,100)}"` | Changes the value to a given one when the threshold is met. In the given example the value will be replaced to 100 when the current value is greater than 10 +| lessthan(\[threshold_number\],\[result_value\]) | `"{state\|lessthan(10,0)}"` | Changes the value to a given one when the threshold is met. In the given example the value will be replaced to 0 when the current value is less than 10 +| between(\[lower_threshold_number\],[upper_threshold_number\],\[result_value\]) | `"{state\|between(2,6,30)}"` | Changes the value to a given one when the value is between two given numbers. In the given example the value will be replaced to 30 when the current value is between 2 and 6 +| thresholds(\[number1\],\[number2\],...) | `"{state\|thresholds(22,89,200,450)}"` | Converts the value to percentage based on given thresholds. In the given example values will be converted in the following way 20=>0, 30=>25, 99=>50, 250=>75, 555=>100 -You can execute functions one after another. For example if you have the value "Battery level: 26.543234%" and you want to extract and round the number then you can do the following: `"{attribute.battery_level|replace(Battery level:=)|replace(%=)|round()} %"` and the end result will be "27 %" +You can execute functions one after another. For example if you have the value "Battery level: 26.543234%" and you want to extract and round the number then you can do the following: `"{attribute.battery_level|replace(Battery level:=)|replace(%=)|round()} %"` and the end result will be "27" ### Sort object diff --git a/src/rich-string-processor.ts b/src/rich-string-processor.ts index 4cdee94d..f23aefcc 100644 --- a/src/rich-string-processor.ts +++ b/src/rich-string-processor.ts @@ -110,6 +110,65 @@ const availableProcessors: IMap = { } return val => parseFloat(val).toFixed(decimalPlaces); + }, + "multiply": (params) => { + if (params === "") { + log("[KString]multiply function is missing parameter"); + return val => val; + } + + const multiplier = Number(params); + + return val => isNaN(multiplier) ? val : (Number(val) * multiplier).toString(); + }, + "greaterthan": (params) => { + const chunks = params.split(","); + if (chunks.length != 2) { + log("[KString]greaterthan function requires two parameters"); + return val => val; + } + + const compareTo = Number(chunks[0]); + return val => Number(val) > compareTo ? chunks[1] : val; + }, + "lessthan": (params) => { + const chunks = params.split(","); + if (chunks.length != 2) { + log("[KString]lessthan function requires two parameters"); + return val => val; + } + + const compareTo = Number(chunks[0]); + return val => Number(val) < compareTo ? chunks[1] : val; + }, + "between": (params) => { + const chunks = params.split(","); + if (chunks.length != 3) { + log("[KString]between function requires three parameters"); + return val => val; + } + + const compareLower = Number(chunks[0]); + const compareGreater = Number(chunks[1]); + return val => { + const numericVal = Number(val); + return compareLower < numericVal && compareGreater > numericVal ? chunks[2] : val; + } + }, + "thresholds": (params) => { + const thresholds = params.split(",").map(v => Number(v)); + + return val => { + const numericVal = Number(val); + const result = thresholds.findIndex(v => numericVal < v); + + if (result == -1) { + // looks like the value is higher than the last threshold + return "100"; + } + + return Math.round(100 / thresholds.length * result).toString(); + } } } diff --git a/test/other/rich-string-processor.test.ts b/test/other/rich-string-processor.test.ts index f4628359..9cbe8837 100644 --- a/test/other/rich-string-processor.test.ts +++ b/test/other/rich-string-processor.test.ts @@ -60,4 +60,56 @@ describe("RichStringProcessor", () => { const result = proc.process("{is_charging}"); expect(result).toBe("Charging"); }); + + test.each([ + ["Value {state|multiply(2)}", "20.56", "Value 41.12"], + ["Value {state|multiply(0.5)}", "20.56", "Value 10.28"], + ["Value {state|multiply()}", "20.56", "Value 20.56"], // param missing + ])("multiply function", (text: string, state:string, expectedResult: string) => { + const hassMock = new HomeAssistantMock(true); + const motionEntity = hassMock.addEntity("Bedroom motion", state, {}, "sensor"); + const proc = new RichStringProcessor(hassMock.hass, motionEntity.entity_id); + + const result = proc.process(text); + expect(result).toBe(expectedResult); + }); + + test.each([ + ["{state|lessthan(2,0)|greaterthan(7,100)|between(1,8,50)}", "1", "0"], + ["{state|lessthan(2,0)|greaterthan(7,100)|between(1,8,50)}", "2", "50"], + ["{state|lessthan(2,0)|greaterthan(7,100)|between(1,8,50)}", "5", "50"], + ["{state|lessthan(2,0)|greaterthan(7,100)|between(1,8,50)}", "7", "50"], + ["{state|lessthan(2,0)|greaterthan(7,100)|between(1,8,50)}", "8", "100"], + ["{state|lessthan(2,0)|greaterthan(7,100)|between(1,8,50)}", "70", "100"], + // missing params + ["{state|lessthan()|greaterthan(7,100)|between(1,8,50)}", "1", "1"], + ["{state|lessthan(2,0)|greaterthan(7,100)|between()}", "5", "5"], + ["{state|lessthan(2,0)|greaterthan()|between(1,8,50)}", "70", "70"], + ])("greater, lessthan, between functions", (text: string, state:string, expectedResult: string) => { + const hassMock = new HomeAssistantMock(true); + const motionEntity = hassMock.addEntity("Bedroom motion", state, {}, "sensor"); + const proc = new RichStringProcessor(hassMock.hass, motionEntity.entity_id); + + const result = proc.process(text); + expect(result).toBe(expectedResult); + }); + + test.each([ + ["{state|thresholds(22,88,200,450)}", "1", "0"], + ["{state|thresholds(22,88,200,450)}", "22", "25"], + ["{state|thresholds(22,88,200,450)}", "60", "25"], + ["{state|thresholds(22,88,200,450)}", "90", "50"], + ["{state|thresholds(22,88,200,450)}", "205", "75"], + ["{state|thresholds(22,88,200,450)}", "449", "75"], + ["{state|thresholds(22,88,200,450)}", "500", "100"], + ["{state|thresholds(22,88,200)}", "90", "67"], + ["{state|thresholds(22,88,200)}", "200", "100"], + ])("threshold function", (text: string, state:string, expectedResult: string) => { + const hassMock = new HomeAssistantMock(true); + const motionEntity = hassMock.addEntity("Bedroom motion", state, {}, "sensor"); + const proc = new RichStringProcessor(hassMock.hass, motionEntity.entity_id); + + const result = proc.process(text); + expect(result).toBe(expectedResult); + }); }) \ No newline at end of file