Improve icon translations support (#19516)

* Use slot for icon picker

* Use icon translations for disabled entities in device page

* Don't use fallback for light effect

* Fix device class

* Fix climate hvac mode

* Migrate fan direction to icon translations

* Remove attribute fallback

* Rename variable

* Update src/panels/lovelace/card-features/hui-climate-hvac-modes-card-feature.ts

Co-authored-by: Bram Kragten <mail@bramkragten.nl>

---------

Co-authored-by: Bram Kragten <mail@bramkragten.nl>
This commit is contained in:
Paul Bottein 2024-01-24 18:31:06 +01:00 committed by GitHub
parent beb3454f8d
commit 85f086d02e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 237 additions and 388 deletions

View File

@ -1,46 +0,0 @@
/** Return an icon representing a attribute. */
import { mdiCircleMedium, mdiCreation } from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
import {
computeFanModeIcon,
computeHvacModeIcon,
computePresetModeIcon,
computeSwingModeIcon,
} from "../../data/climate";
import { computeHumidiferModeIcon } from "../../data/humidifier";
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,
},
humidifier: {
mode: computeHumidiferModeIcon,
},
light: {
effect: () => mdiCreation,
},
fan: {
preset_mode: () => mdiCircleMedium,
},
};
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

@ -6,7 +6,6 @@ import { attributeIcon } from "../data/icons";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-icon"; import "./ha-icon";
import "./ha-svg-icon"; import "./ha-svg-icon";
import { attributeIconPath } from "../common/entity/attribute_icon_path";
@customElement("ha-attribute-icon") @customElement("ha-attribute-icon")
export class HaAttributeIcon extends LitElement { export class HaAttributeIcon extends LitElement {
@ -30,7 +29,7 @@ export class HaAttributeIcon extends LitElement {
} }
if (!this.hass) { if (!this.hass) {
return this._renderFallback(); return nothing;
} }
const icon = attributeIcon( const icon = attributeIcon(
@ -42,23 +41,11 @@ export class HaAttributeIcon extends LitElement {
if (icn) { if (icn) {
return html`<ha-icon .icon=${icn}></ha-icon>`; return html`<ha-icon .icon=${icn}></ha-icon>`;
} }
return this._renderFallback(); return nothing;
}); });
return html`${until(icon)}`; return html`${until(icon)}`;
} }
private _renderFallback() {
return html`
<ha-svg-icon
.path=${attributeIconPath(
this.stateObj!,
this.attribute!,
this.attributeValue!
)}
></ha-svg-icon>
`;
}
} }
declare global { declare global {

View File

@ -84,8 +84,6 @@ export class HaIconPicker extends LitElement {
@property() public placeholder?: string; @property() public placeholder?: string;
@property() public fallbackPath?: string;
@property({ attribute: "error-message" }) public errorMessage?: string; @property({ attribute: "error-message" }) public errorMessage?: string;
@property({ type: Boolean }) public disabled = false; @property({ type: Boolean }) public disabled = false;
@ -120,12 +118,7 @@ export class HaIconPicker extends LitElement {
<ha-icon .icon=${this._value || this.placeholder} slot="icon"> <ha-icon .icon=${this._value || this.placeholder} slot="icon">
</ha-icon> </ha-icon>
` `
: this.fallbackPath : html`<slot name="fallback"></slot>`}
? html`<ha-svg-icon
.path=${this.fallbackPath}
slot="icon"
></ha-svg-icon>`
: ""}
</ha-combo-box> </ha-combo-box>
`; `;
} }

View File

