1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-20 02:38:53 +00:00
Files
frontend/src/components/entity/ha-state-label-badge.ts

318 lines
8.7 KiB
TypeScript

import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import type { PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { arrayLiteralIncludes } from "../../common/array/literal-includes";
import secondsToDuration from "../../common/datetime/seconds_to_duration";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states";
import {
formatNumber,
getNumberFormatOptions,
isNumericState,
} from "../../common/number/format_number";
import {
isUnavailableState,
UNAVAILABLE,
UNKNOWN,
} from "../../data/entity/entity";
import type { EntityRegistryDisplayEntry } from "../../data/entity/entity_registry";
import { timerTimeRemaining } from "../../data/timer";
import type { HomeAssistant } from "../../types";
import "../ha-label-badge";
import "../ha-state-icon";
// Define the domains whose states have special truncated strings
const TRUNCATED_DOMAINS = [
"alarm_control_panel",
"device_tracker",
"person",
] as const satisfies readonly (keyof typeof FIXED_DOMAIN_STATES)[];
type TruncatedDomain = (typeof TRUNCATED_DOMAINS)[number];
type TruncatedKey = {
[T in TruncatedDomain]: `${T}.${(typeof FIXED_DOMAIN_STATES)[T][number]}`;
}[TruncatedDomain];
const getTruncatedKey = (domainKey: string, stateKey: string) => {
if (
arrayLiteralIncludes(TRUNCATED_DOMAINS)(domainKey) &&
arrayLiteralIncludes(FIXED_DOMAIN_STATES[domainKey])(stateKey)
) {
return `${domainKey}.${stateKey}` as TruncatedKey;
}
return null;
};
@customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement {
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public state?: HassEntity;
@property() public name?: string;
@property() public icon?: string;
@property() public image?: string;
@property({ attribute: "show-name", type: Boolean }) public showName = false;
@state() private _timerTimeRemaining?: number;
private _connected?: boolean;
private _updateRemaining?: number;
public connectedCallback(): void {
super.connectedCallback();
this._connected = true;
this._startInterval(this.state);
}
public disconnectedCallback(): void {
super.disconnectedCallback();
this._connected = false;
this._clearInterval();
}
protected render(): TemplateResult {
const entityState = this.state;
if (!entityState) {
return html`
<ha-label-badge
class="warning"
label=${this.hass!.localize("state_badge.default.error")}
description=${this.hass!.localize(
"state_badge.default.entity_not_found"
)}
>
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
</ha-label-badge>
`;
}
// Rendering priority inside badge:
// 1. Icon directly defined in badge config
// 2. Image directly defined in badge config
// 3. Image taken from entity picture
// 4. Icon determined via entity state
// 5. Value string as fallback
const domain = computeStateDomain(entityState);
const entry = this.hass?.entities[entityState.entity_id];
const showIcon =
this.icon || this._computeShowIcon(domain, entityState, entry);
const image = this.icon
? ""
: this.image
? this.image
: entityState.attributes.entity_picture_local ||
entityState.attributes.entity_picture;
const value =
!image && !showIcon
? this._computeValue(domain, entityState, entry)
: undefined;
return html`
<ha-label-badge
class=${classMap({
[domain]: true,
"has-unit_of_measurement":
"unit_of_measurement" in entityState.attributes,
})}
.image=${image}
.label=${this._computeLabel(
domain,
entityState,
this._timerTimeRemaining
)}
.description=${this.showName
? (this.name ?? computeStateName(entityState))
: undefined}
>
${!image && showIcon
? html`<ha-state-icon
.icon=${this.icon}
.stateObj=${entityState}
.hass=${this.hass}
></ha-state-icon>`
: ""}
${value && !image && !showIcon
? html`<span class=${value && value.length > 4 ? "big" : ""}
>${value}</span
>`
: ""}
</ha-label-badge>
`;
}
protected updated(changedProperties: PropertyValues): void {
super.updated(changedProperties);
if (this._connected && changedProperties.has("state")) {
this._startInterval(this.state);
}
}
private _computeValue(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryDisplayEntry
) {
switch (domain) {
case "alarm_control_panel":
case "binary_sensor":
case "device_tracker":
case "person":
case "scene":
case "sun":
case "timer":
return null;
// @ts-expect-error we don't break and go to default
case "sensor":
if (entry?.platform === "moon") {
return null;
}
// eslint-disable-next-line: disable=no-fallthrough
default:
return entityState.state === UNKNOWN ||
entityState.state === UNAVAILABLE
? "—"
: isNumericState(entityState)
? formatNumber(
entityState.state,
this.hass!.locale,
getNumberFormatOptions(entityState, entry)
)
: this.hass!.formatEntityState(entityState);
}
}
private _computeShowIcon(
domain: string,
entityState: HassEntity,
entry?: EntityRegistryDisplayEntry
): boolean {
if (entityState.state === UNAVAILABLE) {
return false;
}
switch (domain) {
case "alarm_control_panel":
case "binary_sensor":
case "device_tracker":
case "person":
case "scene":
case "sun":
return true;
case "timer":
return true;
case "sensor":
return entry?.platform === "moon";
default:
return false;
}
}
private _computeLabel(
domain: string,
entityState: HassEntity,
_timerTimeRemaining = 0
) {
// For unavailable states or certain domains, use a special translation that is truncated to fit within the badge label
if (isUnavailableState(entityState.state)) {
return this.hass!.localize(`state_badge.default.${entityState.state}`);
}
const domainStateKey = getTruncatedKey(domain, entityState.state);
if (domainStateKey) {
return this.hass!.localize(`state_badge.${domainStateKey}`);
}
// Person and device tracker state can be zone name
if (domain === "person" || domain === "device_tracker") {
return entityState.state;
}
if (domain === "timer") {
return secondsToDuration(_timerTimeRemaining);
}
return entityState.attributes.unit_of_measurement || null;
}
private _clearInterval() {
if (this._updateRemaining) {
clearInterval(this._updateRemaining);
this._updateRemaining = undefined;
}
}
private _startInterval(stateObj) {
this._clearInterval();
if (stateObj && computeStateDomain(stateObj) === "timer") {
this._calculateTimerRemaining(stateObj);
if (stateObj.state === "active") {
this._updateRemaining = window.setInterval(
() => this._calculateTimerRemaining(this.state),
1000
);
}
}
}
private _calculateTimerRemaining(stateObj) {
this._timerTimeRemaining = timerTimeRemaining(stateObj);
}
static styles = css`
:host {
cursor: pointer;
}
.big {
font-size: var(--ha-font-size-xs);
}
ha-label-badge {
--ha-label-badge-color: var(--label-badge-red);
}
ha-label-badge.has-unit_of_measurement {
--ha-label-badge-label-text-transform: none;
}
ha-label-badge.binary_sensor {
--ha-label-badge-color: var(--label-badge-blue);
}
.red {
--ha-label-badge-color: var(--label-badge-red);
}
.blue {
--ha-label-badge-color: var(--label-badge-blue);
}
.green {
--ha-label-badge-color: var(--label-badge-green);
}
.yellow {
--ha-label-badge-color: var(--label-badge-yellow);
}
.grey {
--ha-label-badge-color: var(--label-badge-grey);
}
.warning {
--ha-label-badge-color: var(--label-badge-yellow);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-state-label-badge": HaStateLabelBadge;
}
}