Skip to content

Commit

Permalink
Merge pull request #614 from maxwroc/RelTimeKString
Browse files Browse the repository at this point in the history
KString convert to relative time func
  • Loading branch information
maxwroc authored Dec 21, 2023
2 parents ed04eee + 863eb71 commit 31d8c64
Show file tree
Hide file tree
Showing 10 changed files with 129 additions and 20 deletions.
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,7 @@ Keywords support simple functions to convert the values
| 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
| abs() | `"{state\|abs()}"` | Produces the absolute value
| equals(\[value\],\[result_value\]) | `"{state\|equals(on,1)}"` | Changes the value conditionally - whenever the initial value is equal the given one
| reltime() | `"Changed: {last_changed\|reltime()}"` | Converts date to relative time e.g. "1 minute ago"

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"

Expand Down
4 changes: 2 additions & 2 deletions src/custom-elements/battery-state-card.views.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { IBatteryCollection } from "../battery-provider";
import { IBatteryGroup } from "../grouping";
import { BatteryStateCard } from "./battery-state-card";
import { BatteryStateEntity } from "./battery-state-entity";
import { icon, secondaryInfo } from "./battery-state-entity.views";
import { icon } from "./battery-state-entity.views";

const header = (text: string | undefined) => text && html`
<div class="card-header">
Expand All @@ -22,7 +22,7 @@ export const collapsableWrapper = (model: IBatteryGroup, batteries: IBatteryColl
${icon(model.icon, model.iconColor)}
<div class="name truncate">
${model.title}
${secondaryInfo(model.secondaryInfo)}
${model.secondaryInfo ? html`<div class="secondary">${model.secondaryInfo}</div>` : null}
</div>
<div class="chevron">&lsaquo;</div>
</div>
Expand Down
4 changes: 2 additions & 2 deletions src/custom-elements/battery-state-entity.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { css } from "lit";
import { property } from "lit/decorators.js"
import { isNumber, safeGetConfigObject } from "../utils";
import { safeGetConfigObject } from "../utils";
import { batteryHtml } from "./battery-state-entity.views";
import { LovelaceCard } from "./lovelace-card";
import sharedStyles from "./shared.css"
Expand Down Expand Up @@ -28,7 +28,7 @@ export class BatteryStateEntity extends LovelaceCard<IBatteryEntityConfig> {
* Secondary information displayed undreneath the name
*/
@property({ attribute: false })
public secondaryInfo: string | Date;
public secondaryInfo: string;

/**
* Entity state / battery level
Expand Down
46 changes: 37 additions & 9 deletions src/custom-elements/battery-state-entity.views.ts
Original file line number Diff line number Diff line change
@@ -1,15 +1,43 @@
import { HomeAssistant } from "custom-card-helpers";
import { html } from "lit";
import { TemplateResult, html } from "lit";
import { BatteryStateEntity } from "./battery-state-entity";

export const secondaryInfo = (text?: string) => text && html`
<div class="secondary">${text}</div>
`;
const relativeTimeTag = new RegExp("<rt>([^<]+)</rt>", "g");

const secondaryInfoTime = (hass: HomeAssistant | undefined, time?: Date) => time && html`
<div class="secondary">
<ha-relative-time .hass="${hass}" .datetime="${time}"></ha-relative-time>
</div>
/**
* Replaces temporary RT tages with proper HA "relative-time" ones
*
* @param text Text to be processed
* @param hass HomeAssistant instance
* @returns Rendered templates
*/
const replaceTags = (text: string, hass?: HomeAssistant): TemplateResult[] => {

const result: TemplateResult[] = []

let matches: string[] | null = [];
let currentPos = 0;
while(matches = relativeTimeTag.exec(text)) {
const matchPos = text.indexOf(matches[0], currentPos);

if (matchPos != 0) {
result.push(html`${text.substring(currentPos, matchPos)}`);
}

result.push(html`<ha-relative-time .hass="${hass}" .datetime="${new Date(matches[1])}"></ha-relative-time>`);

currentPos += matchPos + matches[0].length;
}

if (currentPos < text.length) {
result.push(html`${text.substring(currentPos, text.length)}`);
}

return result;
}

export const secondaryInfo = (text?: string, hass?: HomeAssistant) => text && html`
<div class="secondary">${replaceTags(text, hass)}</div>
`;

