lovelace-timer-bar-card
Version:
Progress bar display for Home Assistant timers
270 lines (248 loc) • 7.88 kB
text/typescript
/* eslint-disable @typescript-eslint/no-explicit-any */
import {
html,
CSSResultGroup,
TemplateResult,
css,
nothing,
PropertyValues,
} from "lit";
import { property } from "lit/decorators.js";
import { computeRTL, HomeAssistant } from "custom-card-helpers";
import {
HassEntity,
Mushroom,
TimerBarConfig,
TimerBarEntityConfig,
} from "./types";
import { createActionHandler, createHandleAction } from "./helpers-actions";
import { TimerBarEntityRow, fillConfig } from "./timer-bar-entity-row";
import {
defaultColorCss,
defaultDarkColorCss,
cardStyle,
themeColorCss,
themeVariables,
computeRgbColor,
computeInfoDisplay,
} from "./lib/mushroom";
const computeObjectId = (entityId: string): string =>
entityId.substring(entityId.indexOf(".") + 1);
const computeStateName = (stateObj: any): string =>
stateObj.attributes.friendly_name === undefined
? computeObjectId(stateObj.entity_id).replace(/_/g, " ")
: stateObj.attributes.friendly_name || "";
const computeDarkMode = (hass: HomeAssistant | undefined) =>
hass && !!(hass.themes as any).darkMode;
/** Style for outer timer bar card */
export function mushroomStyle(mushroom: Mushroom) {
if (mushroom.fill_container) return "height: 100%";
return;
}
export function fillMushroomConfig(
config: TimerBarEntityConfig,
mushroom: Mushroom,
): TimerBarConfig {
let color = "var(--rgb-state-entity)";
if (mushroom.icon_color) color = computeRgbColor(mushroom.icon_color);
if (mushroom.color) color = computeRgbColor(mushroom.color);
return {
...fillConfig(config),
bar_background: `rgba(${color}, 0.2)`,
bar_foreground: `rgb(${color})`,
bar_radius: "2px",
translations: config.translations,
...config,
};
}
export class TimerBarMushroomRow extends TimerBarEntityRow {
() public mushroom: Mushroom = {};
({ reflect: true, type: String })
public layout: string | undefined;
protected updated(changedProps: PropertyValues): void {
// This function comes from base-element.ts in mushroom cards
super.updated(changedProps);
if (changedProps.has("hass") && this.hass) {
const currentDarkMode = computeDarkMode(changedProps.get("hass"));
const newDarkMode = computeDarkMode(this.hass);
if (currentDarkMode !== newDarkMode) {
this.toggleAttribute("dark-mode", newDarkMode);
}
}
}
protected _renderRow(config: TimerBarConfig, contents: TemplateResult) {
if (!this.hass) return html``;
const warning = this._warning
? html`<hui-warning>${this._warning}</hui-warning>`
: "";
const rtl = computeRTL(this.hass);
const state = this.hass!.states[this.config.entity!];
const name = config.name ?? computeStateName(state);
if (this.modConfig.layout === "hide_name") config = { ...config, name: "" };
const appearance = this.appearance();
const primary = computeInfoDisplay(
appearance.primary_info,
name,
super._renderState(),
state,
this.hass,
);
return html` <ha-card
class=${appearance.fill_container ? "fill-container" : ""}
>
<mushroom-card ?rtl=${rtl} .appearance=${appearance}>
<mushroom-state-item
.appearance=${appearance}
?rtl=${rtl}
@action=${createHandleAction(this.hass, config)}
.actionHandler=${createActionHandler(config)}
>
${warning} ${this._renderIcon(state)} ${this._renderBadge(state)}
<div class="container" slot="info">
<span class="primary">${primary}</span>
<span class="secondary ${this.appearance().layout}"
>${contents}</span
>
</div>
</mushroom-state-item>
</mushroom-card>
${this._renderDebug()}
</ha-card>`;
}
protected _renderState() {
const state = this.hass!.states[this.config.entity!];
const name = this.config.name ?? computeStateName(state);
const appearance = this.appearance();
const stateStr = super._renderState();
return computeInfoDisplay(
appearance.secondary_info,
name,
stateStr,
state,
this.hass!,
) as TemplateResult;
}
protected _renderIcon(stateObj: HassEntity): TemplateResult {
const active = this._mode() == "active";
const icon = active ? this.config.active_icon ?? this.config.icon : this.config.icon;
let style = "";
if (this.mushroom.icon_color || this.mushroom.color) {
const iconRgbColor = computeRgbColor(
this.mushroom.color! || this.mushroom.icon_color!,
);
style += `--icon-color:rgb(${iconRgbColor});`;
style += `--shape-color:rgba(${iconRgbColor}, 0.2);`;
}
return html`<mushroom-shape-icon
slot="icon"
.disabled=${!active}
style=${style}
>
<ha-state-icon
.hass=${this.hass}
.stateObj=${stateObj}
.state=${stateObj}
.icon=${icon}
></ha-state-icon>
</mushroom-shape-icon>`;
}
protected _renderBadge(stateObj: HassEntity) {
return stateObj.state === "unavailable"
? html`<mushroom-badge-icon
class="unavailable"
slot="badge"
icon="mdi:help"
></mushroom-badge-icon>`
: nothing;
}
private appearance() {
// NOTE: Not all of these are used or implemented.
return {
layout: this.mushroom.layout ?? "default",
fill_container: this.mushroom.fill_container ?? false,
primary_info: this.mushroom.primary_info ?? "name",
secondary_info: this.mushroom.secondary_info ?? "state",
icon_type: this.mushroom.icon_type ?? "icon",
};
}
protected localize(value: string, state: HassEntity, _: boolean) {
return super.localize(value, state, false);
}
static get styles(): CSSResultGroup {
return [
super.styles,
css`
:host {
${defaultColorCss}
}
:host([dark-mode]) {
${defaultDarkColorCss}
}
:host {
${themeColorCss}
${themeVariables}
}
`,
cardStyle,
css`
:host([layout="grid"]) {
height: 100%;
}
mushroom-state-item {
cursor: pointer;
}
mushroom-shape-icon {
--icon-color: rgb(var(--rgb-state-entity));
--shape-color: rgba(var(--rgb-state-entity), 0.2);
}
.container {
min-width: 0;
flex: 1;
display: flex;
flex-direction: column;
}
.primary {
font-weight: var(--card-primary-font-weight);
font-size: var(--card-primary-font-size);
line-height: var(--card-primary-line-height);
color: var(--card-primary-color);
letter-spacing: var(--card-primary-letter-spacing);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
}
.secondary {
font-weight: var(--card-secondary-font-weight);
font-size: var(--card-secondary-font-size);
line-height: var(--card-secondary-line-height);
color: var(--card-secondary-color);
letter-spacing: var(--card-secondary-letter-spacing);
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
display: flex;
flex-direction: row-reverse;
justify-content: start;
align-items: center;
}
.secondary.vertical {
flex-direction: column-reverse;
justify-content: center;
}
.secondary.vertical > .text-content {
width: 100% ;
text-align: center;
}
.bar {
margin-top: 0;
}
.bar-container {
flex-grow: 1;
}
.text-content {
text-align: start;
}
`,
];
}
}