mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-23 17:26:42 +00:00
Detect and format date & timestamp attributes (#9074)
This commit is contained in:
parent
d97fb19f05
commit
3d4d789f7f
11
src/common/string/is_date.ts
Normal file
11
src/common/string/is_date.ts
Normal 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);
|
11
src/common/string/is_timestamp.ts
Normal file
11
src/common/string/is_timestamp.ts
Normal 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);
|
@ -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) {
|
||||
|
@ -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(
|
||||
|
@ -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>
|
||||
|
@ -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;
|
||||
}
|
||||
|
12
test-mocha/common/string/is_date.ts
Normal file
12
test-mocha/common/string/is_date.ts
Normal 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);
|
||||
});
|
Loading…
x
Reference in New Issue
Block a user