mirror of
https://github.com/home-assistant/frontend.git
synced 2025-11-09 02:49:51 +00:00
270 lines
8.2 KiB
TypeScript
270 lines
8.2 KiB
TypeScript
import { mdiAlert } from "@mdi/js";
|
|
import type { HassEntity } from "home-assistant-js-websocket";
|
|
import type { CSSResultGroup, PropertyValues } from "lit";
|
|
import { LitElement, css, html, nothing } from "lit";
|
|
import { property, state } from "lit/decorators";
|
|
import { ifDefined } from "lit/directives/if-defined";
|
|
import { styleMap } from "lit/directives/style-map";
|
|
import { computeDomain } from "../../common/entity/compute_domain";
|
|
import { computeStateDomain } from "../../common/entity/compute_state_domain";
|
|
import {
|
|
stateColorBrightness,
|
|
stateColorCss,
|
|
} from "../../common/entity/state_color";
|
|
import { iconColorCSS } from "../../common/style/icon_color_css";
|
|
import { cameraUrlWithWidthHeight } from "../../data/camera";
|
|
import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../data/climate";
|
|
import type { HomeAssistant } from "../../types";
|
|
import "../ha-state-icon";
|
|
|
|
export class StateBadge extends LitElement {
|
|
public hass?: HomeAssistant;
|
|
|
|
@property({ attribute: false }) public stateObj?: HassEntity;
|
|
|
|
@property({ attribute: false }) public overrideIcon?: string;
|
|
|
|
@property({ attribute: false }) public overrideImage?: string;
|
|
|
|
// Cannot be a boolean attribute because undefined is treated different than
|
|
// false. When it is undefined, state is still colored for light entities.
|
|
@property({ attribute: false }) public stateColor?: boolean;
|
|
|
|
@property() public color?: string;
|
|
|
|
// @todo Consider reworking to eliminate need for attribute since it is manipulated internally
|
|
@property({ type: Boolean, reflect: true }) public icon = true;
|
|
|
|
@state() private _iconStyle: Record<string, string | undefined> = {};
|
|
|
|
connectedCallback(): void {
|
|
super.connectedCallback();
|
|
if (
|
|
this.hasUpdated &&
|
|
this.overrideImage === undefined &&
|
|
(this.stateObj?.attributes.entity_picture ||
|
|
this.stateObj?.attributes.entity_picture_local)
|
|
) {
|
|
// Update image on connect, so we get new auth token
|
|
this.requestUpdate("stateObj");
|
|
}
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
super.disconnectedCallback();
|
|
if (
|
|
this.overrideImage === undefined &&
|
|
(this.stateObj?.attributes.entity_picture ||
|
|
this.stateObj?.attributes.entity_picture_local)
|
|
) {
|
|
// Clear image on disconnect so we don't fetch with old auth when we reconnect
|
|
this.style.backgroundImage = "";
|
|
}
|
|
}
|
|
|
|
private get _stateColor() {
|
|
const domain = this.stateObj
|
|
? computeStateDomain(this.stateObj)
|
|
: undefined;
|
|
return this.stateColor ?? domain === "light";
|
|
}
|
|
|
|
protected render() {
|
|
const stateObj = this.stateObj;
|
|
|
|
// We either need a `stateObj` or one override
|
|
if (!stateObj && !this.overrideIcon && !this.overrideImage) {
|
|
return html`<div class="missing">
|
|
<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>
|
|
</div>`;
|
|
}
|
|
|
|
const cls = this.getClass();
|
|
if (cls) {
|
|
cls.forEach((toSet, className) => {
|
|
if (!toSet) {
|
|
this.classList.remove(className);
|
|
} else {
|
|
this.classList.add(className);
|
|
}
|
|
});
|
|
}
|
|
|
|
if (!this.icon) {
|
|
return nothing;
|
|
}
|
|
|
|
const domain = stateObj ? computeStateDomain(stateObj) : undefined;
|
|
|
|
return html`<ha-state-icon
|
|
.hass=${this.hass}
|
|
style=${styleMap(this._iconStyle)}
|
|
data-domain=${ifDefined(domain)}
|
|
data-state=${ifDefined(stateObj?.state)}
|
|
.icon=${this.overrideIcon}
|
|
.stateObj=${stateObj}
|
|
></ha-state-icon>`;
|
|
}
|
|
|
|
public willUpdate(changedProps: PropertyValues<this>) {
|
|
super.willUpdate(changedProps);
|
|
if (
|
|
!changedProps.has("stateObj") &&
|
|
!changedProps.has("overrideImage") &&
|
|
!changedProps.has("overrideIcon") &&
|
|
!changedProps.has("stateColor") &&
|
|
!changedProps.has("color")
|
|
) {
|
|
return;
|
|
}
|
|
const stateObj = this.stateObj;
|
|
|
|
const iconStyle: Record<string, string> = {};
|
|
let backgroundImage = "";
|
|
|
|
this.icon = true;
|
|
|
|
if (stateObj) {
|
|
const domain = computeDomain(stateObj.entity_id);
|
|
if (this.overrideImage === undefined) {
|
|
// hide icon if we have entity picture
|
|
if (
|
|
(stateObj.attributes.entity_picture_local ||
|
|
stateObj.attributes.entity_picture) &&
|
|
!this.overrideIcon
|
|
) {
|
|
let imageUrl =
|
|
stateObj.attributes.entity_picture_local ||
|
|
stateObj.attributes.entity_picture;
|
|
if (this.hass) {
|
|
imageUrl = this.hass.hassUrl(imageUrl);
|
|
}
|
|
if (domain === "camera") {
|
|
imageUrl = cameraUrlWithWidthHeight(imageUrl, 80, 80);
|
|
}
|
|
backgroundImage = `url(${imageUrl})`;
|
|
this.icon = false;
|
|
} else if (this.color) {
|
|
// Externally provided overriding color wins over state color
|
|
iconStyle.color = this.color;
|
|
} else if (this._stateColor) {
|
|
const color = stateColorCss(stateObj);
|
|
if (color) {
|
|
iconStyle.color = color;
|
|
}
|
|
if (stateObj.attributes.rgb_color) {
|
|
iconStyle.color = `rgb(${stateObj.attributes.rgb_color.join(",")})`;
|
|
}
|
|
if (stateObj.attributes.brightness) {
|
|
const brightness = stateObj.attributes.brightness;
|
|
if (typeof brightness !== "number") {
|
|
const errorMessage = `Type error: state-badge expected number, but type of ${
|
|
stateObj.entity_id
|
|
}.attributes.brightness is ${typeof brightness} (${brightness})`;
|
|
// eslint-disable-next-line
|
|
console.warn(errorMessage);
|
|
}
|
|
iconStyle.filter = stateColorBrightness(stateObj);
|
|
}
|
|
if (stateObj.attributes.hvac_action) {
|
|
const hvacAction = stateObj.attributes.hvac_action;
|
|
if (hvacAction in CLIMATE_HVAC_ACTION_TO_MODE) {
|
|
iconStyle.color = stateColorCss(
|
|
stateObj,
|
|
CLIMATE_HVAC_ACTION_TO_MODE[hvacAction]
|
|
)!;
|
|
} else {
|
|
delete iconStyle.color;
|
|
}
|
|
}
|
|
}
|
|
} else if (this.overrideImage) {
|
|
let imageUrl = this.overrideImage;
|
|
if (this.hass) {
|
|
imageUrl = this.hass.hassUrl(imageUrl);
|
|
}
|
|
backgroundImage = `url(${imageUrl})`;
|
|
this.icon = false;
|
|
}
|
|
}
|
|
|
|
this._iconStyle = iconStyle;
|
|
this.style.backgroundImage = backgroundImage;
|
|
}
|
|
|
|
protected getClass() {
|
|
const cls = new Map(
|
|
["has-no-radius", "has-media-image", "has-image"].map((_cls) => [
|
|
_cls,
|
|
false,
|
|
])
|
|
);
|
|
if (this.stateObj) {
|
|
const domain = computeDomain(this.stateObj.entity_id);
|
|
if (domain === "update") {
|
|
cls.set("has-no-radius", true);
|
|
} else if (domain === "media_player" || domain === "camera") {
|
|
cls.set("has-media-image", true);
|
|
} else if (this.style.backgroundImage !== "") {
|
|
cls.set("has-image", true);
|
|
}
|
|
}
|
|
return cls;
|
|
}
|
|
|
|
static get styles(): CSSResultGroup {
|
|
return [
|
|
iconColorCSS,
|
|
css`
|
|
:host {
|
|
position: relative;
|
|
display: inline-flex;
|
|
width: 40px;
|
|
color: var(--state-icon-color);
|
|
border-radius: var(--state-badge-border-radius, 50%);
|
|
height: 40px;
|
|
background-size: cover;
|
|
box-sizing: border-box;
|
|
--state-inactive-color: initial;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
:host(.has-image) {
|
|
border-radius: var(--state-badge-with-image-border-radius, 50%);
|
|
}
|
|
:host(.has-media-image) {
|
|
border-radius: var(--state-badge-with-media-image-border-radius, 8%);
|
|
}
|
|
:host(.has-no-radius) {
|
|
border-radius: var(--ha-border-radius-square);
|
|
}
|
|
:host(:focus) {
|
|
outline: none;
|
|
}
|
|
:host(:not([icon]):focus) {
|
|
border: 2px solid var(--divider-color);
|
|
}
|
|
:host([icon]:focus) {
|
|
background: var(--divider-color);
|
|
}
|
|
ha-state-icon {
|
|
transition:
|
|
color 0.3s ease-in-out,
|
|
filter 0.3s ease-in-out;
|
|
}
|
|
.missing {
|
|
color: #fce588;
|
|
}
|
|
`,
|
|
];
|
|
}
|
|
}
|
|
|
|
declare global {
|
|
interface HTMLElementTagNameMap {
|
|
"state-badge": StateBadge;
|
|
}
|
|
}
|
|
|
|
customElements.define("state-badge", StateBadge);
|