diff --git a/src/common/datetime/check_valid_date.ts b/src/common/datetime/check_valid_date.ts new file mode 100644 index 0000000000..e380a9b79f --- /dev/null +++ b/src/common/datetime/check_valid_date.ts @@ -0,0 +1,7 @@ +export default function checkValidDate(date?: Date): boolean { + if (!date) { + return false; + } + + return date instanceof Date && !isNaN(date.valueOf()); +} diff --git a/src/panels/lovelace/components/hui-timestamp-display.ts b/src/panels/lovelace/components/hui-timestamp-display.ts index 8fab650212..08bcfe8770 100644 --- a/src/panels/lovelace/components/hui-timestamp-display.ts +++ b/src/panels/lovelace/components/hui-timestamp-display.ts @@ -12,6 +12,7 @@ import { formatDateTime } from "../../../common/datetime/format_date_time"; import { formatTime } from "../../../common/datetime/format_time"; import relativeTime from "../../../common/datetime/relative_time"; import { HomeAssistant } from "../../../types"; +import { TimestampRenderingFormats } from "./types"; const FORMATS: { [key: string]: (ts: Date, lang: string) => string } = { date: formatDate, @@ -26,12 +27,7 @@ class HuiTimestampDisplay extends LitElement { @property() public ts?: Date; - @property() public format?: - | "relative" - | "total" - | "date" - | "datetime" - | "time"; + @property() public format?: TimestampRenderingFormats; @internalProperty() private _relative?: string; diff --git a/src/panels/lovelace/components/types.ts b/src/panels/lovelace/components/types.ts index 40638895a9..4e49ee3e44 100644 --- a/src/panels/lovelace/components/types.ts +++ b/src/panels/lovelace/components/types.ts @@ -6,3 +6,10 @@ export interface ConditionalBaseConfig extends LovelaceCardConfig { card: LovelaceCardConfig | LovelaceElementConfig; conditions: Condition[]; } + +export type TimestampRenderingFormats = + | "relative" + | "total" + | "date" + | "time" + | "datetime"; diff --git a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts index cd62d9bd31..d91e841d09 100644 --- a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts @@ -22,10 +22,11 @@ import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; import "../components/hui-timestamp-display"; import { createEntityNotFoundWarning } from "../components/hui-warning"; +import { TimestampRenderingFormats } from "../components/types"; import { LovelaceRow } from "./types"; interface SensorEntityConfig extends EntitiesCardEntityConfig { - format?: "relative" | "total" | "date" | "time" | "datetime"; + format?: TimestampRenderingFormats; } @customElement("hui-sensor-entity-row") diff --git a/src/panels/lovelace/entity-rows/types.ts b/src/panels/lovelace/entity-rows/types.ts index 729a986a15..b2eb451582 100644 --- a/src/panels/lovelace/entity-rows/types.ts +++ b/src/panels/lovelace/entity-rows/types.ts @@ -1,6 +1,7 @@ import { ActionConfig } from "../../../data/lovelace"; import { HomeAssistant } from "../../../types"; import { Condition } from "../common/validate-condition"; +import { TimestampRenderingFormats } from "../components/types"; export interface EntityConfig { entity: string; @@ -84,8 +85,10 @@ export interface ConditionalRowConfig extends EntityConfig { row: EntityConfig; conditions: Condition[]; } + export interface AttributeRowConfig extends EntityConfig { attribute: string; prefix?: string; suffix?: string; + format?: TimestampRenderingFormats; } diff --git a/src/panels/lovelace/special-rows/hui-attribute-row.ts b/src/panels/lovelace/special-rows/hui-attribute-row.ts index e2c9ed0951..da15054a57 100644 --- a/src/panels/lovelace/special-rows/hui-attribute-row.ts +++ b/src/panels/lovelace/special-rows/hui-attribute-row.ts @@ -9,9 +9,11 @@ import { PropertyValues, TemplateResult, } from "lit-element"; +import checkValidDate from "../../../common/datetime/check_valid_date"; import { HomeAssistant } from "../../../types"; import { hasConfigOrEntityChanged } from "../common/has-changed"; import "../components/hui-generic-entity-row"; +import "../components/hui-timestamp-display"; import { createEntityNotFoundWarning } from "../components/hui-warning"; import { AttributeRowConfig, LovelaceRow } from "../entity-rows/types"; @@ -44,7 +46,6 @@ class HuiAttributeRow extends LitElement implements LovelaceRow { } const stateObj = this.hass.states[this._config.entity]; - const attribute = stateObj.attributes[this._config.attribute]; if (!stateObj) { return html` @@ -54,10 +55,24 @@ class HuiAttributeRow extends LitElement implements LovelaceRow { `; } + const attribute = stateObj.attributes[this._config.attribute]; + let date: Date | undefined; + if (this._config.format) { + date = new Date(attribute); + } + return html`
- ${this._config.prefix} ${attribute ?? "-"} ${this._config.suffix} + ${this._config.prefix} + ${this._config.format && checkValidDate(date) + ? html` ` + : attribute ?? "-"} + ${this._config.suffix}
`; diff --git a/test-mocha/common/datetime/check_valid_date.ts b/test-mocha/common/datetime/check_valid_date.ts new file mode 100644 index 0000000000..1ef7d1ace4 --- /dev/null +++ b/test-mocha/common/datetime/check_valid_date.ts @@ -0,0 +1,19 @@ +import { assert } from "chai"; + +import checkValidDate from "../../../src/common/datetime/check_valid_date"; + +describe("checkValidDate", () => { + it("works", () => { + assert.strictEqual(checkValidDate(new Date()), true); + assert.strictEqual( + checkValidDate(new Date("2021-01-19T11:36:57+00:00")), + true + ); + assert.strictEqual( + checkValidDate(new Date("2021-01-19X11:36:57+00:00")), + false + ); + assert.strictEqual(checkValidDate(new Date("2021-01-19")), true); + assert.strictEqual(checkValidDate(undefined), false); + }); +}); diff --git a/test-mocha/common/datetime/relative_time.ts b/test-mocha/common/datetime/relative_time.ts new file mode 100644 index 0000000000..d7aaa0b097 --- /dev/null +++ b/test-mocha/common/datetime/relative_time.ts @@ -0,0 +1,217 @@ +import { assert } from "chai"; + +import relativeTime from "../../../src/common/datetime/relative_time"; + +describe("relativeTime", () => { + // Mock localize function for testing + const localize = (message, ...args) => + message + (args.length ? ": " + args.join(",") : ""); + + it("now", () => { + const now = new Date(); + assert.strictEqual( + relativeTime(now, localize, { compareTime: now }), + "ui.components.relative_time.just_now" + ); + }); + + it("past_second", () => { + const inputdt = new Date("2021-02-03T11:22:00+00:00"); + const compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.past_duration.second: count,33" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.second: count,33" + ); + }); + + it("past_minute", () => { + const inputdt = new Date("2021-02-03T11:20:33+00:00"); + const compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.past_duration.minute: count,2" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.minute: count,2" + ); + }); + + it("past_hour", () => { + const inputdt = new Date("2021-02-03T09:22:33+00:00"); + const compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.past_duration.hour: count,2" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.hour: count,2" + ); + }); + + it("past_day", () => { + let inputdt = new Date("2021-02-01T11:22:33+00:00"); + let compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.past_duration.day: count,2" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.day: count,2" + ); + + // Test switch from days to weeks + inputdt = new Date("2021-01-28T11:22:33+00:00"); + compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.day: count,6" + ); + inputdt = new Date("2021-01-27T11:22:33+00:00"); + compare = new Date("2021-02-03T11:22:33+00:00"); + assert.notStrictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.day: count,7" + ); + }); + + it("past_week", () => { + const inputdt = new Date("2021-01-03T11:22:33+00:00"); + const compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.past_duration.week: count,4" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.week: count,4" + ); + }); + + it("future_second", () => { + const inputdt = new Date("2021-02-03T11:22:55+00:00"); + const compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.future_duration.second: count,22" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.second: count,22" + ); + }); + + it("future_minute", () => { + const inputdt = new Date("2021-02-03T11:24:33+00:00"); + const compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.future_duration.minute: count,2" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.minute: count,2" + ); + }); + + it("future_hour", () => { + const inputdt = new Date("2021-02-03T13:22:33+00:00"); + const compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.future_duration.hour: count,2" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.hour: count,2" + ); + }); + + it("future_day", () => { + let inputdt = new Date("2021-02-05T11:22:33+00:00"); + let compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.future_duration.day: count,2" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.day: count,2" + ); + + // Test switch from days to weeks + inputdt = new Date("2021-02-09T11:22:33+00:00"); + compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.day: count,6" + ); + inputdt = new Date("2021-02-10T11:22:33+00:00"); + compare = new Date("2021-02-03T11:22:33+00:00"); + assert.notStrictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.day: count,7" + ); + }); + + it("future_week", () => { + const inputdt = new Date("2021-03-03T11:22:33+00:00"); + const compare = new Date("2021-02-03T11:22:33+00:00"); + assert.strictEqual( + relativeTime(inputdt, localize, { compareTime: compare }), + "ui.components.relative_time.future_duration.week: count,4" + ); + assert.strictEqual( + relativeTime(inputdt, localize, { + compareTime: compare, + includeTense: false, + }), + "ui.components.relative_time.duration.week: count,4" + ); + }); +});