Add support for icon translations (#19406)

* Add support for icon translations

* Use slot for icons

* Update more-info-climate.ts

* Review and fixes

* Update entity-registry-settings-editor.ts
This commit is contained in:
Bram Kragten 2024-01-19 14:55:02 +01:00 committed by GitHub
parent b969144f50
commit 081636b3e7
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 479 additions and 87 deletions

View File

@ -344,6 +344,7 @@ export class DemoEntityState extends LitElement {
title: "Icon",
template: (entry) => html`
<state-badge
.hass=${hass}
.stateObj=${entry.stateObj}
.stateColor=${true}
></state-badge>

View File

@ -0,0 +1,35 @@
/** Return an icon representing a attribute. */
import { HassEntity } from "home-assistant-js-websocket";
import {
computeFanModeIcon,
computeHvacModeIcon,
computePresetModeIcon,
computeSwingModeIcon,
} from "../../data/climate";
import { computeDomain } from "./compute_domain";
const iconGenerators: Record<string, Record<string, (value: any) => string>> = {
climate: {
fan_mode: computeFanModeIcon,
hvac_mode: computeHvacModeIcon,
preset_mode: computePresetModeIcon,
swing_mode: computeSwingModeIcon,
},
};
export const attributeIconPath = (
state: HassEntity | undefined,
attribute: string,
attributeValue?: string
) => {
if (!state) {
return undefined;
}
const domain = computeDomain(state.entity_id);
if (iconGenerators[domain]?.[attribute]) {
return iconGenerators[domain]?.[attribute](
attributeValue || state.attributes[attribute]
);
}
return undefined;
};

View File

@ -4,7 +4,7 @@ import { DEFAULT_DOMAIN_ICON } from "../const";
import { computeDomain } from "./compute_domain";
import { domainIcon } from "./domain_icon";
export const stateIconPath = (state?: HassEntity) => {
export const stateIconPath = (state: HassEntity | undefined) => {
if (!state) {
return DEFAULT_DOMAIN_ICON;
}

View File

@ -25,16 +25,6 @@ interface HassEntityWithCachedName extends HassEntity, ScorableTextItem {
export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
// eslint-disable-next-line lit/prefer-static-styles
const rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (item) =>
html`<ha-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge slot="graphic" .stateObj=${item}></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</ha-list-item>`;
@customElement("ha-entity-picker")
export class HaEntityPicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@ -127,6 +117,21 @@ export class HaEntityPicker extends LitElement {
private _states: HassEntityWithCachedName[] = [];
private _rowRenderer: ComboBoxLitRenderer<HassEntityWithCachedName> = (
item
) =>
html`<ha-list-item graphic="avatar" .twoline=${!!item.entity_id}>
${item.state
? html`<state-badge
slot="graphic"
.stateObj=${item}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.friendly_name}</span>
<span slot="secondary">${item.entity_id}</span>
</ha-list-item>`;
private _getStates = memoizeOne(
(
_opened: boolean,
@ -326,7 +331,7 @@ export class HaEntityPicker extends LitElement {
.helper=${this.helper}
.allowCustomValue=${this.allowCustomEntity}
.filteredItems=${this._states}
.renderer=${rowRenderer}
.renderer=${this._rowRenderer}
.required=${this.required}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged}

View File

@ -140,7 +140,8 @@ export class HaStateLabelBadge extends LitElement {
${!image && showIcon
? html`<ha-state-icon
.icon=${this.icon}
.state=${entityState}
.stateObj=${entityState}
.hass=${this.hass}
></ha-state-icon>`
: ""}
${value && !image && !showIcon

View File

@ -105,6 +105,7 @@ export class HaStatisticPicker extends LitElement {
? html`<state-badge
slot="graphic"
.stateObj=${item.state}
.hass=${this.hass}
></state-badge>`
: ""}
<span>${item.name}</span>

View File

@ -1,11 +1,11 @@
import { mdiAlert } from "@mdi/js";
import type { HassEntity } from "home-assistant-js-websocket";
import {
css,
CSSResultGroup,
html,
LitElement,
PropertyValues,
css,
html,
nothing,
} from "lit";
import { property, state } from "lit/decorators";
@ -14,8 +14,8 @@ import { styleMap } from "lit/directives/style-map";
import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import {
stateColorCss,
stateColorBrightness,
stateColorCss,
} from "../../common/entity/state_color";
import { iconColorCSS } from "../../common/style/icon_color_css";
import { cameraUrlWithWidthHeight } from "../../data/camera";
@ -90,11 +90,12 @@ export class StateBadge extends LitElement {
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}
.state=${stateObj}
.stateObj=${stateObj}
></ha-state-icon>`;
}

View File

@ -29,6 +29,7 @@ class StateInfo extends LitElement {
const name = computeStateName(this.stateObj);
return html`<state-badge
.hass=${this.hass}
.stateObj=${this.stateObj}
.stateColor=${true}
.color=${this.color}

View File

@ -0,0 +1,68 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { attributeIcon } from "../data/icons";
import { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
import { attributeIconPath } from "../common/entity/attribute_icon_path";
@customElement("ha-attribute-icon")
export class HaAttributeIcon extends LitElement {
@property() public hass!: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property() public attribute?: string;
@property() public attributeValue?: string;
@property() public icon?: string;
protected render() {
if (this.icon) {
return html`<ha-icon .icon=${this.icon}></ha-icon>`;
}
if (!this.stateObj || !this.attribute) {
return nothing;
}
if (!this.hass) {
return this._renderFallback();
}
const icon = attributeIcon(
this.hass,
this.stateObj,
this.attribute,
this.attributeValue
).then((icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
});
return html`${until(icon)}`;
}
private _renderFallback() {
return html`
<ha-svg-icon
.path=${attributeIconPath(
this.stateObj!,
this.attribute!,
this.attributeValue!
)}
></ha-svg-icon>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-attribute-icon": HaAttributeIcon;
}
}

View File

@ -126,9 +126,9 @@ export class HaControlSelectMenu extends SelectBase {
return html`
<div class="icon">
${icon && "path" in icon
${icon && icon.localName === "ha-svg-icon" && "path" in icon
? html`<ha-svg-icon .path=${icon.path}></ha-svg-icon>`
: icon && "icon" in icon
: icon && icon.localName === "ha-icon" && "icon" in icon
? html`<ha-icon .path=${icon.icon}></ha-icon>`
: html`<slot name="icon"></slot>`}
</div>

View File

@ -246,7 +246,8 @@ export class HaRelatedItems extends LitElement {
graphic="icon"
>
<ha-state-icon
.state=${entity}
.hass=${this.hass}
.stateObj=${entity}
slot="graphic"
></ha-state-icon>
${entity.attributes.friendly_name || entity.entity_id}
@ -270,7 +271,8 @@ export class HaRelatedItems extends LitElement {
graphic="icon"
>
<ha-state-icon
.state=${group}
.hass=${this.hass}
.stateObj=${group}
slot="graphic"
></ha-state-icon>
${group.attributes.friendly_name || group.entity_id}
@ -294,7 +296,8 @@ export class HaRelatedItems extends LitElement {
graphic="icon"
>
<ha-state-icon
.state=${scene}
.hass=${this.hass}
.stateObj=${scene}
slot="graphic"
></ha-state-icon>
${scene.attributes.friendly_name || scene.entity_id}
@ -349,7 +352,8 @@ export class HaRelatedItems extends LitElement {
graphic="icon"
>
<ha-state-icon
.state=${automation}
.hass=${this.hass}
.stateObj=${automation}
slot="graphic"
></ha-state-icon>
${automation.attributes.friendly_name ||
@ -403,7 +407,8 @@ export class HaRelatedItems extends LitElement {
graphic="icon"
>
<ha-state-icon
.state=${script}
.hass=${this.hass}
.stateObj=${script}
slot="graphic"
></ha-state-icon>
${script.attributes.friendly_name || script.entity_id}

View File

@ -1,8 +1,10 @@
import { html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event";
import { computeDomain } from "../../common/entity/compute_domain";
import { domainIcon } from "../../common/entity/domain_icon";
import { entityIcon } from "../../data/icons";
import { IconSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-icon-picker";
@ -33,7 +35,10 @@ export class HaIconSelector extends LitElement {
const stateObj = iconEntity ? this.hass.states[iconEntity] : undefined;
const placeholder =
this.selector.icon?.placeholder || stateObj?.attributes.icon;
this.selector.icon?.placeholder ||
stateObj?.attributes.icon ||
(stateObj && until(entityIcon(this.hass, stateObj)));
const fallbackPath =
!placeholder && stateObj
? domainIcon(computeDomain(iconEntity!), stateObj)

View File

@ -1,25 +1,49 @@
import { HassEntity } from "home-assistant-js-websocket";
import { html, LitElement, TemplateResult } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
import { stateIconPath } from "../common/entity/state_icon_path";
import { entityIcon } from "../data/icons";
import { HomeAssistant } from "../types";
import "./ha-icon";
import "./ha-svg-icon";
@customElement("ha-state-icon")
export class HaStateIcon extends LitElement {
@property({ attribute: false }) public state?: HassEntity;
@property({ attribute: false }) public hass?: HomeAssistant;
@property({ attribute: false }) public stateObj?: HassEntity;
@property() public icon?: string;
protected render(): TemplateResult {
if (this.icon || this.state?.attributes.icon) {
protected render() {
if (this.icon || this.stateObj?.attributes.icon) {
return html`<ha-icon
.icon=${this.icon || this.state?.attributes.icon}
.icon=${this.icon || this.stateObj?.attributes.icon}
></ha-icon>`;
}
return html`<ha-svg-icon .path=${stateIconPath(this.state)}></ha-svg-icon>`;
if (!this.stateObj) {
return nothing;
}
if (!this.hass) {
return this._renderFallback();
}
const icon = entityIcon(this.hass, this.stateObj).then((icn) => {
if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`;
}
return this._renderFallback();
});
return html`${until(icon)}`;
}
private _renderFallback() {
return html`<ha-svg-icon
.path=${stateIconPath(this.stateObj)}
></ha-svg-icon>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-state-icon": HaStateIcon;

View File

@ -224,7 +224,8 @@ export class HaTargetPicker extends LitElement {
${entityState
? html`<ha-state-icon
class="mdc-chip__icon mdc-chip__icon--leading"
.state=${entityState}
.hass=${this.hass}
.stateObj=${entityState}
></ha-state-icon>`
: ""}
<span role="gridcell">

View File

@ -3,6 +3,8 @@ import { HassConfig } from "home-assistant-js-websocket";
import { HomeAssistant } from "../types";
import { EntityRegistryEntry } from "./entity_registry";
export const connectionContext =
createContext<HomeAssistant["connection"]>("connection");
export const statesContext = createContext<HomeAssistant["states"]>("states");
export const entitiesContext =
createContext<HomeAssistant["entities"]>("entities");

133
src/data/icons.ts Normal file
View File

@ -0,0 +1,133 @@
import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "../common/entity/compute_state_domain";
import { HomeAssistant } from "../types";
const resources: Record<IconCategory, any> = {
entity: {},
entity_component: undefined,
};
interface IconResources {
resources: Record<string, string | Record<string, string>>;
}
interface PlatformIcons {
[domain: string]: {
[translation_key: string]: {
state: Record<string, string>;
state_attributes: Record<string, { state: Record<string, string> }>;
default: string;
};
};
}
interface ComponentIcons {
[device_class: string]: {
state: Record<string, string>;
state_attributes: Record<string, { state: Record<string, string> }>;
default: string;
};
}
export type IconCategory = "entity" | "entity_component";
export const getHassIcons = async (
hass: HomeAssistant,
category: IconCategory,
integration?: string
): Promise<IconResources> =>
hass.callWS<{ resources: Record<string, string> }>({
type: "frontend/get_icons",
category,
integration,
});
export const getPlatformIcons = async (
hass: HomeAssistant,
integration: string,
force = false
): Promise<PlatformIcons> => {
if (!force && integration && integration in resources.entity) {
return resources.entity[integration];
}
const result = getHassIcons(hass, "entity", integration);
resources.entity[integration] = result.then(
(res) => res?.resources[integration]
);
return resources.entity[integration];
};
export const getComponentIcons = async (
hass: HomeAssistant,
domain: string,
force = false
): Promise<ComponentIcons> => {
if (!force && resources.entity_component) {
return resources.entity_component.then((res) => res[domain]);
}
resources.entity_component = getHassIcons(hass, "entity_component").then(
(result) => result.resources
);
return resources.entity_component.then((res) => res[domain]);
};
export const entityIcon = async (hass: HomeAssistant, state: HassEntity) => {
let icon: string | undefined;
const domain = computeStateDomain(state);
const entity = hass.entities?.[state.entity_id];
if (entity?.translation_key && entity.platform) {
const platformIcons = await getPlatformIcons(hass, entity.platform);
if (platformIcons) {
icon =
platformIcons[domain]?.[entity.translation_key]?.state?.[state.state] ||
platformIcons[domain]?.[entity.translation_key]?.default;
}
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(hass, domain);
if (entityComponentIcons) {
icon =
entityComponentIcons[state.attributes.device_class || "_"]?.state?.[
state.state
] ||
entityComponentIcons._?.state?.[state.state] ||
entityComponentIcons[state.attributes.device_class || "_"]?.default ||
entityComponentIcons._?.default;
}
}
return icon;
};
export const attributeIcon = async (
hass: HomeAssistant,
state: HassEntity,
attribute: string,
attributeValue?: string
) => {
let icon: string | undefined;
const domain = computeStateDomain(state);
const entity = hass.entities?.[state.entity_id];
if (entity?.translation_key && entity.platform) {
const platformIcons = await getPlatformIcons(hass, entity.platform);
if (platformIcons) {
icon =
platformIcons[domain]?.[entity.translation_key]?.state_attributes?.[
attribute
]?.state?.[attributeValue || state.attributes[attribute]];
}
}
if (!icon) {
const entityComponentIcons = await getComponentIcons(hass, domain);
if (entityComponentIcons) {
icon =
entityComponentIcons[state.attributes.device_class || "_"]
.state_attributes?.[attribute]?.state?.[
attributeValue || state.attributes[attribute]
] ||
entityComponentIcons._.state_attributes?.[attribute]?.state?.[
attributeValue || state.attributes[attribute]
];
}
}
return icon;
};

View File

@ -17,16 +17,13 @@ import "../../../components/ha-icon-button-toggle";
import "../../../components/ha-list-item";
import "../../../components/ha-select";
import "../../../components/ha-switch";
import "../../../components/ha-attribute-icon";
import {
ClimateEntity,
ClimateEntityFeature,
compareClimateHvacModes,
computeFanModeIcon,
computeHvacModeIcon,
computePresetModeIcon,
computeSwingModeIcon,
} from "../../../data/climate";
import { UNAVAILABLE } from "../../../data/entity";
import { UNAVAILABLE, isUnavailableState } from "../../../data/entity";
import "../../../state-control/climate/ha-state-control-climate-humidity";
import "../../../state-control/climate/ha-state-control-climate-temperature";
import { HomeAssistant } from "../../../types";
@ -164,17 +161,31 @@ class MoreInfoClimate extends LitElement {
@selected=${this._handleOperationModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiThermostat}></ha-svg-icon>
${!isUnavailableState(this.stateObj.state)
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="hvac_mode"
.attributeValue=${stateObj.state}
></ha-attribute-icon>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiThermostat}
></ha-svg-icon>`}
${stateObj.attributes.hvac_modes
.concat()
.sort(compareClimateHvacModes)
.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
<ha-attribute-icon
slot="graphic"
.path=${computeHvacModeIcon(mode)}
></ha-svg-icon>
.hass=${this.hass}
.stateObj=${stateObj}
attribute="hvac_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item>
`
@ -194,14 +205,30 @@ class MoreInfoClimate extends LitElement {
@selected=${this._handlePresetmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiTuneVariant}></ha-svg-icon>
${stateObj.attributes.preset_mode
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${stateObj.attributes.preset_mode}
></ha-attribute-icon>`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
`}
${stateObj.attributes.preset_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
<ha-attribute-icon
slot="graphic"
.path=${computePresetModeIcon(mode)}
></ha-svg-icon>
.hass=${this.hass}
.stateObj=${stateObj}
attribute="preset_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"preset_mode",
@ -227,14 +254,27 @@ class MoreInfoClimate extends LitElement {
@selected=${this._handleFanModeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
${stateObj.attributes.fan_mode
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${stateObj.attributes.fan_mode}
></ha-attribute-icon>`
: html`
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
`}
${stateObj.attributes.fan_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
<ha-attribute-icon
slot="graphic"
.path=${computeFanModeIcon(mode)}
></ha-svg-icon>
.hass=${this.hass}
.stateObj=${stateObj}
attribute="fan_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"fan_mode",
@ -260,17 +300,30 @@ class MoreInfoClimate extends LitElement {
@selected=${this._handleSwingmodeChanged}
@closed=${stopPropagation}
>
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
${stateObj.attributes.swing_mode
? html`<ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${stateObj.attributes.swing_mode}
></ha-attribute-icon>`
: html`
<ha-svg-icon
slot="icon"
.path=${mdiArrowOscillating}
></ha-svg-icon>
`}
${stateObj.attributes.swing_modes!.map(
(mode) => html`
<ha-list-item .value=${mode} graphic="icon">
<ha-svg-icon
<ha-attribute-icon
slot="graphic"
.path=${computeSwingModeIcon(mode)}
></ha-svg-icon>
.hass=${this.hass}
.stateObj=${stateObj}
attribute="swing_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue(
stateObj,
"swing_mode",

View File

@ -3,16 +3,16 @@ import { customElement, state } from "lit/decorators";
import { isNavigationClick } from "../common/dom/is-navigation-click";
import { navigate } from "../common/navigate";
import { getStorageDefaultPanelUrlPath } from "../data/panel";
import { WindowWithPreloads } from "../data/preloads";
import { getRecorderInfo, RecorderInfo } from "../data/recorder";
import "../resources/custom-card-support";
import { HassElement } from "../state/hass-element";
import QuickBarMixin from "../state/quick-bar-mixin";
import { HomeAssistant, Route } from "../types";
import { WindowWithPreloads } from "../data/preloads";
import { storeState } from "../util/ha-pref-storage";
import {
renderLaunchScreenInfoBox,
removeLaunchScreen,
renderLaunchScreenInfoBox,
} from "../util/launch-screen";
import {
registerServiceWorker,

View File

@ -112,7 +112,11 @@ class PanelCalendar extends LitElement {
.value=${selCal.entity_id}
.selected=${!this._deSelectedCalendars.includes(selCal.entity_id)}
>
<ha-state-icon slot="graphic" .state=${selCal}></ha-state-icon>
<ha-state-icon
slot="graphic"
.hass=${this.hass}
.stateObj=${selCal}
></ha-state-icon>
${selCal.name}
</ha-check-list-item>
`

View File

@ -123,7 +123,8 @@ class HaAutomationPicker extends LitElement {
type: "icon",
template: (automation) =>
html`<ha-state-icon
.state=${automation}
.hass=${this.hass}
.stateObj=${automation}
style=${styleMap({
color:
automation.state === UNAVAILABLE

View File

@ -95,6 +95,7 @@ class HaConfigUpdates extends SubscribeMixin(LitElement) {
slot="graphic"
.title=${entity.attributes.title ||
entity.attributes.friendly_name}
.hass=${this.hass}
.stateObj=${entity}
class=${ifDefined(
this.narrow && entity.attributes.in_progress

View File

@ -12,7 +12,6 @@ import { computeDomain } from "../../../../common/entity/compute_domain";
import { computeStateName } from "../../../../common/entity/compute_state_name";
import { domainIcon } from "../../../../common/entity/domain_icon";
import { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-card";
import "../../../../components/ha-icon";
import "../../../../components/ha-list-item";

View File

@ -84,7 +84,10 @@ export class EnergyDeviceSettings extends LitElement {
const entityState = this.hass.states[device.stat_consumption];
return html`
<div class="row">
<ha-state-icon .state=${entityState}></ha-state-icon>
<ha-state-icon
.hass=${this.hass}
.stateObj=${entityState}
></ha-state-icon>
<span class="content"
>${getStatisticLabel(
this.hass,

View File

@ -11,6 +11,7 @@ import {
PropertyValues,
} from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import memoizeOne from "memoize-one";
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
import { fireEvent } from "../../../common/dom/fire_event";
@ -65,6 +66,7 @@ import {
subscribeEntityRegistry,
updateEntityRegistryEntry,
} from "../../../data/entity_registry";
import { entityIcon } from "../../../data/icons";
import { domainToName } from "../../../data/integration";
import { getNumberDeviceClassConvertibleUnits } from "../../../data/number";
import {
@ -379,7 +381,8 @@ export class EntityRegistrySettingsEditor extends LitElement {
"ui.dialogs.entity_registry.editor.icon"
)}
.placeholder=${this.entry.original_icon ||
stateObj?.attributes.icon}
stateObj?.attributes.icon ||
(stateObj && until(entityIcon(this.hass, stateObj)))}
.fallbackPath=${!this._icon &&
!stateObj?.attributes.icon &&
stateObj

View File

@ -212,7 +212,8 @@ export class HaConfigEntities extends LitElement {
<ha-state-icon
title=${ifDefined(entry.entity?.state)}
slot="item-icon"
.state=${entry.entity}
.hass=${this.hass}
.stateObj=${entry.entity}
></ha-state-icon>
`,
},

View File

@ -129,7 +129,10 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
type: "icon",
template: (helper) =>
helper.entity
? html`<ha-state-icon .state=${helper.entity}></ha-state-icon>`
? html`<ha-state-icon
.hass=${this.hass}
.stateObj=${helper.entity}
></ha-state-icon>`
: html`<ha-svg-icon
.path=${helper.icon}
style="color: var(--error-color)"

View File

@ -91,6 +91,7 @@ class ZHADeviceCard extends SubscribeMixin(LitElement) {
<state-badge
@click=${this._openMoreInfo}
.title=${entity.stateName!}
.hass=${this.hass}
.stateObj=${this.hass!.states[entity.entity_id]}
slot="item-icon"
></state-badge>

View File

@ -104,7 +104,10 @@ class HaSceneDashboard extends LitElement {
),
type: "icon",
template: (scene) => html`
<ha-state-icon .state=${scene}></ha-state-icon>
<ha-state-icon
.hass=${this.hass}
.stateObj=${scene}
></ha-state-icon>
`,
},
name: {

View File

@ -349,6 +349,7 @@ export class HaSceneEditor extends SubscribeMixin(
@click=${this._showMoreInfo}
>
<state-badge
.hass=${this.hass}
.stateObj=${entityStateObj}
slot="graphic"
></state-badge>
@ -416,6 +417,7 @@ export class HaSceneEditor extends SubscribeMixin(
@click=${this._showMoreInfo}
>
<state-badge
.hass=${this.hass}
.stateObj=${entityStateObj}
slot="graphic"
></state-badge>

View File

@ -119,7 +119,8 @@ class HaScriptPicker extends LitElement {
type: "icon",
template: (script) =>
html`<ha-state-icon
.state=${script}
.hass=${this.hass}
.stateObj=${script}
style=${styleMap({
color:
script.state === UNAVAILABLE ? "var(--error-color)" : "unset",

View File

@ -153,7 +153,8 @@ class DialogExposeEntity extends LitElement {
<ha-state-icon
title=${ifDefined(entityState?.state)}
slot="graphic"
.state=${entityState}
.hass=${this.hass}
.stateObj=${entityState}
></ha-state-icon>
${computeStateName(entityState)}
<span slot="secondary">${entityState.entity_id}</span>

View File

@ -137,7 +137,8 @@ export class VoiceAssistantsExpose extends LitElement {
template: (entry) => html`
<ha-state-icon
title=${ifDefined(entry.entity?.state)}
.state=${entry.entity}
.stateObj=${entry.entity}
.hass=${this.hass}
></ha-state-icon>
`,
},

View File

@ -33,10 +33,8 @@ import {
} from "../../../common/number/format_number";
import { subscribeOne } from "../../../common/util/subscribe-one";
import parseAspectRatio from "../../../common/util/parse-aspect-ratio";
import "../../../components/entity/state-badge";
import "../../../components/ha-card";
import "../../../components/ha-icon-button";
import "../../../components/ha-state-icon";
import "../../../components/ha-svg-icon";
import {
AreaRegistryEntry,

View File

@ -214,7 +214,8 @@ export class HuiButtonCard extends LitElement implements LovelaceCard {
)}
data-state=${ifDefined(stateObj?.state)}
.icon=${this._config.icon}
.state=${stateObj}
.hass=${this.hass}
.stateObj=${stateObj}
style=${styleMap({
color: colored ? this._computeColor(stateObj) : undefined,
filter: colored ? stateColorBrightness(stateObj) : undefined,

View File

@ -139,7 +139,8 @@ export class HuiEntityCard extends LitElement implements LovelaceCard {
<div class="icon">
<ha-state-icon
.icon=${this._config.icon}
.state=${stateObj}
.stateObj=${stateObj}
.hass=${this.hass}
data-domain=${ifDefined(domain)}
data-state=${stateObj.state}
style=${styleMap({

View File

@ -148,7 +148,8 @@ export class HuiLightCard extends LitElement implements LovelaceCard {
>
<ha-state-icon
.icon=${this._config.icon}
.state=${stateObj}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
</ha-icon-button>
</div>

View File

@ -234,7 +234,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard {
>
<div class="top-info">
<div class="icon-name">
<ha-state-icon class="icon" .state=${stateObj}></ha-state-icon>
<ha-state-icon
class="icon"
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
<div>
${this._config!.name ||
computeStateName(this.hass!.states[this._config!.entity])}

View File

@ -181,6 +181,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard {
hui-image {
cursor: pointer;
height: 100%;
}
.footer {

View File

@ -278,7 +278,8 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard {
>
<ha-state-icon
.icon=${entityConf.icon}
.state=${stateObj}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
</ha-icon-button>

View File

@ -144,7 +144,8 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard {
<div class="icon">
<ha-state-icon
.icon=${this._config.icon}
.state=${stateObj}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
</div>
</div>

View File

@ -256,7 +256,8 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard {
html`
<ha-state-icon
class="weather-icon"
.state=${stateObj}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
`}
</div>

View File

@ -400,12 +400,14 @@ export class HuiImage extends LitElement {
.container {
transition: filter 0.2s linear;
height: 100%;
}
img {
display: block;
height: auto;
height: 100%;
width: 100%;
object-fit: cover;
}
.progress-container {
@ -428,6 +430,12 @@ export class HuiImage extends LitElement {
background-size: contain;
background-repeat: no-repeat;
}
.fill img {
object-fit: fill;
}
.contain img {
object-fit: contain;
}
.ratio img,
.ratio div {

View File

@ -19,7 +19,6 @@ import {
import { fireEvent, HASSDomEvent } from "../../../../common/dom/fire_event";
import { customType } from "../../../../common/structs/is-custom-type";
import { computeRTLDirection } from "../../../../common/util/compute_rtl";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-card";
import "../../../../components/ha-formfield";
import "../../../../components/ha-icon";

View File

@ -59,6 +59,7 @@ export class HuiStateIconElement extends LitElement implements LovelaceElement {
return html`
<state-badge
.hass=${this.hass}
.stateObj=${stateObj}
.title=${computeTooltip(this.hass, this._config)}
@action=${this._handleAction}

View File

@ -10,7 +10,6 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { ifDefined } from "lit/directives/if-defined";
import { computeStateName } from "../../../common/entity/compute_state_name";
import "../../../components/entity/state-badge";
import { isUnavailableState } from "../../../data/entity";
import { ActionHandlerEvent } from "../../../data/lovelace/action_handler";
import {
@ -145,7 +144,8 @@ class HuiWeatherEntityRow extends LitElement implements LovelaceRow {
html`
<ha-state-icon
class="weather-icon"
.state=${stateObj}
.stateObj=${stateObj}
.hass=${this.hass}
></ha-state-icon>
`}
</div>

View File

@ -53,7 +53,11 @@ export class HuiButtonRow extends LitElement implements LovelaceRow {
this._config.name ?? (stateObj ? computeStateName(stateObj) : "");
return html`
<ha-state-icon .icon=${this._config.icon} .state=${stateObj}>
<ha-state-icon
.icon=${this._config.icon}
.stateObj=${stateObj}
.hass=${this.hass}
>
</ha-state-icon>
<div class="flex">
<div .title=${name}>${name}</div>

View File

@ -167,7 +167,11 @@ class PanelTodo extends LitElement {
.entityId=${list.entity_id}
.activated=${list.entity_id === this._entityId}
>
<ha-state-icon .state=${list} slot="graphic"></ha-state-icon
<ha-state-icon
.stateObj=${list}
.hass=${this.hass}
slot="graphic"
></ha-state-icon
>${list.name}
</ha-list-item> `
);

View File

@ -40,7 +40,7 @@ class StateCardInputSelect extends LitElement {
protected render(): TemplateResult {
return html`
<state-badge .stateObj=${this.stateObj}></state-badge>
<state-badge .hass=${this.hass} .stateObj=${this.stateObj}></state-badge>
<ha-select
.label=${computeStateName(this.stateObj)}
.value=${this.stateObj.state}

View File

@ -17,7 +17,7 @@ class StateCardSelect extends LitElement {
protected render(): TemplateResult {
return html`
<state-badge .stateObj=${this.stateObj}></state-badge>
<state-badge .hass=${this.hass} .stateObj=${this.stateObj}></state-badge>
<ha-select
.value=${this.stateObj.state}
.label=${computeStateName(this.stateObj)}

View File

@ -16,7 +16,7 @@ class StateCardText extends LitElement {
protected render(): TemplateResult {
return html`
<state-badge .stateObj=${this.stateObj}></state-badge>
<state-badge .hass=${this.hass} .stateObj=${this.stateObj}></state-badge>
<ha-textfield
.label=${computeStateName(this.stateObj)}
.disabled=${this.stateObj.state === UNAVAILABLE}

View File

@ -2,6 +2,7 @@ import { ContextProvider } from "@lit-labs/context";
import {
areasContext,
configContext,
connectionContext,
devicesContext,
entitiesContext,
localeContext,
@ -24,6 +25,12 @@ export const contextMixin = <T extends Constructor<HassBaseEl>>(
string,
ContextProvider<any> | undefined
> = {
connection: new ContextProvider(this, {
context: connectionContext,
initialValue: this.hass
? this.hass.connection
: this._pendingHass.connection,
}),
states: new ContextProvider(this, {
context: statesContext,
initialValue: this.hass ? this.hass.states : this._pendingHass.states,