From a7ab652dd37878f7ea3ae402be996a06938a0b9c Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Fri, 23 Nov 2018 19:38:28 +0100 Subject: [PATCH] Add support for timestamp device class (#2087) --- src/common/datetime/relative_time.ts | 34 +++-- .../lovelace/common/create-row-element.js | 2 + .../components/hui-timestamp-display.ts | 130 ++++++++++++++++++ .../entity-rows/hui-error-entity-row.ts | 5 +- .../entity-rows/hui-sensor-entity-row.ts | 85 ++++++++++++ 5 files changed, 244 insertions(+), 12 deletions(-) create mode 100644 src/panels/lovelace/components/hui-timestamp-display.ts create mode 100644 src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts diff --git a/src/common/datetime/relative_time.ts b/src/common/datetime/relative_time.ts index 7daabdc657..ef5a895df0 100644 --- a/src/common/datetime/relative_time.ts +++ b/src/common/datetime/relative_time.ts @@ -10,31 +10,43 @@ const langKey = ["second", "minute", "hour", "day"]; export default function relativeTime( dateObj: Date, - localize: LocalizeFunc + localize: LocalizeFunc, + options: { + compareTime?: Date; + includeTense?: boolean; + } = {} ): string { - let delta = (new Date().getTime() - dateObj.getTime()) / 1000; + const compareTime = options.compareTime || new Date(); + let delta = (compareTime.getTime() - dateObj.getTime()) / 1000; const tense = delta >= 0 ? "past" : "future"; delta = Math.abs(delta); + let timeDesc; + for (let i = 0; i < tests.length; i++) { if (delta < tests[i]) { delta = Math.floor(delta); - const timeDesc = localize( + timeDesc = localize( `ui.components.relative_time.duration.${langKey[i]}`, "count", delta ); - return localize(`ui.components.relative_time.${tense}`, "time", timeDesc); + break; } delta /= tests[i]; } - delta = Math.floor(delta); - const time = localize( - "ui.components.relative_time.duration.week", - "count", - delta - ); - return localize(`ui.components.relative_time.${tense}`, "time", time); + if (timeDesc === undefined) { + delta = Math.floor(delta); + timeDesc = localize( + "ui.components.relative_time.duration.week", + "count", + delta + ); + } + + return options.includeTense === false + ? timeDesc + : localize(`ui.components.relative_time.${tense}`, "time", timeDesc); } diff --git a/src/panels/lovelace/common/create-row-element.js b/src/panels/lovelace/common/create-row-element.js index 00d9d36d17..9ec0a5efef 100644 --- a/src/panels/lovelace/common/create-row-element.js +++ b/src/panels/lovelace/common/create-row-element.js @@ -10,6 +10,7 @@ import "../entity-rows/hui-lock-entity-row"; import "../entity-rows/hui-media-player-entity-row"; import "../entity-rows/hui-scene-entity-row"; import "../entity-rows/hui-script-entity-row"; +import "../entity-rows/hui-sensor-entity-row"; import "../entity-rows/hui-text-entity-row"; import "../entity-rows/hui-timer-entity-row"; import "../entity-rows/hui-toggle-entity-row"; @@ -42,6 +43,7 @@ const DOMAIN_TO_ELEMENT_TYPE = { lock: "lock", scene: "scene", script: "script", + sensor: "sensor", timer: "timer", switch: "toggle", vacuum: "toggle", diff --git a/src/panels/lovelace/components/hui-timestamp-display.ts b/src/panels/lovelace/components/hui-timestamp-display.ts new file mode 100644 index 0000000000..d97b9abdd0 --- /dev/null +++ b/src/panels/lovelace/components/hui-timestamp-display.ts @@ -0,0 +1,130 @@ +import { + html, + LitElement, + PropertyDeclarations, + PropertyValues, +} from "@polymer/lit-element"; +import { TemplateResult } from "lit-html"; + +import { HomeAssistant } from "../../../types"; +import format_date from "../../../common/datetime/format_date"; +import format_date_time from "../../../common/datetime/format_date_time"; +import format_time from "../../../common/datetime/format_time"; +import relativeTime from "../../../common/datetime/relative_time"; +import { hassLocalizeLitMixin } from "../../../mixins/lit-localize-mixin"; + +const FORMATS: { [key: string]: (ts: Date, lang: string) => string } = { + date: format_date, + datetime: format_date_time, + time: format_time, +}; +const INTERVAL_FORMAT = ["relative", "total"]; + +class HuiTimestampDisplay extends hassLocalizeLitMixin(LitElement) { + public hass?: HomeAssistant; + public ts?: Date; + public format?: "relative" | "total" | "date" | "datetime" | "time"; + private _relative?: string; + private _connected?: boolean; + private _interval?: number; + + static get properties(): PropertyDeclarations { + return { + ts: {}, + hass: {}, + format: {}, + _relative: {}, + }; + } + + public connectedCallback() { + super.connectedCallback(); + this._connected = true; + this._startInterval(); + } + + public disconnectedCallback() { + super.disconnectedCallback(); + this._connected = false; + this._clearInterval(); + } + + protected render(): TemplateResult { + if (!this.ts || !this.hass) { + return html``; + } + + if (isNaN(this.ts.getTime())) { + return html` + Invalid date + `; + } + + const format = this._format; + + if (INTERVAL_FORMAT.includes(format)) { + return html` + ${this._relative} + `; + } else if (format in FORMATS) { + return html` + ${FORMATS[format](this.ts, this.hass.language)} + `; + } else { + return html` + Invalid format + `; + } + } + + protected updated(changedProperties: PropertyValues) { + if (!changedProperties.has("format") || !this._connected) { + return; + } + + if (INTERVAL_FORMAT.includes("relative")) { + this._startInterval(); + } else { + this._clearInterval(); + } + } + + private get _format() { + return this.format || "relative"; + } + + private _startInterval() { + this._clearInterval(); + if (this._connected && INTERVAL_FORMAT.includes(this._format)) { + this._updateRelative(); + this._interval = window.setInterval(() => this._updateRelative(), 1000); + } + } + + private _clearInterval() { + if (this._interval) { + clearInterval(this._interval); + this._interval = undefined; + } + } + + private _updateRelative() { + if (this.ts && this.localize) { + this._relative = + this._format === "relative" + ? relativeTime(this.ts, this.localize) + : (this._relative = relativeTime(new Date(), this.localize, { + compareTime: this.ts, + includeTense: false, + })); + } + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-timestamp-display": HuiTimestampDisplay; + } +} + +customElements.define("hui-timestamp-display", HuiTimestampDisplay); diff --git a/src/panels/lovelace/entity-rows/hui-error-entity-row.ts b/src/panels/lovelace/entity-rows/hui-error-entity-row.ts index c235d4aac1..252ed1dfd6 100644 --- a/src/panels/lovelace/entity-rows/hui-error-entity-row.ts +++ b/src/panels/lovelace/entity-rows/hui-error-entity-row.ts @@ -3,16 +3,19 @@ import { TemplateResult } from "lit-html"; class HuiErrorEntityRow extends LitElement { public entity?: string; + public error?: string; static get properties() { return { + error: {}, entity: {}, }; } protected render(): TemplateResult { return html` - ${this.renderStyle()} Entity not available: ${this.entity || ""} + ${this.renderStyle()} ${this.error || "Entity not available"}: + ${this.entity || ""} `; } diff --git a/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts new file mode 100644 index 0000000000..79b1707774 --- /dev/null +++ b/src/panels/lovelace/entity-rows/hui-sensor-entity-row.ts @@ -0,0 +1,85 @@ +import { html, LitElement, PropertyDeclarations } from "@polymer/lit-element"; +import { TemplateResult } from "lit-html"; + +import "../components/hui-generic-entity-row"; +import "../components/hui-timestamp-display"; +import "./hui-error-entity-row"; + +import { HomeAssistant } from "../../../types"; +import { EntityRow, EntityConfig } from "./types"; + +interface SensorEntityConfig extends EntityConfig { + format?: "relative" | "date" | "time" | "datetime"; +} + +class HuiSensorEntityRow extends LitElement implements EntityRow { + public hass?: HomeAssistant; + private _config?: SensorEntityConfig; + + static get properties(): PropertyDeclarations { + return { + hass: {}, + _config: {}, + }; + } + + public setConfig(config: SensorEntityConfig): void { + if (!config) { + throw new Error("Configuration error"); + } + this._config = config; + } + + protected render(): TemplateResult { + if (!this._config || !this.hass) { + return html``; + } + + const stateObj = this.hass.states[this._config.entity]; + + if (!stateObj) { + return html` + + `; + } + + return html` + ${this.renderStyle()} + +
+ ${ + stateObj.attributes.device_class === "timestamp" + ? html` + + ` + : stateObj.state + } +
+
+ `; + } + + private renderStyle(): TemplateResult { + return html` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "hui-sensor-entity-row": HuiSensorEntityRow; + } +} + +customElements.define("hui-sensor-entity-row", HuiSensorEntityRow);