Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

KString convert to relative time func #614

Merged
merged 4 commits into from
Dec 21, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
});
})
Loading