mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-27 11:16:35 +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 { HassEntity } from "home-assistant-js-websocket";
|
||||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
import { until } from "lit/directives/until";
|
|
||||||
import { haStyle } from "../resources/styles";
|
import { haStyle } from "../resources/styles";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import hassAttributeUtil, {
|
import hassAttributeUtil, {
|
||||||
formatAttributeName,
|
formatAttributeName,
|
||||||
|
formatAttributeValue,
|
||||||
} from "../util/hass-attributes-util";
|
} from "../util/hass-attributes-util";
|
||||||
import "./ha-expansion-panel";
|
import "./ha-expansion-panel";
|
||||||
|
|
||||||
let jsYamlPromise: Promise<typeof import("js-yaml")>;
|
|
||||||
|
|
||||||
@customElement("ha-attributes")
|
@customElement("ha-attributes")
|
||||||
class HaAttributes extends LitElement {
|
class HaAttributes extends LitElement {
|
||||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
@ -124,38 +122,7 @@ class HaAttributes extends LitElement {
|
|||||||
return "-";
|
return "-";
|
||||||
}
|
}
|
||||||
const value = this.stateObj.attributes[attribute];
|
const value = this.stateObj.attributes[attribute];
|
||||||
return this.formatAttributeValue(value);
|
return formatAttributeValue(this.hass, 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private expandedChanged(ev) {
|
private expandedChanged(ev) {
|
||||||
|
@ -18,6 +18,7 @@ import "../../../components/ha-card";
|
|||||||
import "../../../components/ha-icon";
|
import "../../../components/ha-icon";
|
||||||
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
import { UNAVAILABLE_STATES } from "../../../data/entity";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { formatAttributeValue } from "../../../util/hass-attributes-util";
|
||||||
import { computeCardSize } from "../common/compute-card-size";
|
import { computeCardSize } from "../common/compute-card-size";
|
||||||
import { findEntities } from "../common/find-entities";
|
import { findEntities } from "../common/find-entities";
|
||||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||||
@ -124,8 +125,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
|
|||||||
<div class="info">
|
<div class="info">
|
||||||
<span class="value"
|
<span class="value"
|
||||||
>${"attribute" in this._config
|
>${"attribute" in this._config
|
||||||
? stateObj.attributes[this._config.attribute!] ??
|
? formatAttributeValue(
|
||||||
|
this.hass,
|
||||||
|
stateObj.attributes[this._config.attribute!] ??
|
||||||
this.hass.localize("state.default.unknown")
|
this.hass.localize("state.default.unknown")
|
||||||
|
)
|
||||||
: stateObj.attributes.unit_of_measurement
|
: stateObj.attributes.unit_of_measurement
|
||||||
? formatNumber(stateObj.state, this.hass.locale)
|
? formatNumber(stateObj.state, this.hass.locale)
|
||||||
: computeStateDisplay(
|
: computeStateDisplay(
|
||||||
|
@ -10,6 +10,7 @@ import { customElement, property, state } from "lit/decorators";
|
|||||||
import checkValidDate from "../../../common/datetime/check_valid_date";
|
import checkValidDate from "../../../common/datetime/check_valid_date";
|
||||||
import { formatNumber } from "../../../common/string/format_number";
|
import { formatNumber } from "../../../common/string/format_number";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
|
import { formatAttributeValue } from "../../../util/hass-attributes-util";
|
||||||
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
import { hasConfigOrEntityChanged } from "../common/has-changed";
|
||||||
import "../components/hui-generic-entity-row";
|
import "../components/hui-generic-entity-row";
|
||||||
import "../components/hui-timestamp-display";
|
import "../components/hui-timestamp-display";
|
||||||
@ -72,7 +73,9 @@ class HuiAttributeRow extends LitElement implements LovelaceRow {
|
|||||||
></hui-timestamp-display>`
|
></hui-timestamp-display>`
|
||||||
: typeof attribute === "number"
|
: typeof attribute === "number"
|
||||||
? formatNumber(attribute, this.hass.locale)
|
? formatNumber(attribute, this.hass.locale)
|
||||||
: attribute ?? "-"}
|
: attribute !== undefined
|
||||||
|
? formatAttributeValue(this.hass, attribute)
|
||||||
|
: "-"}
|
||||||
${this._config.suffix}
|
${this._config.suffix}
|
||||||
</div>
|
</div>
|
||||||
</hui-generic-entity-row>
|
</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 = {
|
const hassAttributeUtil = {
|
||||||
DOMAIN_DEVICE_CLASS: {
|
DOMAIN_DEVICE_CLASS: {
|
||||||
binary_sensor: [
|
binary_sensor: [
|
||||||
@ -130,3 +141,59 @@ export function formatAttributeName(value: string): string {
|
|||||||
.replace(/\bgps\b/g, "GPS");
|
.replace(/\bgps\b/g, "GPS");
|
||||||
return value.charAt(0).toUpperCase() + value.slice(1);
|
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