export const icon = (icon?: string, color?: string) => icon && html`
Expand All @@ -25,7 +53,7 @@ export const batteryHtml = (model: BatteryStateEntity) => html`
${icon(model.icon, model.iconColor)}
<div class="name truncate">
${model.name}
${model.secondaryInfo instanceof Date ? secondaryInfoTime(model.hass, model.secondaryInfo) : secondaryInfo(model.secondaryInfo)}
${secondaryInfo(model.secondaryInfo, model.hass)}
</div>
<div class="state">
${model.state}${model.unit}
Expand Down
5 changes: 3 additions & 2 deletions src/entity-fields/get-secondary-info.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { isNumber } from "../utils";
* @param isCharging Whther battery is in charging mode
* @returns Secondary info text
*/
export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssistant | undefined, isCharging: boolean): string | Date => {
export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssistant | undefined, isCharging: boolean): string => {
if (config.secondary_info) {
const processor = new RichStringProcessor(hass, config.entity, {
"charging": isCharging ? (config.charging_state?.secondary_info_text || "Charging") : "" // todo: think about i18n
Expand All @@ -24,7 +24,8 @@ export const getSecondaryInfo = (config: IBatteryEntityConfig, hass: HomeAssista
}

const dateVal = Date.parse(result);
return isNaN(dateVal) ? result : new Date(dateVal);
// The RT tags will be converted to proper HA tags at the views layer
return isNaN(dateVal) ? result : "<rt>" + new Date(dateVal).getTime() + "</rt>";
}

return <any>null;
Expand Down
16 changes: 14 additions & 2 deletions src/rich-string-processor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ const validEntityDomains = ["sensor", "binary_sensor"];
* Replaces keywords in given string with the data
*/
process(text: string): string {
if (text === "") {
return text;
if (!text) {
return "";
}

return text.replace(/\{([^\}]+)\}/g, (matchWithBraces, keyword) => this.replaceKeyword(keyword, matchWithBraces));
Expand Down Expand Up @@ -191,6 +191,18 @@ const availableProcessors: IMap<IProcessorCtor> = {

return val => isNaN(addend) ? val : (Number(val) + addend).toString();
},
"reltime": () => {
return val => {
const unixTime = Date.parse(val);
if (isNaN(unixTime)) {
log("[KString]value isn't a valid date: " + val);
return val;
}

// The RT tags will be converted to proper HA tags at the views layer
return `<rt>${val}</rt>`
};
}
}

interface IProcessor {
Expand Down
2 changes: 1 addition & 1 deletion test/entity/secondary-info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,5 +68,5 @@ test("Secondary info date value - renders relative time element", async () => {
await cardElem.cardUpdated;

const entity = new EntityElements(cardElem);
expect((<HTMLElement>entity.secondaryInfo?.firstChild).tagName).toBe("HA-RELATIVE-TIME");
expect((<HTMLElement>entity.secondaryInfo?.firstElementChild).tagName).toBe("HA-RELATIVE-TIME");
});
16 changes: 16 additions & 0 deletions test/other/entity-fields/get-icon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,4 +61,20 @@ describe("Get icon", () => {
let icon = getIcon({ entity: "battery_state", icon: configuredIcon }, Number(state), false, hassMock.hass);
expect(icon).toBe(expectedResult);
})

test("icon from attribute", () => {
const hassMock = new HomeAssistantMock();
hassMock.addEntity("Battery state", "45", { icon: "mdi:attribute-icon" });

let icon = getIcon({ entity: "battery_state", icon: "attribute.icon" }, 45, false, hassMock.hass);
expect(icon).toBe("mdi:attribute-icon");
})

test("icon from attribute - attribute missing", () => {
const hassMock = new HomeAssistantMock();
hassMock.addEntity("Battery state", "45", { icon: "mdi:attribute-icon" });

let icon = getIcon({ entity: "battery_state", icon: "attribute.icon_non_existing" }, 45, false, hassMock.hass);
expect(icon).toBe("attribute.icon_non_existing");
})
});
13 changes: 11 additions & 2 deletions test/other/entity-fields/get-secondary-info.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,16 @@ describe("Secondary info", () => {
const secondaryInfoConfig = "{last_changed}";
const result = getSecondaryInfo({ entity: entity.entity_id, secondary_info: secondaryInfoConfig }, hassMock.hass, false);

expect(result).toBeInstanceOf(Date);
expect(JSON.stringify(result).slice(1, -1)).toBe("2022-02-07T00:00:00.000Z");
expect(result).toBe("<rt>1644192000000</rt>");
})

test("Secondary info config not set'", () => {
const hassMock = new HomeAssistantMock(true);
const entity = hassMock.addEntity("Motion sensor kitchen", "50", {}, "sensor");
entity.setLastChanged("2022-02-07");
const secondaryInfoConfig = "{last_changed}";
const result = getSecondaryInfo({ entity: entity.entity_id }, hassMock.hass, false);

expect(result).toBeNull();
})
})
42 changes: 42 additions & 0 deletions test/other/rich-string-processor.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,22 @@ import { RichStringProcessor } from "../../src/rich-string-processor"
import { HomeAssistantMock } from "../helpers"

describe("RichStringProcessor", () => {

test.each([
[null, ""],
[undefined, ""],
["", ""],
["false", "false"],
["0", "0"],
])("missing text", (input: any, expected: string) => {
const hassMock = new HomeAssistantMock<BatteryStateEntity>(true);
const motionEntity = hassMock.addEntity("Bedroom motion", "50", {}, "sensor");
const proc = new RichStringProcessor(hassMock.hass, motionEntity.entity_id);

const result = proc.process(input);
expect(result).toBe(expected);
})

test.each([
["Value {state}, {last_updated}", "Value 20.56, 2021-04-05 15:11:35"], // few placeholders
["Value {state}, {attributes.charging_state}", "Value 20.56, Charging"], // attribute value
Expand Down Expand Up @@ -138,4 +154,30 @@ describe("RichStringProcessor", () => {
const result = proc.process(text);
expect(result).toBe(expectedResult);
});

test.each([
["{state|equals(64,ok)}", "64", "ok"],
["{state|equals(65,err)}", "64", "64"],
["Not enough params {state|equals(64)}", "64", "Not enough params 64"],
])("equals function", (text: string, state:string, expectedResult: string) => {
const hassMock = new HomeAssistantMock<BatteryStateEntity>(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|reltime()}", "2021-08-25T00:00:00.000Z", "<rt>2021-08-25T00:00:00.000Z</rt>"],
["Rel time: {state|reltime()}", "2021-08-25T00:00:00.000Z", "Rel time: <rt>2021-08-25T00:00:00.000Z</rt>"],
["Not date: {state|reltime()}", "this is not date", "Not date: this is not date"],
])("reltime function", (text: string, state:string, expectedResult: string) => {
const hassMock = new HomeAssistantMock<BatteryStateEntity>(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);
});
})

0 comments on commit 31d8c64

Please sign in to comment.