diff --git a/package.json b/package.json index c7fdde0..56c4e24 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "minimalistic-area-card", - "version": "1.1.16", + "version": "1.2.0", "description": "Minimalistic Area Card for Home Assistant", "keywords": [ "home-assistant", @@ -15,7 +15,7 @@ "author": "Marcos Junior ", "license": "MIT", "dependencies": { - "@lit-labs/scoped-registry-mixin": "^1.0.1", + "@lit-labs/scoped-registry-mixin": "^1.0.3", "@material/mwc-formfield": "^0.27.0", "@material/mwc-list": "^0.27.0", "@material/mwc-menu": "^0.27.0", @@ -25,8 +25,8 @@ "@material/mwc-switch": "^0.27.0", "@material/mwc-textfield": "^0.27.0", "custom-card-helpers": "^1.9.0", - "home-assistant-js-websocket": "^8.0.1", - "lit": "^2.7.0" + "home-assistant-js-websocket": "^9.3.0", + "lit": "^3.1.3" }, "devDependencies": { "@babel/core": "^7.21.3", diff --git a/src/minimalistic-area-card.ts b/src/minimalistic-area-card.ts index 9a857f7..ebf5b1f 100644 --- a/src/minimalistic-area-card.ts +++ b/src/minimalistic-area-card.ts @@ -1,14 +1,18 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { ActionHandlerEvent, computeStateDisplay, EntitiesCardEntityConfig, - handleAction, hasAction, hasConfigOrEntityChanged, NavigateActionConfig + FrontendLocaleData, + handleAction, hasAction, hasConfigOrEntityChanged, NavigateActionConfig, + NumberFormat, + numberFormatToLocale, + round } from 'custom-card-helpers'; // This is a community maintained npm module with common helper functions/types. https://github.com/custom-cards/custom-card-helpers import { css, html, LitElement } from 'lit'; import { classMap } from 'lit/directives/class-map'; import { ifDefined } from "lit/directives/if-defined"; import { actionHandler } from './action-handler-directive'; import { findEntities } from './find-entities'; -import { cardType, HomeAssistantArea, HomeAssistantExt, MinimalisticAreaCardConfig, STATES_OFF, UNAVAILABLE } from './types'; +import { cardType, EntityRegistryDisplayEntry, HomeAssistantArea, HomeAssistantExt, MinimalisticAreaCardConfig, STATES_OFF, UNAVAILABLE } from './types'; import { HassEntity } from 'home-assistant-js-websocket'; import { version as pkgVersion } from "../package.json"; @@ -224,6 +228,7 @@ class MinimalisticAreaCard extends LitElement { isSensor: boolean ) { const stateObj = this.hass.states[entityConf.entity]; + const entity = this.hass.entities[entityConf.entity] as EntityRegistryDisplayEntry; entityConf = { tap_action: { action: dialog ? "more-info" : "toggle" }, @@ -271,7 +276,7 @@ class MinimalisticAreaCard extends LitElement { ${stateObj.attributes[entityConf.attribute]} ${entityConf.suffix} ` - : this.computeStateValue(stateObj)} + : this.computeStateValue(stateObj, entity)} ` : null} @@ -283,16 +288,19 @@ class MinimalisticAreaCard extends LitElement { !!stateObj.attributes.state_class; } - computeStateValue(stateObj: HassEntity) { + computeStateValue(stateObj: HassEntity, entity?: EntityRegistryDisplayEntry) { const [domain, _] = stateObj.entity_id.split("."); if (this.isNumericState(stateObj)) { const value = Number(stateObj.state); if (isNaN(value)) return null; - else - return `${value}${stateObj.attributes.unit_of_measurement + else { + const opt = this.getNumberFormatOptions(stateObj, entity); + const str = this.formatNumber(value, this.hass.locale, opt); + return `${str}${stateObj.attributes.unit_of_measurement ? " " + stateObj.attributes.unit_of_measurement : ""}`; + } } else if (domain !== "binary_sensor" && stateObj.state !== "unavailable" && stateObj.state !== "idle") { return stateObj.state; @@ -302,6 +310,105 @@ class MinimalisticAreaCard extends LitElement { } } + /** + * Checks if the current entity state should be formatted as an integer based on the `state` and `step` attribute and returns the appropriate `Intl.NumberFormatOptions` object with `maximumFractionDigits` set + * @param entityState The state object of the entity + * @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined` + */ + getNumberFormatOptions( + entityState: HassEntity, + entity?: EntityRegistryDisplayEntry + ): Intl.NumberFormatOptions | undefined { + const precision = entity?.display_precision; + if (precision != null) { + return { + maximumFractionDigits: precision, + minimumFractionDigits: precision, + }; + } + if ( + Number.isInteger(Number(entityState.attributes?.step)) && + Number.isInteger(Number(entityState.state)) + ) { + return { maximumFractionDigits: 0 }; + } + return undefined; + } + + + /** + * Formats a number based on the user's preference with thousands separator(s) and decimal character for better legibility. + * + * @param num The number to format + * @param localeOptions The user-selected language and formatting, from `hass.locale` + * @param options Intl.NumberFormatOptions to use + */ + formatNumber( + num: string | number, + localeOptions?: FrontendLocaleData, + options?: Intl.NumberFormatOptions + ): string { + const locale = localeOptions ? numberFormatToLocale(localeOptions) : undefined; + + // Polyfill for Number.isNaN, which is more reliable than the global isNaN() + Number.isNaN = + Number.isNaN || + function isNaN(input) { + return typeof input === "number" && isNaN(input); + }; + + if (localeOptions?.number_format !== NumberFormat.none && !Number.isNaN(Number(num)) && Intl) { + try { + return new Intl.NumberFormat(locale, this.getDefaultFormatOptions(num, options)).format( + Number(num) + ); + } catch (err: any) { + // Don't fail when using "TEST" language + // eslint-disable-next-line no-console + console.error(err); + return new Intl.NumberFormat(undefined, this.getDefaultFormatOptions(num, options)).format( + Number(num) + ); + } + } + if (typeof num === "string") { + return num; + } + return `${round(num, options?.maximumFractionDigits).toString()}${options?.style === "currency" ? ` ${options.currency}` : "" + }`; + } + + /** + * Generates default options for Intl.NumberFormat + * @param num The number to be formatted + * @param options The Intl.NumberFormatOptions that should be included in the returned options + */ + getDefaultFormatOptions( + num: string | number, + options?: Intl.NumberFormatOptions + ): Intl.NumberFormatOptions { + const defaultOptions: Intl.NumberFormatOptions = { + maximumFractionDigits: 2, + ...options, + }; + + if (typeof num !== "string") { + return defaultOptions; + } + + // Keep decimal trailing zeros if they are present in a string numeric value + if ( + !options || + (options.minimumFractionDigits === undefined && options.maximumFractionDigits === undefined) + ) { + const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0; + defaultOptions.minimumFractionDigits = digits; + defaultOptions.maximumFractionDigits = digits; + } + + return defaultOptions; + } + shouldUpdate(changedProps) { if (hasConfigOrEntityChanged(this, changedProps, false)) { return true; diff --git a/src/types.ts b/src/types.ts index 322198c..750224f 100644 --- a/src/types.ts +++ b/src/types.ts @@ -31,6 +31,18 @@ export interface HomeAssistantArea { name: string } +export interface EntityRegistryDisplayEntry { + entity_id: string; + name?: string; + device_id?: string; + area_id?: string; + hidden?: boolean; + entity_category?: "config" | "diagnostic"; + translation_key?: string; + platform?: string; + display_precision?: number; +} + export const UNAVAILABLE = "unavailable"; export const STATES_OFF = [...STATES_OFF_HELPER, UNAVAILABLE, "idle" , "disconnected"]; diff --git a/yarn.lock b/yarn.lock index 8b31d4d..3947bce 100644 --- a/yarn.lock +++ b/yarn.lock @@ -421,20 +421,32 @@ "@jridgewell/resolve-uri" "3.1.0" "@jridgewell/sourcemap-codec" "1.4.14" -"@lit-labs/scoped-registry-mixin@^1.0.1": - version "1.0.1" - resolved "https://registry.yarnpkg.com/@lit-labs/scoped-registry-mixin/-/scoped-registry-mixin-1.0.1.tgz#0ad266c029f1eb385711d2cd26252baadf145a5d" - integrity sha512-7aKnBKb5izcTjICO4VdeQLRYpZoB8FepJjG7TmE2oe0cU8cb4MUA4NFCjze+vbE+wP/55xv4dAMFOuJN9EG26w== +"@lit-labs/scoped-registry-mixin@^1.0.3": + version "1.0.3" + resolved "https://registry.yarnpkg.com/@lit-labs/scoped-registry-mixin/-/scoped-registry-mixin-1.0.3.tgz#fb8d711c4c351527fc80d751b2aa2b48faa78cc1" + integrity sha512-+DywdIJIIR30K0UGdWkn3iPlVvuSfmvo4qUlPTz1hEizksPD4Vl8TrwuXwi4HTIQ2XugYK3WXaWfWqqLEzbrxg== dependencies: - "@lit/reactive-element" "^1.0.0" - lit "^2.0.0" + "@lit/reactive-element" "^1.0.0 || ^2.0.0" + lit "^2.0.0 || ^3.0.0" "@lit-labs/ssr-dom-shim@^1.0.0", "@lit-labs/ssr-dom-shim@^1.1.0": version "1.1.0" resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.1.0.tgz#3361d6b8c4cb2ac426d5794ac7cd9776cd2f0814" integrity sha512-92uQ5ARf7UXYrzaFcAX3T2rTvaS9Z1//ukV+DqjACM4c8s0ZBQd7ayJU5Dh2AFLD/Ayuyz4uMmxQec8q3U4Ong== -"@lit/reactive-element@^1.0.0", "@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0": +"@lit-labs/ssr-dom-shim@^1.2.0": + version "1.2.0" + resolved "https://registry.yarnpkg.com/@lit-labs/ssr-dom-shim/-/ssr-dom-shim-1.2.0.tgz#353ce4a76c83fadec272ea5674ede767650762fd" + integrity sha512-yWJKmpGE6lUURKAaIltoPIE/wrbY3TEkqQt+X0m+7fQNnAv0keydnYvbiJFP1PnMhizmIWRWOG5KLhYyc/xl+g== + +"@lit/reactive-element@^1.0.0 || ^2.0.0", "@lit/reactive-element@^2.0.4": + version "2.0.4" + resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-2.0.4.tgz#8f2ed950a848016383894a26180ff06c56ae001b" + integrity sha512-GFn91inaUa2oHLak8awSIigYz0cU0Payr1rcFsrkf5OJ5eSPxElyZfKh0f2p9FsTiZWXQdWGJeXZICEfXXYSXQ== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.2.0" + +"@lit/reactive-element@^1.3.0", "@lit/reactive-element@^1.6.0": version "1.6.1" resolved "https://registry.yarnpkg.com/@lit/reactive-element/-/reactive-element-1.6.1.tgz#0d958b6d479d0e3db5fc1132ecc4fa84be3f0b93" integrity sha512-va15kYZr7KZNNPZdxONGQzpUr+4sxVu7V/VG7a8mRfPPXUyhEYj5RzXCQmGrlP3tAh0L3HHm5AjBMFYRqlM9SA== @@ -1904,10 +1916,10 @@ home-assistant-js-websocket@^6.0.1: resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-6.1.1.tgz#87ba846753c4fb58a2e5ace6bb15a82689fd0735" integrity sha512-TnZFzF4mn5F/v0XKUTK2GMQXrn/+eQpgaSDSELl6U0HSwSbFwRhGWLz330YT+hiKMspDflamsye//RPL+zwhDw== -home-assistant-js-websocket@^8.0.1: - version "8.0.1" - resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-8.0.1.tgz#4c91f6e740600de9a59e0da3f018ea3487568415" - integrity sha512-CVi1yu4+hj3m3+ZxgzEYiP+UYFmjf/iAsUKVyCmhVm9T8Pn7ZCeW16O3pniC4h1DOmTuXW+z4v373UwKxVOMAg== +home-assistant-js-websocket@^9.3.0: + version "9.3.0" + resolved "https://registry.yarnpkg.com/home-assistant-js-websocket/-/home-assistant-js-websocket-9.3.0.tgz#ca2cd395b47fbcd1cf9f3948c2e805e0afc566ed" + integrity sha512-DB2jCo57+0OIOhF3zMjHcfZnRyzbWBdeT24LwTh4pO75vaIiXbdaWwZFEFukHOz3K8qTFmeUnLemHV3Fspib+A== ignore@^5.2.0: version "5.2.4" @@ -2179,6 +2191,15 @@ lit-element@^3.3.0: "@lit/reactive-element" "^1.3.0" lit-html "^2.7.0" +lit-element@^4.0.4: + version "4.0.5" + resolved "https://registry.yarnpkg.com/lit-element/-/lit-element-4.0.5.tgz#f20cd8a6231eaf5358f7a6877ca6ea7628fa2015" + integrity sha512-iTWskWZEtn9SyEf4aBG6rKT8GABZMrTWop1+jopsEOgEcugcXJGKuX5bEbkq9qfzY+XB4MAgCaSPwnNpdsNQ3Q== + dependencies: + "@lit-labs/ssr-dom-shim" "^1.2.0" + "@lit/reactive-element" "^2.0.4" + lit-html "^3.1.2" + lit-html@^2.7.0: version "2.7.0" resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-2.7.0.tgz#b244457d0f8c4782a50e83b2c6f3611347ef775d" @@ -2186,7 +2207,14 @@ lit-html@^2.7.0: dependencies: "@types/trusted-types" "^2.0.2" -lit@^2.0.0, lit@^2.1.1, lit@^2.7.0: +lit-html@^3.1.2: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lit-html/-/lit-html-3.1.3.tgz#ae2e9fee0258d0a1b5d7b86c87da51117e4f911b" + integrity sha512-FwIbqDD8O/8lM4vUZ4KvQZjPPNx7V1VhT7vmRB8RBAO0AU6wuTVdoXiu2CivVjEGdugvcbPNBLtPE1y0ifplHA== + dependencies: + "@types/trusted-types" "^2.0.2" + +lit@^2.0.0, lit@^2.1.1: version "2.7.0" resolved "https://registry.yarnpkg.com/lit/-/lit-2.7.0.tgz#94242caa20f7b9e60d49cc0b843e4a694c4af3bb" integrity sha512-qSy2BAVA+OiWtNptP404egcC/izDdNRw6iHGIbUmkZtbMJvPKfNsaoKrNs8Zmsbjmv5ZX2tur1l9TfzkSWWT4g== @@ -2195,6 +2223,15 @@ lit@^2.0.0, lit@^2.1.1, lit@^2.7.0: lit-element "^3.3.0" lit-html "^2.7.0" +"lit@^2.0.0 || ^3.0.0", lit@^3.1.3: + version "3.1.3" + resolved "https://registry.yarnpkg.com/lit/-/lit-3.1.3.tgz#809ecdaccfea47e1e3b46649fae6c6e7b9802675" + integrity sha512-l4slfspEsnCcHVRTvaP7YnkTZEZggNFywLEIhQaGhYDczG+tu/vlgm/KaWIEjIp+ZyV20r2JnZctMb8LeLCG7Q== + dependencies: + "@lit/reactive-element" "^2.0.4" + lit-element "^4.0.4" + lit-html "^3.1.2" + locate-path@^5.0.0: version "5.0.0" resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"