@ -1,13 +1,12 @@
import { html, LitElement } from "lit"; import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators"; import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until"; import { until } from "lit/directives/until";
import { fireEvent } from "../../common/dom/fire_event"; 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 { entityIcon } from "../../data/icons";
import { IconSelector } from "../../data/selector"; import { IconSelector } from "../../data/selector";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-icon-picker"; import "../ha-icon-picker";
import "../ha-state-icon";
@customElement("ha-selector-icon") @customElement("ha-selector-icon")
export class HaIconSelector extends LitElement { export class HaIconSelector extends LitElement {
@ -39,11 +38,6 @@ export class HaIconSelector extends LitElement {
stateObj?.attributes.icon || stateObj?.attributes.icon ||
(stateObj && until(entityIcon(this.hass, stateObj))); (stateObj && until(entityIcon(this.hass, stateObj)));
const fallbackPath =
!placeholder && stateObj
? domainIcon(computeDomain(iconEntity!), stateObj)
: undefined;
return html` return html`
<ha-icon-picker <ha-icon-picker
.hass=${this.hass} .hass=${this.hass}
@ -52,10 +46,19 @@ export class HaIconSelector extends LitElement {
.required=${this.required} .required=${this.required}
.disabled=${this.disabled} .disabled=${this.disabled}
.helper=${this.helper} .helper=${this.helper}
.fallbackPath=${this.selector.icon?.fallbackPath ?? fallbackPath}
.placeholder=${this.selector.icon?.placeholder ?? placeholder} .placeholder=${this.selector.icon?.placeholder ?? placeholder}
@value-changed=${this._valueChanged} @value-changed=${this._valueChanged}
></ha-icon-picker> >
${!placeholder && stateObj
? html`
<ha-state-icon
slot="fallback"
.hass=${this.hass}
.stateObj=${stateObj}
></ha-state-icon>
`
: nothing}
</ha-icon-picker>
`; `;
} }

View File

@ -1,33 +1,12 @@
import { import {
mdiAccountArrowRight,
mdiArrowAll,
mdiArrowLeftRight,
mdiArrowOscillating,
mdiArrowOscillatingOff,
mdiArrowUpDown,
mdiBed,
mdiCircleMedium,
mdiClockOutline,
mdiFan, mdiFan,
mdiFanAuto,
mdiFanOff,
mdiFire, mdiFire,
mdiHeatWave,
mdiHome,
mdiLeaf,
mdiMotionSensor,
mdiPower, mdiPower,
mdiRocketLaunch,
mdiSnowflake, mdiSnowflake,
mdiSofa,
mdiSpeedometer,
mdiSpeedometerMedium,
mdiSpeedometerSlow,
mdiSunSnowflakeVariant, mdiSunSnowflakeVariant,
mdiTarget, mdiThermostat,
mdiThermostatAuto, mdiThermostatAuto,
mdiWaterPercent, mdiWaterPercent,
mdiWeatherWindy,
} from "@mdi/js"; } from "@mdi/js";
import { import {
HassEntityAttributeBase, HassEntityAttributeBase,
@ -116,16 +95,6 @@ export const CLIMATE_HVAC_ACTION_TO_MODE: Record<HvacAction, HvacMode> = {
off: "off", off: "off",
}; };
export const CLIMATE_HVAC_ACTION_ICONS: Record<HvacAction, string> = {
cooling: mdiSnowflake,
drying: mdiWaterPercent,
fan: mdiFan,
heating: mdiFire,
idle: mdiClockOutline,
off: mdiPower,
preheating: mdiHeatWave,
};
export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = { export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = {
cool: mdiSnowflake, cool: mdiSnowflake,
dry: mdiWaterPercent, dry: mdiWaterPercent,
@ -136,81 +105,5 @@ export const CLIMATE_HVAC_MODE_ICONS: Record<HvacMode, string> = {
heat_cool: mdiSunSnowflakeVariant, heat_cool: mdiSunSnowflakeVariant,
}; };
export const computeHvacModeIcon = (mode: HvacMode) => export const climateHvacModeIcon = (mode: string) =>
CLIMATE_HVAC_MODE_ICONS[mode]; CLIMATE_HVAC_MODE_ICONS[mode] || mdiThermostat;
type ClimateBuiltInPresetMode =
| "eco"
| "away"
| "boost"
| "comfort"
| "home"
| "sleep"
| "activity";
export const CLIMATE_PRESET_MODE_ICONS: Record<
ClimateBuiltInPresetMode,
string
> = {
away: mdiAccountArrowRight,
boost: mdiRocketLaunch,
comfort: mdiSofa,
eco: mdiLeaf,
home: mdiHome,
sleep: mdiBed,
activity: mdiMotionSensor,
};
export const computePresetModeIcon = (mode: string) =>
mode in CLIMATE_PRESET_MODE_ICONS
? CLIMATE_PRESET_MODE_ICONS[mode]
: mdiCircleMedium;
type ClimateBuiltInFanMode =
| "on"
| "off"
| "auto"
| "low"
| "medium"
| "high"
| "middle"
| "focus"
| "diffuse";
export const CLIMATE_FAN_MODE_ICONS: Record<ClimateBuiltInFanMode, string> = {
on: mdiFan,
off: mdiFanOff,
auto: mdiFanAuto,
low: mdiSpeedometerSlow,
medium: mdiSpeedometerMedium,
high: mdiSpeedometer,
middle: mdiSpeedometerMedium,
focus: mdiTarget,
diffuse: mdiWeatherWindy,
};
export const computeFanModeIcon = (mode: string) =>
mode in CLIMATE_FAN_MODE_ICONS
? CLIMATE_FAN_MODE_ICONS[mode]
: mdiCircleMedium;
type ClimateBuiltInSwingMode =
| "off"
| "on"
| "vertical"
| "horizontal"
| "both";
export const CLIMATE_SWING_MODE_ICONS: Record<ClimateBuiltInSwingMode, string> =
{
on: mdiArrowOscillating,
off: mdiArrowOscillatingOff,
vertical: mdiArrowUpDown,
horizontal: mdiArrowLeftRight,
both: mdiArrowAll,
};
export const computeSwingModeIcon = (mode: string) =>
mode in CLIMATE_SWING_MODE_ICONS
? CLIMATE_SWING_MODE_ICONS[mode]
: mdiCircleMedium;

View File

@ -1,19 +1,3 @@
import {
mdiAccountArrowRight,
mdiArrowDownBold,
mdiArrowUpBold,
mdiBabyCarriage,
mdiCircleMedium,
mdiClockOutline,
mdiHome,
mdiLeaf,
mdiPower,
mdiPowerSleep,
mdiRefreshAuto,
mdiRocketLaunch,
mdiSofa,
mdiWaterPercent,
} from "@mdi/js";
import { import {
HassEntityAttributeBase, HassEntityAttributeBase,
HassEntityBase, HassEntityBase,
@ -44,39 +28,6 @@ export const enum HumidifierEntityDeviceClass {
DEHUMIDIFIER = "dehumidifier", DEHUMIDIFIER = "dehumidifier",
} }
type HumidifierBuiltInMode =
| "normal"
| "eco"
| "away"
| "boost"
| "comfort"
| "home"
| "sleep"
| "auto"
| "baby";
export const HUMIDIFIER_MODE_ICONS: Record<HumidifierBuiltInMode, string> = {
auto: mdiRefreshAuto,
away: mdiAccountArrowRight,
baby: mdiBabyCarriage,
boost: mdiRocketLaunch,
comfort: mdiSofa,
eco: mdiLeaf,
home: mdiHome,
normal: mdiWaterPercent,
sleep: mdiPowerSleep,
};
export const computeHumidiferModeIcon = (mode?: string) =>
HUMIDIFIER_MODE_ICONS[mode as HumidifierBuiltInMode] ?? mdiCircleMedium;
export const HUMIDIFIER_ACTION_ICONS: Record<HumidifierAction, string> = {
drying: mdiArrowDownBold,
humidifying: mdiArrowUpBold,
idle: mdiClockOutline,
off: mdiPower,
};
export const HUMIDIFIER_ACTION_MODE: Record<HumidifierAction, HumidifierState> = export const HUMIDIFIER_ACTION_MODE: Record<HumidifierAction, HumidifierState> =
{ {
drying: "on", drying: "on",

View File

@ -1,6 +1,11 @@
import { HassEntity } from "home-assistant-js-websocket"; import { HassEntity } from "home-assistant-js-websocket";
import { computeStateDomain } from "../common/entity/compute_state_domain"; import { computeStateDomain } from "../common/entity/compute_state_domain";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "./entity_registry";
import { computeDomain } from "../common/entity/compute_domain";
const resources: Record<IconCategory, any> = { const resources: Record<IconCategory, any> = {
entity: {}, entity: {},
@ -15,7 +20,13 @@ interface PlatformIcons {
[domain: string]: { [domain: string]: {
[translation_key: string]: { [translation_key: string]: {
state: Record<string, string>; state: Record<string, string>;
state_attributes: Record<string, { state: Record<string, string> }>; state_attributes: Record<
string,
{
state: Record<string, string>;
default: string;
}
>;
default: string; default: string;
}; };
}; };
@ -24,7 +35,13 @@ interface PlatformIcons {
interface ComponentIcons { interface ComponentIcons {
[device_class: string]: { [device_class: string]: {
state: Record<string, string>; state: Record<string, string>;
state_attributes: Record<string, { state: Record<string, string> }>; state_attributes: Record<
string,
{
state: Record<string, string>;
default: string;
}
>;
default: string; default: string;
}; };
} }
@ -76,32 +93,67 @@ export const entityIcon = async (
state: HassEntity, state: HassEntity,
stateValue?: string stateValue?: string
) => { ) => {
let icon: string | undefined; const entity = hass.entities?.[state.entity_id] as
const domain = computeStateDomain(state); | EntityRegistryDisplayEntry
const entity = hass.entities?.[state.entity_id]; | undefined;
const value = stateValue ?? state.state;
if (entity?.icon) { if (entity?.icon) {
return entity.icon; return entity.icon;
} }
if (entity?.translation_key && entity.platform) { const domain = computeStateDomain(state);
const platformIcons = await getPlatformIcons(hass, entity.platform); const deviceClass = state.attributes.device_class;
return getEntityIcon(
hass,
domain,
deviceClass,
stateValue ?? state.state,
entity?.platform,
entity?.translation_key
);
};
export const entryIcon = async (
hass: HomeAssistant,
entry: EntityRegistryEntry | EntityRegistryDisplayEntry
) => {
if (entry.icon) {
return entry.icon;
}
const domain = computeDomain(entry.entity_id);
return getEntityIcon(
hass,
domain,
undefined,
undefined,
entry.platform,
entry.translation_key
);
};
const getEntityIcon = async (
hass: HomeAssistant,
domain: string,
deviceClass?: string,
value?: string,
platform?: string,
translation_key?: string
) => {
let icon: string | undefined;
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(hass, platform);
if (platformIcons) { if (platformIcons) {
icon = const translations = platformIcons[domain]?.[translation_key];
platformIcons[domain]?.[entity.translation_key]?.state?.[value] || icon = (value && translations?.state?.[value]) || translations?.default;
platformIcons[domain]?.[entity.translation_key]?.default;
} }
} }
if (!icon) { if (!icon) {
const entityComponentIcons = await getComponentIcons(hass, domain); const entityComponentIcons = await getComponentIcons(hass, domain);
if (entityComponentIcons) { if (entityComponentIcons) {
icon = const translations =
entityComponentIcons[state.attributes.device_class || "_"]?.state?.[ (deviceClass && entityComponentIcons[deviceClass]) ||
value entityComponentIcons._;
] || icon = (value && translations?.state?.[value]) || translations?.default;
entityComponentIcons._?.state?.[value] ||
entityComponentIcons[state.attributes.device_class || "_"]?.default ||
entityComponentIcons._?.default;
} }
} }
return icon; return icon;
@ -115,24 +167,32 @@ export const attributeIcon = async (
) => { ) => {
let icon: string | undefined; let icon: string | undefined;
const domain = computeStateDomain(state); const domain = computeStateDomain(state);
const entity = hass.entities?.[state.entity_id]; const deviceClass = state.attributes.device_class;
const value = attributeValue ?? state.attributes[attribute]; const entity = hass.entities?.[state.entity_id] as
if (entity?.translation_key && entity.platform) { | EntityRegistryDisplayEntry
const platformIcons = await getPlatformIcons(hass, entity.platform); | undefined;
const platform = entity?.platform;
const translation_key = entity?.translation_key;
const value =
attributeValue ??
(state.attributes[attribute] as string | number | undefined);
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(hass, platform);
if (platformIcons) { if (platformIcons) {
icon = const translations =
platformIcons[domain]?.[entity.translation_key]?.state_attributes?.[ platformIcons[domain]?.[translation_key]?.state_attributes?.[attribute];
attribute icon = (value && translations?.state?.[value]) || translations?.default;
]?.state?.[value];
} }
} }
if (!icon) { if (!icon) {
const entityComponentIcons = await getComponentIcons(hass, domain); const entityComponentIcons = await getComponentIcons(hass, domain);
if (entityComponentIcons) { if (entityComponentIcons) {
icon = const translations =
entityComponentIcons[state.attributes.device_class || "_"] (deviceClass &&
.state_attributes?.[attribute]?.state?.[value] || entityComponentIcons[deviceClass]?.state_attributes?.[attribute]) ||
entityComponentIcons._.state_attributes?.[attribute]?.state?.[value]; entityComponentIcons._?.state_attributes?.[attribute];
icon = (value && translations?.state?.[value]) || translations?.default;
} }
} }
return icon; return icon;

View File

@ -3,7 +3,6 @@ import {
mdiArrowOscillating, mdiArrowOscillating,
mdiFan, mdiFan,
mdiThermometer, mdiThermometer,
mdiThermostat,
mdiTuneVariant, mdiTuneVariant,
mdiWaterPercent, mdiWaterPercent,
} from "@mdi/js"; } from "@mdi/js";
@ -11,19 +10,20 @@ import { CSSResultGroup, LitElement, css, html, nothing } from "lit";
import { property, state } from "lit/decorators"; import { property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu"; import "../../../components/ha-control-select-menu";
import "../../../components/ha-icon-button-group"; import "../../../components/ha-icon-button-group";
import "../../../components/ha-icon-button-toggle"; import "../../../components/ha-icon-button-toggle";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import "../../../components/ha-select"; import "../../../components/ha-select";
import "../../../components/ha-switch"; import "../../../components/ha-switch";
import "../../../components/ha-attribute-icon";
import { import {
ClimateEntity, ClimateEntity,
ClimateEntityFeature, ClimateEntityFeature,
climateHvacModeIcon,
compareClimateHvacModes, compareClimateHvacModes,
} from "../../../data/climate"; } from "../../../data/climate";
import { UNAVAILABLE, isUnavailableState } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
import "../../../state-control/climate/ha-state-control-climate-humidity"; import "../../../state-control/climate/ha-state-control-climate-humidity";
import "../../../state-control/climate/ha-state-control-climate-temperature"; import "../../../state-control/climate/ha-state-control-climate-temperature";
import { HomeAssistant } from "../../../types"; import { HomeAssistant } from "../../../types";
@ -161,31 +161,22 @@ class MoreInfoClimate extends LitElement {
@selected=${this._handleOperationModeChanged} @selected=${this._handleOperationModeChanged}
@closed=${stopPropagation} @closed=${stopPropagation}
> >
${!isUnavailableState(this.stateObj.state) ${html`
? html`<ha-attribute-icon <ha-svg-icon
slot="icon" slot="icon"
.hass=${this.hass} .path=${climateHvacModeIcon(stateObj.state)}
.stateObj=${stateObj} ></ha-svg-icon>
attribute="hvac_mode" `}
.attributeValue=${stateObj.state}
></ha-attribute-icon>`
: html`<ha-svg-icon
slot="icon"
.path=${mdiThermostat}
></ha-svg-icon>`}
${stateObj.attributes.hvac_modes ${stateObj.attributes.hvac_modes
.concat() .concat()
.sort(compareClimateHvacModes) .sort(compareClimateHvacModes)
.map( .map(
(mode) => html` (mode) => html`
<ha-list-item .value=${mode} graphic="icon"> <ha-list-item .value=${mode} graphic="icon">
<ha-attribute-icon <ha-svg-icon
slot="graphic" slot="graphic"
.hass=${this.hass} .path=${climateHvacModeIcon(mode)}
.stateObj=${stateObj} ></ha-svg-icon>
attribute="hvac_mode"
.attributeValue=${mode}
></ha-attribute-icon>
${this.hass.formatEntityState(stateObj, mode)} ${this.hass.formatEntityState(stateObj, mode)}
</ha-list-item> </ha-list-item>
` `
@ -206,13 +197,15 @@ class MoreInfoClimate extends LitElement {
@closed=${stopPropagation} @closed=${stopPropagation}
> >
${stateObj.attributes.preset_mode ${stateObj.attributes.preset_mode
? html`<ha-attribute-icon ? html`
slot="icon" <ha-attribute-icon
.hass=${this.hass} slot="icon"
.stateObj=${stateObj} .hass=${this.hass}
attribute="preset_mode" .stateObj=${stateObj}
.attributeValue=${stateObj.attributes.preset_mode} attribute="preset_mode"
></ha-attribute-icon>` .attributeValue=${stateObj.attributes.preset_mode}
></ha-attribute-icon>
`
: html` : html`
<ha-svg-icon <ha-svg-icon
slot="icon" slot="icon"
@ -255,13 +248,15 @@ class MoreInfoClimate extends LitElement {
@closed=${stopPropagation} @closed=${stopPropagation}
> >
${stateObj.attributes.fan_mode ${stateObj.attributes.fan_mode
? html`<ha-attribute-icon ? html`
slot="icon" <ha-attribute-icon
.hass=${this.hass} slot="icon"
.stateObj=${stateObj} .hass=${this.hass}
attribute="fan_mode" .stateObj=${stateObj}
.attributeValue=${stateObj.attributes.fan_mode} attribute="fan_mode"
></ha-attribute-icon>` .attributeValue=${stateObj.attributes.fan_mode}
></ha-attribute-icon>
`
: html` : html`
<ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon> <ha-svg-icon slot="icon" .path=${mdiFan}></ha-svg-icon>
`} `}
@ -301,13 +296,15 @@ class MoreInfoClimate extends LitElement {
@closed=${stopPropagation} @closed=${stopPropagation}
> >
${stateObj.attributes.swing_mode ${stateObj.attributes.swing_mode
? html`<ha-attribute-icon ? html`
slot="icon" <ha-attribute-icon
.hass=${this.hass} slot="icon"
.stateObj=${stateObj} .hass=${this.hass}
attribute="swing_mode" .stateObj=${stateObj}
.attributeValue=${stateObj.attributes.swing_mode} attribute="swing_mode"
></ha-attribute-icon>` .attributeValue=${stateObj.attributes.swing_mode}
></ha-attribute-icon>
`
: html` : html`
<ha-svg-icon <ha-svg-icon
slot="icon" slot="icon"

View File

@ -4,8 +4,6 @@ import {
mdiFan, mdiFan,
mdiFanOff, mdiFanOff,
mdiPower, mdiPower,
mdiRotateLeft,
mdiRotateRight,
mdiTuneVariant, mdiTuneVariant,
} from "@mdi/js"; } from "@mdi/js";
import { CSSResultGroup, LitElement, PropertyValues, html, nothing } from "lit"; import { CSSResultGroup, LitElement, PropertyValues, html, nothing } from "lit";
@ -13,8 +11,8 @@ import { customElement, property, state } from "lit/decorators";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { stateActive } from "../../../common/entity/state_active"; import { stateActive } from "../../../common/entity/state_active";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-attribute-icon"; import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select-menu";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import "../../../components/ha-outlined-icon-button"; import "../../../components/ha-outlined-icon-button";
import { UNAVAILABLE } from "../../../data/entity"; import { UNAVAILABLE } from "../../../data/entity";
@ -237,12 +235,21 @@ class MoreInfoFan extends LitElement {
@selected=${this._handleDirection} @selected=${this._handleDirection}
@closed=${stopPropagation} @closed=${stopPropagation}
> >
<ha-svg-icon slot="icon" .path=${mdiRotateLeft}></ha-svg-icon> <ha-attribute-icon
slot="icon"
.hass=${this.hass}
.stateObj=${this.stateObj}
attribute="direction"
.attributeValue=${this.stateObj.attributes.direction}
></ha-attribute-icon>
<ha-list-item value="forward" graphic="icon"> <ha-list-item value="forward" graphic="icon">
<ha-svg-icon <ha-attribute-icon
slot="graphic" slot="graphic"
.path=${mdiRotateRight} .hass=${this.hass}
></ha-svg-icon> .stateObj=${this.stateObj}
attribute="direction"
attributeValue="forward"
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue( ${this.hass.formatEntityAttributeValue(
this.stateObj, this.stateObj,
"direction", "direction",
@ -250,10 +257,13 @@ class MoreInfoFan extends LitElement {
)} )}
</ha-list-item> </ha-list-item>
<ha-list-item value="reverse" graphic="icon"> <ha-list-item value="reverse" graphic="icon">
<ha-svg-icon <ha-attribute-icon
slot="graphic" slot="graphic"
.path=${mdiRotateLeft} .hass=${this.hass}
></ha-svg-icon> .stateObj=${this.stateObj}
attribute="direction"
attributeValue="reverse"
></ha-attribute-icon>
${this.hass.formatEntityAttributeValue( ${this.hass.formatEntityAttributeValue(
this.stateObj, this.stateObj,
"direction", "direction",

View File

@ -110,17 +110,21 @@ class MoreInfoHumidifier extends LitElement {
@closed=${stopPropagation} @closed=${stopPropagation}
> >
${stateObj.attributes.mode ${stateObj.attributes.mode
? html`<ha-attribute-icon ? html`
slot="icon" <ha-attribute-icon
.hass=${this.hass} slot="icon"
.stateObj=${stateObj} .hass=${this.hass}
attribute="mode" .stateObj=${stateObj}
.attributeValue=${stateObj.attributes.mode} attribute="mode"
></ha-attribute-icon>` .attributeValue=${stateObj.attributes.mode}
: html`<ha-svg-icon ></ha-attribute-icon>
slot="icon" `
.path=${mdiTuneVariant} : html`
></ha-svg-icon>`} <ha-svg-icon
slot="icon"
.path=${mdiTuneVariant}
></ha-svg-icon>
`}
${stateObj.attributes.available_modes!.map( ${stateObj.attributes.available_modes!.map(
(mode) => html` (mode) => html`
<ha-list-item .value=${mode} graphic="icon"> <ha-list-item .value=${mode} graphic="icon">

View File

@ -285,19 +285,19 @@ class MoreInfoLight extends LitElement {
.path=${mdiCreation} .path=${mdiCreation}
></ha-svg-icon>`} ></ha-svg-icon>`}
${this.stateObj.attributes.effect_list?.map( ${this.stateObj.attributes.effect_list?.map(
(mode) => html` (effect) => html`
<ha-list-item .value=${mode} graphic="icon"> <ha-list-item .value=${effect} graphic="icon">
<ha-attribute-icon <ha-attribute-icon
slot="graphic" slot="graphic"
.hass=${this.hass} .hass=${this.hass}
.stateObj=${this.stateObj} .stateObj=${this.stateObj}
attribute="effect" attribute="effect"
.attributeValue=${mode} .attributeValue=${effect}
></ha-attribute-icon> ></ha-attribute-icon>
${this.hass.formatEntityAttributeValue( ${this.hass.formatEntityAttributeValue(
this.stateObj!, this.stateObj!,
"effect", "effect",
mode effect
)} )}
</ha-list-item> </ha-list-item>
` `

View File

@ -8,9 +8,8 @@ import {
TemplateResult, TemplateResult,
} from "lit"; } from "lit";
import { customElement, property, state } from "lit/decorators"; import { customElement, property, state } from "lit/decorators";
import { computeDomain } from "../../../../common/entity/compute_domain"; import { until } from "lit/directives/until";
import { computeStateName } from "../../../../common/entity/compute_state_name"; 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 { stripPrefixFromEntityName } from "../../../../common/entity/strip_prefix_from_entity_name";
import "../../../../components/ha-card"; import "../../../../components/ha-card";
import "../../../../components/ha-icon"; import "../../../../components/ha-icon";
@ -19,6 +18,7 @@ import {
ExtEntityRegistryEntry, ExtEntityRegistryEntry,
getExtendedEntityRegistryEntry, getExtendedEntityRegistryEntry,
} from "../../../../data/entity_registry"; } from "../../../../data/entity_registry";
import { entryIcon } from "../../../../data/icons";
import { showMoreInfoDialog } from "../../../../dialogs/more-info/show-ha-more-info-dialog"; import { showMoreInfoDialog } from "../../../../dialogs/more-info/show-ha-more-info-dialog";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { HuiErrorCard } from "../../../lovelace/cards/hui-error-card"; import type { HuiErrorCard } from "../../../lovelace/cards/hui-error-card";
@ -198,6 +198,8 @@ export class HaDeviceEntitiesCard extends LitElement {
entry.name || entry.name ||
(entry as ExtEntityRegistryEntry).original_name; (entry as ExtEntityRegistryEntry).original_name;
const icon = until(entryIcon(this.hass, entry));
return html` return html`
<ha-list-item <ha-list-item
graphic="icon" graphic="icon"
@ -205,10 +207,7 @@ export class HaDeviceEntitiesCard extends LitElement {
.entry=${entry} .entry=${entry}
@click=${this._openEditEntry} @click=${this._openEditEntry}
> >
<ha-svg-icon <ha-icon slot="graphic" .icon=${icon}></ha-icon>
slot="graphic"
.path=${domainIcon(computeDomain(entry.entity_id))}
></ha-svg-icon>
<div class="name"> <div class="name">
${name ${name
? stripPrefixFromEntityName(name, this.deviceName.toLowerCase()) || ? stripPrefixFromEntityName(name, this.deviceName.toLowerCase()) ||

View File

@ -18,7 +18,6 @@ import { fireEvent } from "../../../common/dom/fire_event";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeObjectId } from "../../../common/entity/compute_object_id"; import { computeObjectId } from "../../../common/entity/compute_object_id";
import { domainIcon } from "../../../common/entity/domain_icon";
import { supportsFeature } from "../../../common/entity/supports-feature"; import { supportsFeature } from "../../../common/entity/supports-feature";
import { formatNumber } from "../../../common/number/format_number"; import { formatNumber } from "../../../common/number/format_number";
import { stringCompare } from "../../../common/string/compare"; import { stringCompare } from "../../../common/string/compare";
@ -32,6 +31,7 @@ import "../../../components/ha-area-picker";
import "../../../components/ha-icon"; import "../../../components/ha-icon";
import "../../../components/ha-icon-button-next"; import "../../../components/ha-icon-button-next";
import "../../../components/ha-icon-picker"; import "../../../components/ha-icon-picker";
import "../../../components/ha-state-icon";
import "../../../components/ha-list-item"; import "../../../components/ha-list-item";
import "../../../components/ha-radio"; import "../../../components/ha-radio";
import "../../../components/ha-select"; import "../../../components/ha-select";
@ -373,29 +373,30 @@ export class EntityRegistrySettingsEditor extends LitElement {
></ha-textfield>`} ></ha-textfield>`}
${this.hideIcon ${this.hideIcon
? nothing ? nothing
: html`<ha-icon-picker : html`
.hass=${this.hass} <ha-icon-picker
.value=${this._icon} .hass=${this.hass}
@value-changed=${this._iconChanged} .value=${this._icon}
.label=${this.hass.localize( @value-changed=${this._iconChanged}
"ui.dialogs.entity_registry.editor.icon" .label=${this.hass.localize(
)} "ui.dialogs.entity_registry.editor.icon"
.placeholder=${this.entry.original_icon || )}
stateObj?.attributes.icon || .placeholder=${this.entry.original_icon ||
(stateObj && until(entityIcon(this.hass, stateObj)))} stateObj?.attributes.icon ||
.fallbackPath=${!this._icon && (stateObj && until(entityIcon(this.hass, stateObj)))}
!stateObj?.attributes.icon && .disabled=${this.disabled}
stateObj >
? domainIcon(computeDomain(stateObj.entity_id), { ${!this._icon && !stateObj?.attributes.icon && stateObj
...stateObj, ? html`
attributes: { <ha-state-icon
...stateObj.attributes, slot="fallback"
device_class: this._deviceClass, .hass=${this.hass}
}, .stateObj=${stateObj}
}) ></ha-state-icon>
: undefined} `
.disabled=${this.disabled} : nothing}
></ha-icon-picker>`} </ha-icon-picker>
`}
${domain === "switch" ${domain === "switch"
? html`<ha-select ? html`<ha-select
.label=${this.hass.localize( .label=${this.hass.localize(

View File

@ -6,13 +6,13 @@ import { styleMap } from "lit/directives/style-map";
import { stopPropagation } from "../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../common/dom/stop_propagation";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { stateColorCss } from "../../../common/entity/state_color"; import { stateColorCss } from "../../../common/entity/state_color";
import "../../../components/ha-attribute-icon";
import "../../../components/ha-control-select"; import "../../../components/ha-control-select";
import type { ControlSelectOption } from "../../../components/ha-control-select"; import type { ControlSelectOption } from "../../../components/ha-control-select";
import "../../../components/ha-control-select-menu"; import "../../../components/ha-control-select-menu";
import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu"; import type { HaControlSelectMenu } from "../../../components/ha-control-select-menu";
import { import {
ClimateEntity, ClimateEntity,
climateHvacModeIcon,
compareClimateHvacModes, compareClimateHvacModes,
HvacMode, HvacMode,
} from "../../../data/climate"; } from "../../../data/climate";
@ -130,13 +130,12 @@ class HuiClimateHvacModesCardFeature
.map<ControlSelectOption>((mode) => ({ .map<ControlSelectOption>((mode) => ({
value: mode, value: mode,
label: this.hass!.formatEntityState(this.stateObj!, mode), label: this.hass!.formatEntityState(this.stateObj!, mode),
icon: html`<ha-attribute-icon icon: html`
slot="graphic" <ha-svg-icon
.hass=${this.hass} slot="graphic"
.stateObj=${this.stateObj} .path=${climateHvacModeIcon(mode)}
attribute="hvac_mode" ></ha-svg-icon>
.attributeValue=${mode} `,
></ha-attribute-icon>`,
})); }));
if (this._config.style === "dropdown") { if (this._config.style === "dropdown") {
@ -154,17 +153,15 @@ class HuiClimateHvacModesCardFeature
@closed=${stopPropagation} @closed=${stopPropagation}
> >
${this._currentHvacMode ${this._currentHvacMode
? html`<ha-attribute-icon ? html`
slot="icon" <ha-svg-icon
.hass=${this.hass} slot="icon"
.stateObj=${this.stateObj} .path=${climateHvacModeIcon(this._currentHvacMode)}
attribute="hvac_mode" ></ha-svg-icon>
.attributeValue=${this._currentHvacMode} `
></ha-attribute-icon>` : html`
: html`<ha-svg-icon <ha-svg-icon slot="icon" .path=${mdiThermostat}></ha-svg-icon>
slot="icon" `}
.path=${mdiThermostat}
></ha-svg-icon>`}
${options.map( ${options.map(
(option) => html` (option) => html`
<ha-list-item .value=${option.value} graphic="icon"> <ha-list-item .value=${option.value} graphic="icon">

View File

@ -10,9 +10,9 @@ import {
import { RenderBadgeFunction } from "./tile-badge"; import { RenderBadgeFunction } from "./tile-badge";
export const renderHumidifierBadge: RenderBadgeFunction = (stateObj, hass) => { export const renderHumidifierBadge: RenderBadgeFunction = (stateObj, hass) => {
const hvacAction = (stateObj as HumidifierEntity).attributes.action; const action = (stateObj as HumidifierEntity).attributes.action;
if (!hvacAction || hvacAction === "off") { if (!action || action === "off") {
return nothing; return nothing;
} }
@ -21,7 +21,7 @@ export const renderHumidifierBadge: RenderBadgeFunction = (stateObj, hass) => {
style=${styleMap({ style=${styleMap({
"--tile-badge-background-color": stateColorCss( "--tile-badge-background-color": stateColorCss(
stateObj, stateObj,
HUMIDIFIER_ACTION_MODE[hvacAction] HUMIDIFIER_ACTION_MODE[action]
), ),
})} })}
> >