Skip to content

Commit 9be239a

Browse files
committed
Replace outdated functionality from custom-cards with updated equivalents
1 parent 9d5c145 commit 9be239a

12 files changed

+315
-12
lines changed

package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,8 @@
1717
"license": "MIT",
1818
"dependencies": {
1919
"custom-card-helpers": "^1.8.0",
20-
"lit": "^2.0.2"
20+
"lit": "^2.0.2",
21+
"memoize-one": "^6.0.0"
2122
},
2223
"devDependencies": {
2324
"@babel/core": "^7.16.5",

src/entity.js

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1-
import { computeEntity, computeStateDisplay, formatNumber, secondsToDuration } from 'custom-card-helpers';
1+
import { secondsToDuration } from './lib/seconds_to_duration';
2+
import { formatNumber } from './lib/format_number';
3+
import { computeStateDisplay } from './lib/compute_state_display';
24
import { isObject, isUnavailable } from './util';
35

46
export const checkEntity = (config) => {
@@ -11,6 +13,8 @@ export const checkEntity = (config) => {
1113
}
1214
};
1315

16+
export const computeEntity = (entityId) => entityId.substr(entityId.indexOf('.') + 1);
17+
1418
export const entityName = (stateObj, config) => {
1519
if (config.name === false) return null;
1620
return (

src/lib/compute_state_display.js

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
// Source: https://github.com/home-assistant/frontend/blob/dev/src/common/entity/compute_state_display.ts
2+
3+
import { UNAVAILABLE, UNKNOWN } from "./constants";
4+
import { formatDate } from './format_date';
5+
import { formatDateTime } from './format_date_time';
6+
import { formatTime } from './format_time';
7+
import { formatNumber, isNumericState } from './format_number';
8+
9+
export const computeStateDomain = (stateObj) => stateObj.entity_id.substr(0, stateObj.entity_id.indexOf('.'));
10+
11+
export const computeStateDisplay = (localize, stateObj, locale, state) => {
12+
const compareState = state !== undefined ? state : stateObj.state;
13+
14+
if (compareState === UNKNOWN || compareState === UNAVAILABLE) {
15+
return localize(`state.default.${compareState}`);
16+
}
17+
18+
// Entities with a `unit_of_measurement` or `state_class` are numeric values and should use `formatNumber`
19+
if (isNumericState(stateObj)) {
20+
if (stateObj.attributes.device_class === 'monetary') {
21+
try {
22+
return formatNumber(compareState, locale, {
23+
style: 'currency',
24+
currency: stateObj.attributes.unit_of_measurement,
25+
});
26+
} catch (_err) {
27+
// fallback to default
28+
}
29+
}
30+
return `${formatNumber(compareState, locale)}${
31+
stateObj.attributes.unit_of_measurement ? ' ' + stateObj.attributes.unit_of_measurement : ''
32+
}`;
33+
}
34+
35+
const domain = computeStateDomain(stateObj);
36+
37+
if (domain === 'input_datetime') {
38+
if (state !== undefined) {
39+
// If trying to display an explicit state, need to parse the explict state to `Date` then format.
40+
// Attributes aren't available, we have to use `state`.
41+
try {
42+
const components = state.split(' ');
43+
if (components.length === 2) {
44+
// Date and time.
45+
return formatDateTime(new Date(components.join('T')), locale);
46+
}
47+
if (components.length === 1) {
48+
if (state.includes('-')) {
49+
// Date only.
50+
return formatDate(new Date(`${state}T00:00`), locale);
51+
}
52+
if (state.includes(':')) {
53+
// Time only.
54+
const now = new Date();
55+
return formatTime(new Date(`${now.toISOString().split('T')[0]}T${state}`), locale);
56+
}
57+
}
58+
return state;
59+
} catch (_e) {
60+
// Formatting methods may throw error if date parsing doesn't go well,
61+
// just return the state string in that case.
62+
return state;
63+
}
64+
} else {
65+
// If not trying to display an explicit state, create `Date` object from `stateObj`'s attributes then format.
66+
let date;
67+
if (stateObj.attributes.has_date && stateObj.attributes.has_time) {
68+
date = new Date(
69+
stateObj.attributes.year,
70+
stateObj.attributes.month - 1,
71+
stateObj.attributes.day,
72+
stateObj.attributes.hour,
73+
stateObj.attributes.minute
74+
);
75+
return formatDateTime(date, locale);
76+
}
77+
if (stateObj.attributes.has_date) {
78+
date = new Date(stateObj.attributes.year, stateObj.attributes.month - 1, stateObj.attributes.day);
79+
return formatDate(date, locale);
80+
}
81+
if (stateObj.attributes.has_time) {
82+
date = new Date();
83+
date.setHours(stateObj.attributes.hour, stateObj.attributes.minute);
84+
return formatTime(date, locale);
85+
}
86+
return stateObj.state;
87+
}
88+
}
89+
90+
if (domain === 'humidifier') {
91+
if (compareState === 'on' && stateObj.attributes.humidity) {
92+
return `${stateObj.attributes.humidity} %`;
93+
}
94+
}
95+
96+
// `counter` `number` and `input_number` domains do not have a unit of measurement but should still use `formatNumber`
97+
if (domain === 'counter' || domain === 'number' || domain === 'input_number') {
98+
return formatNumber(compareState, locale);
99+
}
100+
101+
// state of button is a timestamp
102+
if (domain === 'button' || (domain === 'sensor' && stateObj.attributes.device_class === 'timestamp')) {
103+
return formatDateTime(new Date(compareState), locale);
104+
}
105+
106+
return (
107+
// Return device class translation
108+
(stateObj.attributes.device_class &&
109+
localize(`component.${domain}.state.${stateObj.attributes.device_class}.${compareState}`)) ||
110+
// Return default translation
111+
localize(`component.${domain}.state._.${compareState}`) ||
112+
// We don't know! Return the raw state.
113+
compareState
114+
);
115+
};

src/lib/constants.js

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
// Source:
2+
// https://github.com/home-assistant/frontend/blob/dev/src/data/entity.ts
3+
// https://github.com/home-assistant/frontend/blob/dev/src/data/translation.ts
4+
// https://github.com/home-assistant/frontend/blob/dev/src/panels/lovelace/cards/types.ts
5+
6+
export const UNAVAILABLE = 'unavailable';
7+
export const UNKNOWN = 'unknown';
8+
export const UNAVAILABLE_STATES = [UNAVAILABLE, UNKNOWN];
9+
10+
export const SECONDARY_INFO_VALUES = [
11+
'entity-id',
12+
'last-changed',
13+
'last-updated',
14+
'last-triggered',
15+
'position',
16+
'tilt-position',
17+
'brightness',
18+
];
19+
20+
export const NumberFormat = {
21+
language: 'language',
22+
system: 'system',
23+
comma_decimal: 'comma_decimal',
24+
decimal_comma: 'decimal_comma',
25+
space_comma: 'space_comma',
26+
none: 'none',
27+
};
28+
29+
export const TimeFormat = {
30+
language: 'language',
31+
system: 'system',
32+
am_pm: '12',
33+
twenty_four: '24',
34+
};

src/lib/format_date.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Source: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/format_date.ts
2+
3+
import memoizeOne from 'memoize-one';
4+
5+
export const formatDate = (dateObj, locale) => formatDateMem(locale).format(dateObj);
6+
7+
const formatDateMem = memoizeOne(
8+
(locale) =>
9+
new Intl.DateTimeFormat(locale.language, {
10+
year: 'numeric',
11+
month: 'long',
12+
day: 'numeric',
13+
})
14+
);

src/lib/format_date_time.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// Source: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/format_date_time.ts
2+
3+
import memoizeOne from 'memoize-one';
4+
import { useAmPm } from "./use_am_pm";
5+
6+
export const formatDateTime = (dateObj, locale) => formatDateTimeMem(locale).format(dateObj);
7+
8+
const formatDateTimeMem = memoizeOne(
9+
(locale) =>
10+
new Intl.DateTimeFormat(locale.language, {
11+
year: 'numeric',
12+
month: 'long',
13+
day: 'numeric',
14+
hour: useAmPm(locale) ? 'numeric' : '2-digit',
15+
minute: '2-digit',
16+
hour12: useAmPm(locale),
17+
})
18+
);

src/lib/format_number.js

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
// Source: https://github.com/home-assistant/frontend/blob/dev/src/common/number/format_number.ts
2+
3+
import { NumberFormat } from './constants';
4+
5+
export const round = (value, precision = 2) => Math.round(value * 10 ** precision) / 10 ** precision;
6+
7+
export const isNumericState = (stateObj) =>
8+
!!stateObj.attributes.unit_of_measurement || !!stateObj.attributes.state_class;
9+
10+
export const numberFormatToLocale = (localeOptions) => {
11+
switch (localeOptions.number_format) {
12+
case NumberFormat.comma_decimal:
13+
return ['en-US', 'en']; // Use United States with fallback to English formatting 1,234,567.89
14+
case NumberFormat.decimal_comma:
15+
return ['de', 'es', 'it']; // Use German with fallback to Spanish then Italian formatting 1.234.567,89
16+
case NumberFormat.space_comma:
17+
return ['fr', 'sv', 'cs']; // Use French with fallback to Swedish and Czech formatting 1 234 567,89
18+
case NumberFormat.system:
19+
return undefined;
20+
default:
21+
return localeOptions.language;
22+
}
23+
};
24+
25+
export const formatNumber = (num, localeOptions, options) => {
26+
const locale = localeOptions ? numberFormatToLocale(localeOptions) : undefined;
27+
28+
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
29+
Number.isNaN =
30+
Number.isNaN ||
31+
function isNaN(input) {
32+
return typeof input === 'number' && isNaN(input);
33+
};
34+
35+
if (localeOptions?.number_format !== NumberFormat.none && !Number.isNaN(Number(num)) && Intl) {
36+
try {
37+
return new Intl.NumberFormat(locale, getDefaultFormatOptions(num, options)).format(Number(num));
38+
} catch (err) {
39+
// Don't fail when using "TEST" language
40+
// eslint-disable-next-line no-console
41+
console.error(err);
42+
return new Intl.NumberFormat(undefined, getDefaultFormatOptions(num, options)).format(Number(num));
43+
}
44+
}
45+
if (typeof num === 'string') {
46+
return num;
47+
}
48+
return `${round(num, options?.maximumFractionDigits).toString()}${
49+
options?.style === 'currency' ? ` ${options.currency}` : ''
50+
}`;
51+
};
52+
53+
const getDefaultFormatOptions = (num, options) => {
54+
const defaultOptions = {
55+
maximumFractionDigits: 2,
56+
...options,
57+
};
58+
59+
if (typeof num !== 'string') {
60+
return defaultOptions;
61+
}
62+
63+
// Keep decimal trailing zeros if they are present in a string numeric value
64+
if (!options || (!options.minimumFractionDigits && !options.maximumFractionDigits)) {
65+
const digits = num.indexOf('.') > -1 ? num.split('.')[1].length : 0;
66+
defaultOptions.minimumFractionDigits = digits;
67+
defaultOptions.maximumFractionDigits = digits;
68+
}
69+
70+
return defaultOptions;
71+
};

src/lib/format_time.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
// Source: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/format_time.ts
2+
3+
import memoizeOne from 'memoize-one';
4+
import { useAmPm } from "./use_am_pm";
5+
6+
export const formatTime = (dateObj, locale) => formatTimeMem(locale).format(dateObj);
7+
8+
const formatTimeMem = memoizeOne(
9+
(locale) =>
10+
new Intl.DateTimeFormat(locale.language, {
11+
hour: 'numeric',
12+
minute: '2-digit',
13+
hour12: useAmPm(locale),
14+
})
15+
);

src/lib/seconds_to_duration.js

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
// Source: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/seconds_to_duration.ts
2+
3+
const leftPad = (num) => (num < 10 ? `0${num}` : num);
4+
5+
export function secondsToDuration(d) {
6+
const h = Math.floor(d / 3600);
7+
const m = Math.floor((d % 3600) / 60);
8+
const s = Math.floor((d % 3600) % 60);
9+
10+
if (h > 0) {
11+
return `${h}:${leftPad(m)}:${leftPad(s)}`;
12+
}
13+
if (m > 0) {
14+
return `${m}:${leftPad(s)}`;
15+
}
16+
if (s > 0) {
17+
return '' + s;
18+
}
19+
return null;
20+
}

src/lib/use_am_pm.js

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
// Source: https://github.com/home-assistant/frontend/blob/dev/src/common/datetime/use_am_pm.ts
2+
3+
import memoizeOne from 'memoize-one';
4+
import { TimeFormat } from './constants'
5+
6+
export const useAmPm = memoizeOne((locale) => {
7+
if (locale.time_format === TimeFormat.language || locale.time_format === TimeFormat.system) {
8+
const testLanguage = locale.time_format === TimeFormat.language ? locale.language : undefined;
9+
const test = new Date().toLocaleString(testLanguage);
10+
return test.includes('AM') || test.includes('PM');
11+
}
12+
13+
return locale.time_format === TimeFormat.am_pm;
14+
});

src/util.js

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,8 @@
1-
const SECONDARY_INFO_VALUES = [
2-
'entity-id',
3-
'last-changed',
4-
'last-updated',
5-
'last-triggered',
6-
'position',
7-
'tilt-position',
8-
'brightness',
9-
];
1+
import { SECONDARY_INFO_VALUES, UNAVAILABLE_STATES } from './lib/constants';
102

113
export const isObject = (obj) => typeof obj === 'object' && !Array.isArray(obj) && !!obj;
124

13-
export const isUnavailable = (stateObj) => !stateObj || ['unknown', 'unavailable'].includes(stateObj.state);
5+
export const isUnavailable = (stateObj) => !stateObj || UNAVAILABLE_STATES.includes(stateObj.state);
146

157
export const hideUnavailable = (stateObj, config) =>
168
config.hide_unavailable &&

yarn.lock

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2260,6 +2260,11 @@ make-dir@^3.0.2, make-dir@^3.1.0:
22602260
dependencies:
22612261
semver "^6.0.0"
22622262

2263+
memoize-one@^6.0.0:
2264+
version "6.0.0"
2265+
resolved "https://registry.yarnpkg.com/memoize-one/-/memoize-one-6.0.0.tgz#b2591b871ed82948aee4727dc6abceeeac8c1045"
2266+
integrity sha512-rkpe71W0N0c0Xz6QD0eJETuWAJGnJ9afsl1srmwPrI+yBCkge5EycXXbYRyvL29zZVUWQCY7InPRCv3GDXuZNw==
2267+
22632268
merge-stream@^2.0.0:
22642269
version "2.0.0"
22652270
resolved "https://registry.yarnpkg.com/merge-stream/-/merge-stream-2.0.0.tgz#52823629a14dd00c9770fb6ad47dc6310f2c1f60"

0 commit comments

Comments
 (0)