Detect and format date & timestamp attributes (#9074)

This commit is contained in:
Philip Allgaier 2021-05-25 22:39:21 +02:00 committed by GitHub
parent d97fb19f05
commit 3d4d789f7f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 113 additions and 38 deletions

View File

@ -0,0 +1,11 @@
// https://regex101.com/r/kc5C14/2
const regExpString = "^\\d{4}-(0[1-9]|1[0-2])-([12]\\d|0[1-9]|3[01])";
const regExp = new RegExp(regExpString + "$");
// 2nd expression without the "end of string" enforced, so it can be used
// to just verify the start of a string and then based on that result e.g.
// check for a full timestamp string efficiently.
const regExpNoStringEnd = new RegExp(regExpString);
export const isDate = (input: string, allowCharsAfterDate = false): boolean =>
allowCharsAfterDate ? regExpNoStringEnd.test(input) : regExp.test(input);

View File

@ -0,0 +1,11 @@
// https://stackoverflow.com/a/14322189/1947205
// Changes:
// 1. Do not allow a plus or minus at the start.
// 2. Enforce that we have a "T" or a blank after the date portion
// to ensure we have a timestamp and not only a date.
// 3. Disallow dates based on week number.
// 4. Disallow dates only consisting of a year.
// https://regex101.com/r/kc5C14/3
const regexp = /^\d{4}-(0[1-9]|1[0-2])-([12]\d|0[1-9]|3[01])[T| ](((([01]\d|2[0-3])((:?)[0-5]\d)?|24:?00)([.,]\d+(?!:))?)(\8[0-5]\d([.,]\d+)?)?([zZ]|([+-])([01]\d|2[0-3]):?([0-5]\d)?)?)$/;
export const isTimestamp = (input: string): boolean => regexp.test(input);

View File

@ -1,16 +1,14 @@
import { HassEntity } from "home-assistant-js-websocket";
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { haStyle } from "../resources/styles";
import { HomeAssistant } from "../types";
import hassAttributeUtil, {
formatAttributeName,
formatAttributeValue,
} from "../util/hass-attributes-util";
import "./ha-expansion-panel";
let jsYamlPromise: Promise<typeof import("js-yaml")>;
@customElement("ha-attributes")
class HaAttributes extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -124,38 +122,7 @@ class HaAttributes extends LitElement {
return "-";
}
const value = this.stateObj.attributes[attribute];
return this.formatAttributeValue(value);
}
private formatAttributeValue(value: any): string | TemplateResult {
if (value === null) {
return "-";
}
// YAML handling
if (
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("js-yaml");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
return html` <pre>${until(yaml, "")}</pre> `;
}
// URL handling
if (typeof value === "string" && value.startsWith("http")) {
try {
// If invalid URL, exception will be raised
const url = new URL(value);
if (url.protocol === "http:" || url.protocol === "https:")
return html`<a target="_blank" rel="noreferrer" href="${value}"
>${value}</a
>`;
} catch (_) {
// Nothing to do here
}
}
return Array.isArray(value) ? value.join(", ") : value;
return formatAttributeValue(this.hass, value);
}
private expandedChanged(ev) {

View File

@ -18,6 +18,7 @@ import "../../../components/ha-card";
import "../../../components/ha-icon";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
import { formatAttributeValue } from "../../../util/hass-attributes-util";
import { computeCardSize } from "../common/compute-card-size";
import { findEntities } from "../common/find-entities";
import { hasConfigOrEntityChanged } from "../common/has-changed";
@ -124,8 +125,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
<div class="info">
<span class="value"
>${"attribute" in this._config
? stateObj.attributes[this._config.attribute!] ??
this.hass.localize("state.default.unknown")
? formatAttributeValue(
this.hass,
stateObj.attributes[this._config.attribute!] ??
this.hass.localize("state.default.unknown")
)
: stateObj.attributes.unit_of_measurement
? formatNumber(stateObj.state, this.hass.locale)
: computeStateDisplay(

View File

@ -10,6 +10,7 @@ import { customElement, property, state } from "lit/decorators";
import checkValidDate from "../../../common/datetime/check_valid_date";
import { formatNumber } from "../../../common/string/format_number";
import { HomeAssistant } from "../../../types";
import { formatAttributeValue } from "../../../util/hass-attributes-util";
import { hasConfigOrEntityChanged } from "../common/has-changed";
import "../components/hui-generic-entity-row";
import "../components/hui-timestamp-display";
@ -72,7 +73,9 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
></hui-timestamp-display>`
: typeof attribute === "number"
? formatNumber(attribute, this.hass.locale)
: attribute ?? "-"}
: attribute !== undefined
? formatAttributeValue(this.hass, attribute)
: "-"}
${this._config.suffix}
</div>
</hui-generic-entity-row>

View File

@ -1,3 +1,14 @@
import { html, TemplateResult } from "lit";
import { until } from "lit/directives/until";
import checkValidDate from "../common/datetime/check_valid_date";
import { formatDate } from "../common/datetime/format_date";
import { formatDateTimeWithSeconds } from "../common/datetime/format_date_time";
import { isDate } from "../common/string/is_date";
import { isTimestamp } from "../common/string/is_timestamp";
import { HomeAssistant } from "../types";
let jsYamlPromise: Promise<typeof import("js-yaml")>;
const hassAttributeUtil = {
DOMAIN_DEVICE_CLASS: {
binary_sensor: [
@ -130,3 +141,59 @@ export function formatAttributeName(value: string): string {
.replace(/\bgps\b/g, "GPS");
return value.charAt(0).toUpperCase() + value.slice(1);
}
export function formatAttributeValue(
hass: HomeAssistant,
value: any
): string | TemplateResult {
if (value === null) {
return "-";
}
// YAML handling
if (
(Array.isArray(value) && value.some((val) => val instanceof Object)) ||
(!Array.isArray(value) && value instanceof Object)
) {
if (!jsYamlPromise) {
jsYamlPromise = import("js-yaml");
}
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
return html`<pre>${until(yaml, "")}</pre>`;
}
if (typeof value === "string") {
// URL handling
if (value.startsWith("http")) {
try {
// If invalid URL, exception will be raised
const url = new URL(value);
if (url.protocol === "http:" || url.protocol === "https:")
return html`<a target="_blank" rel="noreferrer" href="${value}"
>${value}</a
>`;
} catch (_) {
// Nothing to do here
}
}
// Date handling
if (isDate(value, true)) {
// Timestamp handling
if (isTimestamp(value)) {
const date = new Date(value);
if (checkValidDate(date)) {
return formatDateTimeWithSeconds(date, hass.locale);
}
}
// Value was not a timestamp, so only do date formatting
const date = new Date(value);
if (checkValidDate(date)) {
return formatDate(date, hass.locale);
}
}
}
return Array.isArray(value) ? value.join(", ") : value;
}

View File

@ -0,0 +1,12 @@
import { assert } from "chai";
import { isDate } from "../../../src/common/string/is_date";
describe("isDate", () => {
assert.strictEqual(isDate("ABC"), false);
assert.strictEqual(isDate("2021-02-03", false), true);
assert.strictEqual(isDate("2021-02-03", true), true);
assert.strictEqual(isDate("2021-05-25T19:23:52+00:00", true), true);
assert.strictEqual(isDate("2021-05-25T19:23:52+00:00", false), false);
});