diff --git a/src/common/datetime/duration.ts b/src/common/datetime/duration.ts deleted file mode 100644 index de3b64a671..0000000000 --- a/src/common/datetime/duration.ts +++ /dev/null @@ -1,109 +0,0 @@ -import { DurationFormat } from "@formatjs/intl-durationformat"; -import type { DurationInput } from "@formatjs/intl-durationformat/src/types"; -import memoizeOne from "memoize-one"; -import type { FrontendLocaleData } from "../../data/translation"; -import { round } from "../number/round"; - -export const DURATION_UNITS = ["ms", "s", "min", "h", "d"] as const; - -type DurationUnit = (typeof DURATION_UNITS)[number]; - -const formatDurationDayMem = memoizeOne( - (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { - style: "narrow", - daysDisplay: "always", - }) -); - -const formatDurationHourMem = memoizeOne( - (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { - style: "narrow", - hoursDisplay: "always", - }) -); - -const formatDurationMinuteMem = memoizeOne( - (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { - style: "narrow", - minutesDisplay: "always", - }) -); - -const formatDurationSecondMem = memoizeOne( - (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { - style: "narrow", - secondsDisplay: "always", - }) -); - -const formatDurationMillisecondMem = memoizeOne( - (locale: FrontendLocaleData) => - new DurationFormat(locale.language, { - style: "narrow", - millisecondsDisplay: "always", - }) -); - -export const formatDuration = ( - duration: string, - unit: DurationUnit, - precision: number | undefined, - locale: FrontendLocaleData -): string => { - const value = - precision !== undefined - ? round(parseFloat(duration), precision) - : parseFloat(duration); - - switch (unit) { - case "d": { - const days = Math.floor(value); - const hours = Math.floor((value - days) * 24); - const input: DurationInput = { - days, - hours, - }; - return formatDurationDayMem(locale).format(input); - } - case "h": { - const hours = Math.floor(value); - const minutes = Math.floor((value - hours) * 60); - const input: DurationInput = { - hours, - minutes, - }; - return formatDurationHourMem(locale).format(input); - } - case "min": { - const minutes = Math.floor(value); - const seconds = Math.floor((value - minutes) * 60); - const input: DurationInput = { - minutes, - seconds, - }; - return formatDurationMinuteMem(locale).format(input); - } - case "s": { - const seconds = Math.floor(value); - const milliseconds = Math.floor((value - seconds) * 1000); - const input: DurationInput = { - seconds, - milliseconds, - }; - return formatDurationSecondMem(locale).format(input); - } - case "ms": { - const milliseconds = Math.floor(value); - const input: DurationInput = { - milliseconds, - }; - return formatDurationMillisecondMem(locale).format(input); - } - default: - throw new Error("Invalid duration unit"); - } -}; diff --git a/src/common/datetime/format_duration.ts b/src/common/datetime/format_duration.ts index 1639e08bda..a67d5f82ca 100644 --- a/src/common/datetime/format_duration.ts +++ b/src/common/datetime/format_duration.ts @@ -1,6 +1,9 @@ import { DurationFormat } from "@formatjs/intl-durationformat"; +import type { DurationInput } from "@formatjs/intl-durationformat/src/types"; +import memoizeOne from "memoize-one"; import type { HaDurationData } from "../../components/ha-duration-input"; import type { FrontendLocaleData } from "../../data/translation"; +import { round } from "../number/round"; const leftPad = (num: number) => (num < 10 ? `0${num}` : num); @@ -44,10 +47,131 @@ export const formatNumericDuration = ( return null; }; +const formatDurationLongMem = memoizeOne( + (locale: FrontendLocaleData) => + new DurationFormat(locale.language, { + style: "long", + }) +); + export const formatDurationLong = ( locale: FrontendLocaleData, duration: HaDurationData -) => - new DurationFormat(locale.language, { - style: "long", - }).format(duration); +) => formatDurationLongMem(locale).format(duration); + +const formatDigitalDurationMem = memoizeOne( + (locale: FrontendLocaleData) => + new DurationFormat(locale.language, { + style: "digital", + hoursDisplay: "auto", + }) +); + +export const formatDurationDigital = ( + locale: FrontendLocaleData, + duration: HaDurationData +) => formatDigitalDurationMem(locale).format(duration); + +export const DURATION_UNITS = ["ms", "s", "min", "h", "d"] as const; + +type DurationUnit = (typeof DURATION_UNITS)[number]; + +const formatDurationDayMem = memoizeOne( + (locale: FrontendLocaleData) => + new DurationFormat(locale.language, { + style: "narrow", + daysDisplay: "always", + }) +); + +const formatDurationHourMem = memoizeOne( + (locale: FrontendLocaleData) => + new DurationFormat(locale.language, { + style: "narrow", + hoursDisplay: "always", + }) +); + +const formatDurationMinuteMem = memoizeOne( + (locale: FrontendLocaleData) => + new DurationFormat(locale.language, { + style: "narrow", + minutesDisplay: "always", + }) +); + +const formatDurationSecondMem = memoizeOne( + (locale: FrontendLocaleData) => + new DurationFormat(locale.language, { + style: "narrow", + secondsDisplay: "always", + }) +); + +const formatDurationMillisecondMem = memoizeOne( + (locale: FrontendLocaleData) => + new DurationFormat(locale.language, { + style: "narrow", + millisecondsDisplay: "always", + }) +); + +export const formatDuration = ( + locale: FrontendLocaleData, + duration: string, + unit: DurationUnit, + precision?: number | undefined +): string => { + const value = + precision !== undefined + ? round(parseFloat(duration), precision) + : parseFloat(duration); + + switch (unit) { + case "d": { + const days = Math.floor(value); + const hours = Math.floor((value - days) * 24); + const input: DurationInput = { + days, + hours, + }; + return formatDurationDayMem(locale).format(input); + } + case "h": { + const hours = Math.floor(value); + const minutes = Math.floor((value - hours) * 60); + const input: DurationInput = { + hours, + minutes, + }; + return formatDurationHourMem(locale).format(input); + } + case "min": { + const minutes = Math.floor(value); + const seconds = Math.floor((value - minutes) * 60); + const input: DurationInput = { + minutes, + seconds, + }; + return formatDurationMinuteMem(locale).format(input); + } + case "s": { + const seconds = Math.floor(value); + const milliseconds = Math.floor((value - seconds) * 1000); + const input: DurationInput = { + seconds, + milliseconds, + }; + return formatDurationSecondMem(locale).format(input); + } + case "ms": { + const milliseconds = Math.floor(value); + const input: DurationInput = { + milliseconds, + }; + return formatDurationMillisecondMem(locale).format(input); + } + default: + throw new Error("Invalid duration unit"); + } +}; diff --git a/src/common/entity/compute_state_display.ts b/src/common/entity/compute_state_display.ts index d7e00f1e34..395c6372bb 100644 --- a/src/common/entity/compute_state_display.ts +++ b/src/common/entity/compute_state_display.ts @@ -4,7 +4,7 @@ import type { EntityRegistryDisplayEntry } from "../../data/entity_registry"; import type { FrontendLocaleData } from "../../data/translation"; import { TimeZone } from "../../data/translation"; import type { HomeAssistant } from "../../types"; -import { DURATION_UNITS, formatDuration } from "../datetime/duration"; +import { DURATION_UNITS, formatDuration } from "../datetime/format_duration"; import { formatDate } from "../datetime/format_date"; import { formatDateTime } from "../datetime/format_date_time"; import { formatTime } from "../datetime/format_time"; @@ -72,10 +72,10 @@ export const computeStateDisplayFromEntityAttributes = ( ) { try { return formatDuration( + locale, state, attributes.unit_of_measurement, - entity?.display_precision, - locale + entity?.display_precision ); } catch (_err) { // fallback to default diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts index 68a6ce3084..94c2946346 100644 --- a/src/data/automation_i18n.ts +++ b/src/data/automation_i18n.ts @@ -1,8 +1,8 @@ import type { HassConfig } from "home-assistant-js-websocket"; import { ensureArray } from "../common/array/ensure-array"; import { - formatNumericDuration, formatDurationLong, + formatNumericDuration, } from "../common/datetime/format_duration"; import { formatTime, @@ -12,6 +12,10 @@ import secondsToDuration from "../common/datetime/seconds_to_duration"; import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display"; import { computeStateName } from "../common/entity/compute_state_name"; import { isValidEntityId } from "../common/entity/valid_entity_id"; +import { + formatListWithAnds, + formatListWithOrs, +} from "../common/string/format-list"; import type { HomeAssistant } from "../types"; import type { Condition, ForDict, Trigger } from "./automation"; import type { DeviceCondition, DeviceTrigger } from "./device_automation"; @@ -21,10 +25,6 @@ import { } from "./device_automation"; import type { EntityRegistryEntry } from "./entity_registry"; import type { FrontendLocaleData } from "./translation"; -import { - formatListWithAnds, - formatListWithOrs, -} from "../common/string/format-list"; import { isTriggerList } from "./trigger"; const triggerTranslationBaseKey = diff --git a/src/data/entity_attributes.ts b/src/data/entity_attributes.ts index 75dde0e776..e72bf4cd58 100644 --- a/src/data/entity_attributes.ts +++ b/src/data/entity_attributes.ts @@ -1,4 +1,4 @@ -import { formatNumericDuration } from "../common/datetime/format_duration"; +import { formatDurationDigital } from "../common/datetime/format_duration"; import type { FrontendLocaleData } from "./translation"; export const STATE_ATTRIBUTES = [ @@ -99,7 +99,11 @@ export const DOMAIN_ATTRIBUTES_FORMATERS: Record< }, media_player: { volume_level: (value) => Math.round(value * 100).toString(), - media_duration: (value, locale) => - formatNumericDuration(locale, { seconds: value })!, + media_duration: (value, locale) => { + const hours = Math.floor(value / 3600); + const minutes = Math.floor((value % 3600) / 60); + const seconds = value % 60; + return formatDurationDigital(locale, { hours, minutes, seconds })!; + }, }, }; diff --git a/src/panels/lovelace/cards/hui-entity-card.ts b/src/panels/lovelace/cards/hui-entity-card.ts index 6e5b30e2b4..672a5336d1 100644 --- a/src/panels/lovelace/cards/hui-entity-card.ts +++ b/src/panels/lovelace/cards/hui-entity-card.ts @@ -171,7 +171,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { ` : this.hass.localize("state.default.unknown") - : isNumericState(stateObj) || this._config.unit + : (isNumericState(stateObj) || this._config.unit) && + stateObj.attributes.device_class !== "duration" ? formatNumber( stateObj.state, this.hass.locale, @@ -185,7 +186,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { ? html` ${this._config.unit || - (this._config.attribute + (this._config.attribute || + stateObj.attributes.device_class === "duration" ? "" : stateObj.attributes.unit_of_measurement)} diff --git a/test/common/datetime/duration.test.ts b/test/common/datetime/duration.test.ts deleted file mode 100644 index c0dc1b3c81..0000000000 --- a/test/common/datetime/duration.test.ts +++ /dev/null @@ -1,90 +0,0 @@ -import { assert, describe, it } from "vitest"; - -import { formatDuration } from "../../../src/common/datetime/duration"; -import type { FrontendLocaleData } from "../../../src/data/translation"; -import { - DateFormat, - FirstWeekday, - NumberFormat, - TimeFormat, - TimeZone, -} from "../../../src/data/translation"; - -const LOCALE: FrontendLocaleData = { - language: "en", - number_format: NumberFormat.language, - time_format: TimeFormat.am_pm, - date_format: DateFormat.language, - time_zone: TimeZone.local, - first_weekday: FirstWeekday.language, -}; - -describe("formatDuration", () => { - it("works", () => { - assert.strictEqual(formatDuration("0", "ms", undefined, LOCALE), "0ms"); - assert.strictEqual(formatDuration("1", "ms", undefined, LOCALE), "1ms"); - assert.strictEqual(formatDuration("10", "ms", undefined, LOCALE), "10ms"); - assert.strictEqual(formatDuration("100", "ms", undefined, LOCALE), "100ms"); - assert.strictEqual( - formatDuration("1000", "ms", undefined, LOCALE), - "1,000ms" - ); - assert.strictEqual( - formatDuration("1001", "ms", undefined, LOCALE), - "1,001ms" - ); - assert.strictEqual( - formatDuration("65000", "ms", undefined, LOCALE), - "65,000ms" - ); - - assert.strictEqual( - formatDuration("0.5", "s", undefined, LOCALE), - "0s 500ms" - ); - assert.strictEqual(formatDuration("1", "s", undefined, LOCALE), "1s"); - assert.strictEqual( - formatDuration("1.1", "s", undefined, LOCALE), - "1s 100ms" - ); - assert.strictEqual(formatDuration("65", "s", undefined, LOCALE), "65s"); - - assert.strictEqual( - formatDuration("0.25", "min", undefined, LOCALE), - "0m 15s" - ); - assert.strictEqual( - formatDuration("0.5", "min", undefined, LOCALE), - "0m 30s" - ); - assert.strictEqual(formatDuration("1", "min", undefined, LOCALE), "1m"); - assert.strictEqual(formatDuration("20", "min", undefined, LOCALE), "20m"); - assert.strictEqual( - formatDuration("95.5", "min", undefined, LOCALE), - "95m 30s" - ); - - assert.strictEqual( - formatDuration("0.25", "h", undefined, LOCALE), - "0h 15m" - ); - assert.strictEqual(formatDuration("0.5", "h", undefined, LOCALE), "0h 30m"); - assert.strictEqual(formatDuration("1", "h", undefined, LOCALE), "1h"); - assert.strictEqual(formatDuration("20", "h", undefined, LOCALE), "20h"); - assert.strictEqual( - formatDuration("95.5", "h", undefined, LOCALE), - "95h 30m" - ); - - assert.strictEqual(formatDuration("0", "d", undefined, LOCALE), "0d"); - assert.strictEqual(formatDuration("0.4", "d", undefined, LOCALE), "0d 9h"); - assert.strictEqual(formatDuration("1", "d", undefined, LOCALE), "1d"); - assert.strictEqual(formatDuration("20", "d", undefined, LOCALE), "20d"); - assert.strictEqual( - formatDuration("95.5", "d", undefined, LOCALE), - "95d 12h" - ); - assert.strictEqual(formatDuration("95.75", "d", 0, LOCALE), "96d"); - assert.strictEqual(formatDuration("95.75", "d", 2, LOCALE), "95d 18h"); - }); -}); diff --git a/test/common/datetime/format_duration.test.ts b/test/common/datetime/format_duration.test.ts new file mode 100644 index 0000000000..a713dee22c --- /dev/null +++ b/test/common/datetime/format_duration.test.ts @@ -0,0 +1,57 @@ +import { assert, describe, it } from "vitest"; + +import { formatDuration } from "../../../src/common/datetime/format_duration"; +import type { FrontendLocaleData } from "../../../src/data/translation"; +import { + DateFormat, + FirstWeekday, + NumberFormat, + TimeFormat, + TimeZone, +} from "../../../src/data/translation"; + +const LOCALE: FrontendLocaleData = { + language: "en", + number_format: NumberFormat.language, + time_format: TimeFormat.am_pm, + date_format: DateFormat.language, + time_zone: TimeZone.local, + first_weekday: FirstWeekday.language, +}; + +describe("formatDuration", () => { + it("works", () => { + assert.strictEqual(formatDuration(LOCALE, "0", "ms"), "0ms"); + assert.strictEqual(formatDuration(LOCALE, "1", "ms"), "1ms"); + assert.strictEqual(formatDuration(LOCALE, "10", "ms"), "10ms"); + assert.strictEqual(formatDuration(LOCALE, "100", "ms"), "100ms"); + assert.strictEqual(formatDuration(LOCALE, "1000", "ms"), "1,000ms"); + assert.strictEqual(formatDuration(LOCALE, "1001", "ms"), "1,001ms"); + assert.strictEqual(formatDuration(LOCALE, "65000", "ms"), "65,000ms"); + + assert.strictEqual(formatDuration(LOCALE, "0.5", "s"), "0s 500ms"); + assert.strictEqual(formatDuration(LOCALE, "1", "s"), "1s"); + assert.strictEqual(formatDuration(LOCALE, "1.1", "s"), "1s 100ms"); + assert.strictEqual(formatDuration(LOCALE, "65", "s"), "65s"); + + assert.strictEqual(formatDuration(LOCALE, "0.25", "min"), "0m 15s"); + assert.strictEqual(formatDuration(LOCALE, "0.5", "min"), "0m 30s"); + assert.strictEqual(formatDuration(LOCALE, "1", "min"), "1m"); + assert.strictEqual(formatDuration(LOCALE, "20", "min"), "20m"); + assert.strictEqual(formatDuration(LOCALE, "95.5", "min"), "95m 30s"); + + assert.strictEqual(formatDuration(LOCALE, "0.25", "h"), "0h 15m"); + assert.strictEqual(formatDuration(LOCALE, "0.5", "h"), "0h 30m"); + assert.strictEqual(formatDuration(LOCALE, "1", "h"), "1h"); + assert.strictEqual(formatDuration(LOCALE, "20", "h"), "20h"); + assert.strictEqual(formatDuration(LOCALE, "95.5", "h"), "95h 30m"); + + assert.strictEqual(formatDuration(LOCALE, "0", "d"), "0d"); + assert.strictEqual(formatDuration(LOCALE, "0.4", "d"), "0d 9h"); + assert.strictEqual(formatDuration(LOCALE, "1", "d"), "1d"); + assert.strictEqual(formatDuration(LOCALE, "20", "d"), "20d"); + assert.strictEqual(formatDuration(LOCALE, "95.5", "d"), "95d 12h"); + assert.strictEqual(formatDuration(LOCALE, "95.75", "d", 0), "96d"); + assert.strictEqual(formatDuration(LOCALE, "95.75", "d", 2), "95d 18h"); + }); +});