ha-frontend/src/panels/lovelace/cards/hui-glance-card.ts

358 lines
10 KiB
TypeScript

import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
TemplateResult,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { relativeTime } from "../../../common/datetime/relative_time";
import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element";
import { computeDomain } from "../../../common/entity/compute_domain";
import { computeStateDisplay } from "../../../common/entity/compute_state_display";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import {
ActionHandlerEvent,
CallServiceActionConfig,
MoreInfoActionConfig,
} from "../../../data/lovelace";
import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor";
import { HomeAssistant } from "../../../types";
import { actionHandler } from "../common/directives/action-handler-directive";
import { findEntities } from "../common/find-entities";
import { handleAction } from "../common/handle-action";
import { hasAction } from "../common/has-action";
import { processConfigEntities } from "../common/process-config-entities";
import "../components/hui-timestamp-display";
import { createEntityNotFoundWarning } from "../components/hui-warning";
import "../components/hui-warning-element";
import { LovelaceCard, LovelaceCardEditor } from "../types";
import { GlanceCardConfig, GlanceConfigEntity } from "./types";
@customElement("hui-glance-card")
export class HuiGlanceCard extends LitElement implements LovelaceCard {
public static async getConfigElement(): Promise<LovelaceCardEditor> {
await import("../editor/config-elements/hui-glance-card-editor");
return document.createElement("hui-glance-card-editor");
}
public static getStubConfig(
hass: HomeAssistant,
entities: string[],
entitiesFallback: string[]
): GlanceCardConfig {
const includeDomains = ["sensor"];
const maxEntities = 3;
const foundEntities = findEntities(
hass,
maxEntities,
entities,
entitiesFallback,
includeDomains
);
return { type: "glance", entities: foundEntities };
}
@property({ attribute: false }) public hass?: HomeAssistant;
@state() private _config?: GlanceCardConfig;
private _configEntities?: GlanceConfigEntity[];
public getCardSize(): number {
const rowHeight =
(this._config!.show_icon ? 1 : 0) +
(this._config!.show_name ? 1 : 0) +
(this._config!.show_state ? 1 : 0);
const numRows = Math.ceil(
this._configEntities!.length / (this._config!.columns || 5)
);
return (this._config!.title ? 2 : 0) + rowHeight * numRows;
}
public setConfig(config: GlanceCardConfig): void {
this._config = {
show_name: true,
show_state: true,
show_icon: true,
state_color: true,
...config,
};
const entities = processConfigEntities<GlanceConfigEntity>(
config.entities
).map((entityConf) => ({
hold_action: { action: "more-info" } as MoreInfoActionConfig,
...entityConf,
}));
for (const entity of entities) {
if (
(entity.tap_action &&
entity.tap_action.action === "call-service" &&
!entity.tap_action.service) ||
(entity.hold_action &&
entity.hold_action.action === "call-service" &&
!(entity.hold_action as CallServiceActionConfig).service)
) {
throw new Error(
'Missing required property "service" when tap_action or hold_action is call-service'
);
}
}
const columns = config.columns || Math.min(config.entities.length, 5);
this.style.setProperty("--glance-column-width", `${100 / columns}%`);
this._configEntities = entities;
if (this.hass) {
this.requestUpdate();
}
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
if (changedProps.has("_config")) {
return true;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
if (
!this._configEntities ||
!oldHass ||
oldHass.themes !== this.hass!.themes ||
oldHass.locale !== this.hass!.locale
) {
return true;
}
for (const entity of this._configEntities) {
if (oldHass.states[entity.entity] !== this.hass!.states[entity.entity]) {
return true;
}
}
return false;
}
protected render(): TemplateResult {
if (!this._config || !this.hass) {
return html``;
}
const { title } = this._config;
return html`
<ha-card .header="${title}">
<div class="${classMap({ entities: true, "no-header": !title })}">
${this._configEntities!.map((entityConf) =>
this.renderEntity(entityConf)
)}
</div>
</ha-card>
`;
}
protected updated(changedProps: PropertyValues): void {
super.updated(changedProps);
if (!this._config || !this.hass) {
return;
}
const oldHass = changedProps.get("hass") as HomeAssistant | undefined;
const oldConfig = changedProps.get("_config") as
| GlanceCardConfig
| undefined;
if (
!oldHass ||
!oldConfig ||
oldHass.themes !== this.hass.themes ||
oldConfig.theme !== this._config.theme
) {
applyThemesOnElement(this, this.hass.themes, this._config.theme);
}
}
static get styles(): CSSResultGroup {
return css`
ha-card {
height: 100%;
}
.entities {
display: flex;
padding: 0 16px 4px;
flex-wrap: wrap;
box-sizing: border-box;
align-items: center;
align-content: center;
}
.entities.no-header {
padding-top: 16px;
}
.entity {
box-sizing: border-box;
padding: 0 4px;
display: flex;
flex-direction: column;
align-items: center;
cursor: pointer;
margin-bottom: 12px;
width: var(--glance-column-width, 20%);
}
.entity:focus {
outline: none;
background: var(--divider-color);
border-radius: 14px;
padding: 4px;
margin: -4px 0;
}
.entity div {
width: 100%;
text-align: center;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.name {
min-height: var(--paper-font-body1_-_line-height, 20px);
}
.warning {
cursor: default;
position: relative;
padding: 8px;
width: calc(var(--glance-column-width, 20%) - 4px);
margin: 0 2px;
}
.warning::before {
position: absolute;
top: 0;
right: 0;
bottom: 0;
left: 0;
opacity: 0.12;
pointer-events: none;
content: "";
border-radius: 4px;
background-color: var(--warning-color);
}
state-badge {
margin: 8px 0;
}
hui-warning-element {
padding: 8px;
}
`;
}
private renderEntity(entityConf): TemplateResult {
const stateObj = this.hass!.states[entityConf.entity];
if (!stateObj) {
return html`<div class="entity warning">
${this._config!.show_name
? html`
<div class="name">
${createEntityNotFoundWarning(this.hass!, entityConf.entity)}
</div>
`
: ""}
${this._config!.show_icon
? html` <hui-warning-element
.label=${createEntityNotFoundWarning(
this.hass!,
entityConf.entity
)}
></hui-warning-element>`
: ""}
<div>${this._config!.show_state ? entityConf.entity : ""}</div>
</div>`;
}
return html`
<div
class="entity"
.config="${entityConf}"
@action=${this._handleAction}
.actionHandler=${actionHandler({
hasHold: hasAction(entityConf.hold_action),
hasDoubleClick: hasAction(entityConf.double_tap_action),
})}
tabindex=${ifDefined(
hasAction(entityConf.tap_action) ? "0" : undefined
)}
>
${this._config!.show_name
? html`
<div class="name">
${"name" in entityConf
? entityConf.name
: computeStateName(stateObj)}
</div>
`
: ""}
${this._config!.show_icon
? html`
<state-badge
.hass=${this.hass}
.stateObj=${stateObj}
.overrideIcon=${entityConf.icon}
.overrideImage=${entityConf.image}
.stateColor=${(entityConf.state_color === false ||
entityConf.state_color) ??
this._config!.state_color}
></state-badge>
`
: ""}
${this._config!.show_state && entityConf.show_state !== false
? html`
<div>
${computeDomain(entityConf.entity) === "sensor" &&
stateObj.attributes.device_class ===
SENSOR_DEVICE_CLASS_TIMESTAMP &&
!UNAVAILABLE_STATES.includes(stateObj.state)
? html`
<hui-timestamp-display
.hass=${this.hass}
.ts=${new Date(stateObj.state)}
.format=${entityConf.format}
></hui-timestamp-display>
`
: entityConf.show_last_changed
? relativeTime(
new Date(stateObj.last_changed),
this.hass!.locale
)
: computeStateDisplay(
this.hass!.localize,
stateObj,
this.hass!.locale
)}
</div>
`
: ""}
</div>
`;
}
private _handleAction(ev: ActionHandlerEvent) {
const config = (ev.currentTarget as any).config as GlanceConfigEntity;
handleAction(this, this.hass!, config, ev.detail.action!);
}
}
declare global {
interface HTMLElementTagNameMap {
"hui-glance-card": HuiGlanceCard;
}
}