diff --git a/src/common/const.ts b/src/common/const.ts
index b770cf1d447b..6c3b4faa1fc4 100644
--- a/src/common/const.ts
+++ b/src/common/const.ts
@@ -1,6 +1,7 @@
/** Constants to be used in the frontend. */
import {
+ mdiAccount,
mdiAirFilter,
mdiAlert,
mdiAngleAcute,
@@ -50,8 +51,10 @@ import {
mdiProgressClock,
mdiRayVertex,
mdiRemote,
+ mdiRobot,
mdiRobotMower,
mdiRobotVacuum,
+ mdiRoomService,
mdiScriptText,
mdiSineWave,
mdiSpeakerMessage,
@@ -61,6 +64,7 @@ import {
mdiThermometerLines,
mdiThermostat,
mdiTimerOutline,
+ mdiToggleSwitch,
mdiTransmissionTower,
mdiWater,
mdiWaterPercent,
@@ -69,6 +73,7 @@ import {
mdiWeatherRainy,
mdiWeatherWindy,
mdiWeight,
+ mdiWhiteBalanceSunny,
mdiWifi,
} from "@mdi/js";
@@ -78,6 +83,9 @@ import { mdiHomeAssistant } from "../resources/home-assistant-logo-svg";
// Arrays with values should be alphabetically sorted if order doesn't matter.
// Each constant should have a description what it is supposed to be used for.
+/** Icon to use when no icon specified for service. */
+export const DEFAULT_SERVICE_ICON = mdiRoomService;
+
/** Icon to use when no icon specified for domain. */
export const DEFAULT_DOMAIN_ICON = mdiBookmark;
@@ -85,20 +93,23 @@ export const DEFAULT_DOMAIN_ICON = mdiBookmark;
export const FIXED_DOMAIN_ICONS = {
air_quality: mdiAirFilter,
alert: mdiAlert,
+ automation: mdiRobot,
calendar: mdiCalendar,
climate: mdiThermostat,
configurator: mdiCog,
conversation: mdiMicrophoneMessage,
counter: mdiCounter,
- datetime: mdiCalendarClock,
date: mdiCalendar,
+ datetime: mdiCalendarClock,
demo: mdiHomeAssistant,
+ device_tracker: mdiAccount,
google_assistant: mdiGoogleAssistant,
group: mdiGoogleCirclesCommunities,
homeassistant: mdiHomeAssistant,
homekit: mdiHomeAutomation,
- image: mdiImage,
image_processing: mdiImageFilterFrames,
+ image: mdiImage,
+ input_boolean: mdiToggleSwitch,
input_button: mdiButtonPointer,
input_datetime: mdiCalendarClock,
input_number: mdiRayVertex,
@@ -110,6 +121,7 @@ export const FIXED_DOMAIN_ICONS = {
notify: mdiCommentAlert,
number: mdiRayVertex,
persistent_notification: mdiBell,
+ person: mdiAccount,
plant: mdiFlower,
proximity: mdiAppleSafari,
remote: mdiRemote,
@@ -121,10 +133,11 @@ export const FIXED_DOMAIN_ICONS = {
simple_alarm: mdiBell,
siren: mdiBullhorn,
stt: mdiMicrophoneMessage,
+ sun: mdiWhiteBalanceSunny,
text: mdiFormTextbox,
- todo: mdiClipboardList,
time: mdiClock,
timer: mdiTimerOutline,
+ todo: mdiClipboardList,
tts: mdiSpeakerMessage,
vacuum: mdiRobotVacuum,
wake_word: mdiChatSleep,
diff --git a/src/common/entity/alarm_panel_icon.ts b/src/common/entity/alarm_panel_icon.ts
deleted file mode 100644
index cdbb736b3a4d..000000000000
--- a/src/common/entity/alarm_panel_icon.ts
+++ /dev/null
@@ -1,36 +0,0 @@
-/** Return an icon representing a alarm panel state. */
-
-import {
- mdiShieldLock,
- mdiShieldAirplane,
- mdiShieldHome,
- mdiShieldMoon,
- mdiSecurity,
- mdiShieldOutline,
- mdiBellRing,
- mdiShieldOff,
- mdiShield,
-} from "@mdi/js";
-
-export const alarmPanelIcon = (state?: string) => {
- switch (state) {
- case "armed_away":
- return mdiShieldLock;
- case "armed_vacation":
- return mdiShieldAirplane;
- case "armed_home":
- return mdiShieldHome;
- case "armed_night":
- return mdiShieldMoon;
- case "armed_custom_bypass":
- return mdiSecurity;
- case "pending":
- return mdiShieldOutline;
- case "triggered":
- return mdiBellRing;
- case "disarmed":
- return mdiShieldOff;
- default:
- return mdiShield;
- }
-};
diff --git a/src/common/entity/battery_icon.ts b/src/common/entity/battery_icon.ts
index d73cdbe489b4..75509b5d8b27 100644
--- a/src/common/entity/battery_icon.ts
+++ b/src/common/entity/battery_icon.ts
@@ -1,92 +1,59 @@
-/** Return an icon representing a battery state. */
-import {
- mdiBattery,
- mdiBattery10,
- mdiBattery20,
- mdiBattery30,
- mdiBattery40,
- mdiBattery50,
- mdiBattery60,
- mdiBattery70,
- mdiBattery80,
- mdiBattery90,
- mdiBatteryAlert,
- mdiBatteryAlertVariantOutline,
- mdiBatteryCharging,
- mdiBatteryCharging10,
- mdiBatteryCharging20,
- mdiBatteryCharging30,
- mdiBatteryCharging40,
- mdiBatteryCharging50,
- mdiBatteryCharging60,
- mdiBatteryCharging70,
- mdiBatteryCharging80,
- mdiBatteryCharging90,
- mdiBatteryChargingOutline,
- mdiBatteryUnknown,
-} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
const BATTERY_ICONS = {
- 10: mdiBattery10,
- 20: mdiBattery20,
- 30: mdiBattery30,
- 40: mdiBattery40,
- 50: mdiBattery50,
- 60: mdiBattery60,
- 70: mdiBattery70,
- 80: mdiBattery80,
- 90: mdiBattery90,
- 100: mdiBattery,
+ 10: "mdi:battery-10",
+ 20: "mdi:battery-20",
+ 30: "mdi:battery-30",
+ 40: "mdi:battery-40",
+ 50: "mdi:battery-50",
+ 60: "mdi:battery-60",
+ 70: "mdi:battery-70",
+ 80: "mdi:battery-80",
+ 90: "mdi:battery-90",
+ 100: "mdi:battery",
};
const BATTERY_CHARGING_ICONS = {
- 10: mdiBatteryCharging10,
- 20: mdiBatteryCharging20,
- 30: mdiBatteryCharging30,
- 40: mdiBatteryCharging40,
- 50: mdiBatteryCharging50,
- 60: mdiBatteryCharging60,
- 70: mdiBatteryCharging70,
- 80: mdiBatteryCharging80,
- 90: mdiBatteryCharging90,
- 100: mdiBatteryCharging,
+ 10: "mdi:battery-charging-10",
+ 20: "mdi:battery-charging-20",
+ 30: "mdi:battery-charging-30",
+ 40: "mdi:battery-charging-40",
+ 50: "mdi:battery-charging-50",
+ 60: "mdi:battery-charging-60",
+ 70: "mdi:battery-charging-70",
+ 80: "mdi:battery-charging-80",
+ 90: "mdi:battery-charging-90",
+ 100: "mdi:battery-charging",
};
-export const batteryStateIcon = (
- batteryState: HassEntity,
- batteryChargingState?: HassEntity
-) => {
- const battery = batteryState.state;
- const batteryCharging =
- batteryChargingState && batteryChargingState.state === "on";
-
- return batteryIcon(battery, batteryCharging);
+export const batteryIcon = (stateObj: HassEntity, state?: string) => {
+ const level = state ?? stateObj.state;
+ return batteryLevelIcon(level);
};
-export const batteryIcon = (
- batteryState: number | string,
- batteryCharging?: boolean
-) => {
- const batteryValue = Number(batteryState);
+export const batteryLevelIcon = (
+ batteryLevel: number | string,
+ isBatteryCharging?: boolean
+): string => {
+ const batteryValue = Number(batteryLevel);
if (isNaN(batteryValue)) {
- if (batteryState === "off") {
- return mdiBattery;
+ if (batteryLevel === "off") {
+ return "mdi:battery";
}
- if (batteryState === "on") {
- return mdiBatteryAlert;
+ if (batteryLevel === "on") {
+ return "mdi:battery-alert";
}
- return mdiBatteryUnknown;
+ return "mdi:battery-unknown";
}
const batteryRound = Math.round(batteryValue / 10) * 10;
- if (batteryCharging && batteryValue >= 10) {
+ if (isBatteryCharging && batteryValue >= 10) {
return BATTERY_CHARGING_ICONS[batteryRound];
}
- if (batteryCharging) {
- return mdiBatteryChargingOutline;
+ if (isBatteryCharging) {
+ return "mdi:battery-charging-outline";
}
if (batteryValue <= 5) {
- return mdiBatteryAlertVariantOutline;
+ return "mdi:battery-alert-variant-outline";
}
return BATTERY_ICONS[batteryRound];
};
diff --git a/src/common/entity/cover_icon.ts b/src/common/entity/cover_icon.ts
index bee06d61015f..53a30db752ca 100644
--- a/src/common/entity/cover_icon.ts
+++ b/src/common/entity/cover_icon.ts
@@ -1,132 +1,12 @@
/** Return an icon representing a cover state. */
import {
- mdiArrowUpBox,
- mdiArrowDownBox,
- mdiGarage,
- mdiGarageOpen,
- mdiGateArrowRight,
- mdiGate,
- mdiGateOpen,
- mdiDoorOpen,
- mdiDoorClosed,
- mdiCircle,
- mdiWindowShutter,
- mdiWindowShutterOpen,
- mdiBlindsHorizontal,
- mdiBlindsHorizontalClosed,
- mdiRollerShade,
- mdiRollerShadeClosed,
- mdiWindowClosed,
- mdiWindowOpen,
- mdiArrowExpandHorizontal,
- mdiArrowUp,
mdiArrowCollapseHorizontal,
mdiArrowDown,
- mdiCircleSlice8,
- mdiArrowSplitVertical,
- mdiCurtains,
- mdiCurtainsClosed,
+ mdiArrowExpandHorizontal,
+ mdiArrowUp,
} from "@mdi/js";
import { HassEntity } from "home-assistant-js-websocket";
-export const coverIcon = (state?: string, stateObj?: HassEntity): string => {
- const open = state !== "closed";
-
- switch (stateObj?.attributes.device_class) {
- case "garage":
- switch (state) {
- case "opening":
- return mdiArrowUpBox;
- case "closing":
- return mdiArrowDownBox;
- case "closed":
- return mdiGarage;
- default:
- return mdiGarageOpen;
- }
- case "gate":
- switch (state) {
- case "opening":
- case "closing":
- return mdiGateArrowRight;
- case "closed":
- return mdiGate;
- default:
- return mdiGateOpen;
- }
- case "door":
- return open ? mdiDoorOpen : mdiDoorClosed;
- case "damper":
- return open ? mdiCircle : mdiCircleSlice8;
- case "shutter":
- switch (state) {
- case "opening":
- return mdiArrowUpBox;
- case "closing":
- return mdiArrowDownBox;
- case "closed":
- return mdiWindowShutter;
- default:
- return mdiWindowShutterOpen;
- }
- case "curtain":
- switch (state) {
- case "opening":
- return mdiArrowSplitVertical;
- case "closing":
- return mdiArrowCollapseHorizontal;
- case "closed":
- return mdiCurtainsClosed;
- default:
- return mdiCurtains;
- }
- case "blind":
- switch (state) {
- case "opening":
- return mdiArrowUpBox;
- case "closing":
- return mdiArrowDownBox;
- case "closed":
- return mdiBlindsHorizontalClosed;
- default:
- return mdiBlindsHorizontal;
- }
- case "shade":
- switch (state) {
- case "opening":
- return mdiArrowUpBox;
- case "closing":
- return mdiArrowDownBox;
- case "closed":
- return mdiRollerShadeClosed;
- default:
- return mdiRollerShade;
- }
- case "window":
- switch (state) {
- case "opening":
- return mdiArrowUpBox;
- case "closing":
- return mdiArrowDownBox;
- case "closed":
- return mdiWindowClosed;
- default:
- return mdiWindowOpen;
- }
- }
-
- switch (state) {
- case "opening":
- return mdiArrowUpBox;
- case "closing":
- return mdiArrowDownBox;
- case "closed":
- return mdiWindowClosed;
- default:
- return mdiWindowOpen;
- }
-};
-
export const computeOpenIcon = (stateObj: HassEntity): string => {
switch (stateObj.attributes.device_class) {
case "awning":
diff --git a/src/common/entity/device_tracker_icon.ts b/src/common/entity/device_tracker_icon.ts
new file mode 100644
index 000000000000..02789242391f
--- /dev/null
+++ b/src/common/entity/device_tracker_icon.ts
@@ -0,0 +1,16 @@
+import { HassEntity } from "home-assistant-js-websocket";
+
+export const deviceTrackerIcon = (stateObj: HassEntity, state?: string) => {
+ const compareState = state ?? stateObj.state;
+ if (stateObj?.attributes.source_type === "router") {
+ return compareState === "home" ? "mdi:lan-connect" : "mdi:lan-disconnect";
+ }
+ if (
+ ["bluetooth", "bluetooth_le"].includes(stateObj?.attributes.source_type)
+ ) {
+ return compareState === "home" ? "mdi:bluetooth-connect" : "mdi:bluetooth";
+ }
+ return compareState === "not_home"
+ ? "mdi:account-arrow-right"
+ : "mdi:account";
+};
diff --git a/src/common/entity/domain_icon.ts b/src/common/entity/domain_icon.ts
deleted file mode 100644
index dfbbc69a5e17..000000000000
--- a/src/common/entity/domain_icon.ts
+++ /dev/null
@@ -1,301 +0,0 @@
-import {
- mdiAccount,
- mdiAccountArrowRight,
- mdiAirHumidifier,
- mdiAirHumidifierOff,
- mdiAudioVideo,
- mdiAudioVideoOff,
- mdiBluetooth,
- mdiBluetoothConnect,
- mdiButtonPointer,
- mdiCalendar,
- mdiCast,
- mdiCastConnected,
- mdiCastOff,
- mdiChartSankey,
- mdiCheckCircleOutline,
- mdiClock,
- mdiCloseCircleOutline,
- mdiCrosshairsQuestion,
- mdiDoorbell,
- mdiEyeCheck,
- mdiFan,
- mdiFanOff,
- mdiGestureTapButton,
- mdiLanConnect,
- mdiLanDisconnect,
- mdiLock,
- mdiLockAlert,
- mdiLockClock,
- mdiLockOpen,
- mdiMeterGas,
- mdiMotionSensor,
- mdiPackage,
- mdiPackageDown,
- mdiPackageUp,
- mdiPipeValve,
- mdiPowerPlug,
- mdiPowerPlugOff,
- mdiRestart,
- mdiRobot,
- mdiRobotConfused,
- mdiRobotOff,
- mdiSpeaker,
- mdiSpeakerOff,
- mdiSpeakerPause,
- mdiSpeakerPlay,
- mdiSwapHorizontal,
- mdiTelevision,
- mdiTelevisionOff,
- mdiTelevisionPause,
- mdiTelevisionPlay,
- mdiToggleSwitchVariant,
- mdiToggleSwitchVariantOff,
- mdiVideo,
- mdiVideoOff,
- mdiWaterBoiler,
- mdiWaterBoilerOff,
- mdiWeatherNight,
- mdiWhiteBalanceSunny,
-} from "@mdi/js";
-import { HassEntity } from "home-assistant-js-websocket";
-import { UpdateEntity, updateIsInstalling } from "../../data/update";
-import { weatherIcon } from "../../data/weather";
-/**
- * Return the icon to be used for a domain.
- *
- * Optionally pass in a state to influence the domain icon.
- */
-import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../const";
-import { alarmPanelIcon } from "./alarm_panel_icon";
-import { binarySensorIcon } from "./binary_sensor_icon";
-import { coverIcon } from "./cover_icon";
-import { numberIcon } from "./number_icon";
-import { sensorIcon } from "./sensor_icon";
-
-export const domainIcon = (
- domain: string,
- stateObj?: HassEntity,
- state?: string
-): string => {
- const icon = domainIconWithoutDefault(domain, stateObj, state);
- if (icon) {
- return icon;
- }
- // eslint-disable-next-line
- console.warn(`Unable to find icon for domain ${domain}`);
- return DEFAULT_DOMAIN_ICON;
-};
-
-export const domainIconWithoutDefault = (
- domain: string,
- stateObj?: HassEntity,
- state?: string
-): string | undefined => {
- const compareState = state !== undefined ? state : stateObj?.state;
-
- switch (domain) {
- case "alarm_control_panel":
- return alarmPanelIcon(compareState);
-
- case "automation":
- return compareState === "unavailable"
- ? mdiRobotConfused
- : compareState === "off"
- ? mdiRobotOff
- : mdiRobot;
-
- case "binary_sensor":
- return binarySensorIcon(compareState, stateObj);
-
- case "button":
- switch (stateObj?.attributes.device_class) {
- case "identify":
- return mdiCrosshairsQuestion;
- case "restart":
- return mdiRestart;
- case "update":
- return mdiPackageUp;
- default:
- return mdiButtonPointer;
- }
-
- case "camera":
- return compareState === "off" ? mdiVideoOff : mdiVideo;
-
- case "cover":
- return coverIcon(compareState, stateObj);
-
- case "device_tracker":
- if (stateObj?.attributes.source_type === "router") {
- return compareState === "home" ? mdiLanConnect : mdiLanDisconnect;
- }
- if (
- ["bluetooth", "bluetooth_le"].includes(stateObj?.attributes.source_type)
- ) {
- return compareState === "home" ? mdiBluetoothConnect : mdiBluetooth;
- }
- return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
-
- case "event":
- switch (stateObj?.attributes.device_class) {
- case "doorbell":
- return mdiDoorbell;
- case "button":
- return mdiGestureTapButton;
- case "motion":
- return mdiMotionSensor;
- default:
- return mdiEyeCheck;
- }
-
- case "fan":
- return compareState === "off" ? mdiFanOff : mdiFan;
-
- case "humidifier":
- return compareState === "off" ? mdiAirHumidifierOff : mdiAirHumidifier;
-
- case "input_boolean":
- return compareState === "on"
- ? mdiCheckCircleOutline
- : mdiCloseCircleOutline;
-
- case "input_datetime":
- if (!stateObj?.attributes.has_date) {
- return mdiClock;
- }
- if (!stateObj.attributes.has_time) {
- return mdiCalendar;
- }
- break;
-
- case "lock":
- switch (compareState) {
- case "unlocked":
- return mdiLockOpen;
- case "jammed":
- return mdiLockAlert;
- case "locking":
- case "unlocking":
- return mdiLockClock;
- default:
- return mdiLock;
- }
-
- case "media_player":
- switch (stateObj?.attributes.device_class) {
- case "speaker":
- switch (compareState) {
- case "playing":
- return mdiSpeakerPlay;
- case "paused":
- return mdiSpeakerPause;
- case "off":
- return mdiSpeakerOff;
- default:
- return mdiSpeaker;
- }
- case "tv":
- switch (compareState) {
- case "playing":
- return mdiTelevisionPlay;
- case "paused":
- return mdiTelevisionPause;
- case "off":
- return mdiTelevisionOff;
- default:
- return mdiTelevision;
- }
- case "receiver":
- switch (compareState) {
- case "off":
- return mdiAudioVideoOff;
- default:
- return mdiAudioVideo;
- }
- default:
- switch (compareState) {
- case "playing":
- case "paused":
- return mdiCastConnected;
- case "off":
- return mdiCastOff;
- default:
- return mdiCast;
- }
- }
-
- case "number": {
- const icon = numberIcon(stateObj);
- if (icon) {
- return icon;
- }
-
- break;
- }
-
- case "person":
- return compareState === "not_home" ? mdiAccountArrowRight : mdiAccount;
-
- case "switch":
- switch (stateObj?.attributes.device_class) {
- case "outlet":
- return compareState === "on" ? mdiPowerPlug : mdiPowerPlugOff;
- case "switch":
- return compareState === "on"
- ? mdiToggleSwitchVariant
- : mdiToggleSwitchVariantOff;
- default:
- return mdiToggleSwitchVariant;
- }
-
- case "sensor": {
- const icon = sensorIcon(stateObj);
- if (icon) {
- return icon;
- }
-
- break;
- }
-
- case "sun":
- return stateObj?.state === "above_horizon"
- ? mdiWhiteBalanceSunny
- : mdiWeatherNight;
-
- case "switch_as_x":
- return mdiSwapHorizontal;
-
- case "threshold":
- return mdiChartSankey;
-
- case "update":
- return compareState === "on"
- ? updateIsInstalling(stateObj as UpdateEntity)
- ? mdiPackageDown
- : mdiPackageUp
- : mdiPackage;
-
- case "valve":
- switch (stateObj?.attributes.device_class) {
- case "water":
- return mdiPipeValve;
- case "gas":
- return mdiMeterGas;
- default:
- return mdiPipeValve;
- }
-
- case "water_heater":
- return compareState === "off" ? mdiWaterBoilerOff : mdiWaterBoiler;
-
- case "weather":
- return weatherIcon(stateObj?.state);
- }
-
- if (domain in FIXED_DOMAIN_ICONS) {
- return FIXED_DOMAIN_ICONS[domain];
- }
-
- return undefined;
-};
diff --git a/src/common/entity/number_icon.ts b/src/common/entity/number_icon.ts
deleted file mode 100644
index c468be6fed56..000000000000
--- a/src/common/entity/number_icon.ts
+++ /dev/null
@@ -1,13 +0,0 @@
-/** Return an icon representing a number state. */
-import { HassEntity } from "home-assistant-js-websocket";
-import { FIXED_DEVICE_CLASS_ICONS } from "../const";
-
-export const numberIcon = (stateObj?: HassEntity): string | undefined => {
- const dclass = stateObj?.attributes.device_class;
-
- if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) {
- return FIXED_DEVICE_CLASS_ICONS[dclass];
- }
-
- return undefined;
-};
diff --git a/src/common/entity/sensor_icon.ts b/src/common/entity/sensor_icon.ts
deleted file mode 100644
index 9af29ceb3e7d..000000000000
--- a/src/common/entity/sensor_icon.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-/** Return an icon representing a sensor state. */
-import { mdiBattery, mdiThermometer } from "@mdi/js";
-import { HassEntity } from "home-assistant-js-websocket";
-import { SENSOR_DEVICE_CLASS_BATTERY } from "../../data/sensor";
-import { FIXED_DEVICE_CLASS_ICONS, UNIT_C, UNIT_F } from "../const";
-import { batteryStateIcon } from "./battery_icon";
-
-export const sensorIcon = (stateObj?: HassEntity): string | undefined => {
- const dclass = stateObj?.attributes.device_class;
-
- if (dclass && dclass in FIXED_DEVICE_CLASS_ICONS) {
- return FIXED_DEVICE_CLASS_ICONS[dclass];
- }
-
- if (dclass === SENSOR_DEVICE_CLASS_BATTERY) {
- return stateObj ? batteryStateIcon(stateObj) : mdiBattery;
- }
-
- const unit = stateObj?.attributes.unit_of_measurement;
- if (unit === UNIT_C || unit === UNIT_F) {
- return mdiThermometer;
- }
-
- return undefined;
-};
diff --git a/src/common/entity/state_icon.ts b/src/common/entity/state_icon.ts
new file mode 100644
index 000000000000..f513c652c77c
--- /dev/null
+++ b/src/common/entity/state_icon.ts
@@ -0,0 +1,59 @@
+import { HassEntity } from "home-assistant-js-websocket";
+import { computeStateDomain } from "./compute_state_domain";
+import { updateIcon } from "./update_icon";
+import { deviceTrackerIcon } from "./device_tracker_icon";
+import { batteryIcon } from "./battery_icon";
+
+export const stateIcon = (
+ stateObj: HassEntity,
+ state?: string
+): string | undefined => {
+ const domain = computeStateDomain(stateObj);
+ const compareState = state ?? stateObj?.state;
+ const dc = stateObj?.attributes.device_class;
+ switch (domain) {
+ case "automation":
+ return compareState === "unavailable"
+ ? "mdi:robot-confused"
+ : compareState === "off"
+ ? "mdi:robot-off"
+ : "mdi:robot";
+
+ case "update":
+ return updateIcon(stateObj, compareState);
+
+ case "sensor":
+ if (dc === "battery") {
+ return batteryIcon(stateObj, compareState);
+ }
+ break;
+
+ case "device_tracker":
+ return deviceTrackerIcon(stateObj, compareState);
+
+ case "person":
+ return compareState === "not_home"
+ ? "mdi:account-arrow-right"
+ : "mdi:account";
+
+ case "sun":
+ return compareState === "above_horizon"
+ ? "mdi:white-balance-sunny"
+ : "mdi:weather-night";
+
+ case "input_boolean":
+ return compareState === "on"
+ ? "mdi:check-circle-outline"
+ : "mdi:close-circle-outline";
+
+ case "input_datetime":
+ if (!stateObj?.attributes.has_date) {
+ return "mdi:clock";
+ }
+ if (!stateObj.attributes.has_time) {
+ return "mdi:calendar";
+ }
+ break;
+ }
+ return undefined;
+};
diff --git a/src/common/entity/state_icon_path.ts b/src/common/entity/state_icon_path.ts
deleted file mode 100644
index ac8e2c47ad46..000000000000
--- a/src/common/entity/state_icon_path.ts
+++ /dev/null
@@ -1,12 +0,0 @@
-/** Return an icon representing a state. */
-import { HassEntity } from "home-assistant-js-websocket";
-import { DEFAULT_DOMAIN_ICON } from "../const";
-import { computeDomain } from "./compute_domain";
-import { domainIcon } from "./domain_icon";
-
-export const stateIconPath = (state: HassEntity | undefined) => {
- if (!state) {
- return DEFAULT_DOMAIN_ICON;
- }
- return domainIcon(computeDomain(state.entity_id), state);
-};
diff --git a/src/common/entity/update_icon.ts b/src/common/entity/update_icon.ts
new file mode 100644
index 000000000000..4e1915d7fcad
--- /dev/null
+++ b/src/common/entity/update_icon.ts
@@ -0,0 +1,11 @@
+import { HassEntity } from "home-assistant-js-websocket";
+import { UpdateEntity, updateIsInstalling } from "../../data/update";
+
+export const updateIcon = (stateObj: HassEntity, state?: string) => {
+ const compareState = state ?? stateObj.state;
+ return compareState === "on"
+ ? updateIsInstalling(stateObj as UpdateEntity)
+ ? "mdi:package-down"
+ : "mdi:package-up"
+ : "mdi:package";
+};
diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts
index 44c49ea1090e..7f3d2f61b89b 100644
--- a/src/components/data-table/ha-data-table.ts
+++ b/src/components/data-table/ha-data-table.ts
@@ -764,10 +764,12 @@ export class HaDataTable extends LitElement {
text-align: right;
}
- .mdc-data-table__cell--icon:first-child ha-icon,
.mdc-data-table__cell--icon:first-child img,
+ .mdc-data-table__cell--icon:first-child ha-icon,
+ .mdc-data-table__cell--icon:first-child ha-svg-icon,
.mdc-data-table__cell--icon:first-child ha-state-icon,
- .mdc-data-table__cell--icon:first-child ha-svg-icon {
+ .mdc-data-table__cell--icon:first-child ha-domain-icon,
+ .mdc-data-table__cell--icon:first-child ha-service-icon {
margin-left: 8px;
}
:host([dir="rtl"]) .mdc-data-table__cell--icon:first-child ha-icon,
diff --git a/src/components/entity/ha-battery-icon.ts b/src/components/entity/ha-battery-icon.ts
index 814f9d332e43..41f6a464f507 100644
--- a/src/components/entity/ha-battery-icon.ts
+++ b/src/components/entity/ha-battery-icon.ts
@@ -1,22 +1,25 @@
-import { html, LitElement } from "lit";
+import { HassEntity } from "home-assistant-js-websocket";
+import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
-import { batteryStateIcon } from "../../common/entity/battery_icon";
-import "../ha-svg-icon";
+import { batteryLevelIcon } from "../../common/entity/battery_icon";
+import "../ha-icon";
@customElement("ha-battery-icon")
export class HaBatteryIcon extends LitElement {
- @property() public batteryStateObj;
+ @property({ attribute: false }) public batteryStateObj?: HassEntity;
- @property() public batteryChargingStateObj;
+ @property({ attribute: false }) public batteryChargingStateObj?: HassEntity;
protected render() {
+ if (!this.batteryStateObj) return nothing;
+
return html`
-
+ >
`;
}
}
diff --git a/src/components/ha-domain-icon.ts b/src/components/ha-domain-icon.ts
new file mode 100644
index 000000000000..aa43ff45bfba
--- /dev/null
+++ b/src/components/ha-domain-icon.ts
@@ -0,0 +1,81 @@
+import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
+import { customElement, property } from "lit/decorators";
+import { until } from "lit/directives/until";
+import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../common/const";
+import { domainIcon } from "../data/icons";
+import { HomeAssistant } from "../types";
+import { brandsUrl } from "../util/brands-url";
+import "./ha-icon";
+
+@customElement("ha-domain-icon")
+export class HaDomainIcon extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public domain?: string;
+
+ @property() public icon?: string;
+
+ @property({ type: Boolean }) public brandFallback?: boolean;
+
+ protected render() {
+ if (this.icon) {
+ return html``;
+ }
+
+ if (!this.domain) {
+ return nothing;
+ }
+
+ if (!this.hass) {
+ return this._renderFallback();
+ }
+
+ const icon = domainIcon(this.hass, this.domain).then((icn) => {
+ if (icn) {
+ return html``;
+ }
+ return this._renderFallback();
+ });
+
+ return html`${until(icon)}`;
+ }
+
+ private _renderFallback() {
+ if (this.domain! in FIXED_DOMAIN_ICONS) {
+ return html`
+
+ `;
+ }
+ if (this.brandFallback) {
+ const image = brandsUrl({
+ domain: this.domain!,
+ type: "icon",
+ darkOptimized: this.hass.themes?.darkMode,
+ });
+ return html`
+
+ `;
+ }
+ return html``;
+ }
+
+ static get styles(): CSSResultGroup {
+ return css`
+ img {
+ width: var(--mdc-icon-size, 24px);
+ }
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-domain-icon": HaDomainIcon;
+ }
+}
diff --git a/src/components/ha-service-icon.ts b/src/components/ha-service-icon.ts
index 780d3b81ac90..663043639edb 100644
--- a/src/components/ha-service-icon.ts
+++ b/src/components/ha-service-icon.ts
@@ -1,9 +1,8 @@
-import { mdiRoomService } from "@mdi/js";
import { html, LitElement, nothing } from "lit";
import { customElement, property } from "lit/decorators";
import { until } from "lit/directives/until";
+import { DEFAULT_SERVICE_ICON, FIXED_DOMAIN_ICONS } from "../common/const";
import { computeDomain } from "../common/entity/compute_domain";
-import { domainIconWithoutDefault } from "../common/entity/domain_icon";
import { serviceIcon } from "../data/icons";
import { HomeAssistant } from "../types";
import "./ha-icon";
@@ -41,10 +40,11 @@ export class HaServiceIcon extends LitElement {
}
private _renderFallback() {
+ const domain = computeDomain(this.service!);
+
return html`
`;
}
diff --git a/src/components/ha-state-icon.ts b/src/components/ha-state-icon.ts
index 2589dc318f81..aa6371ec706b 100644
--- a/src/components/ha-state-icon.ts
+++ b/src/components/ha-state-icon.ts
@@ -2,7 +2,8 @@ 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 { stateIconPath } from "../common/entity/state_icon_path";
+import { DEFAULT_DOMAIN_ICON, FIXED_DOMAIN_ICONS } from "../common/const";
+import { computeStateDomain } from "../common/entity/compute_state_domain";
import { entityIcon } from "../data/icons";
import { HomeAssistant } from "../types";
import "./ha-icon";
@@ -44,9 +45,13 @@ export class HaStateIcon extends LitElement {
}
private _renderFallback() {
- return html``;
+ const domain = computeStateDomain(this.stateObj!);
+
+ return html`
+
+ `;
}
}
diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts
index db2f800888e5..29aad8056436 100644
--- a/src/components/trace/hat-script-graph.ts
+++ b/src/components/trace/hat-script-graph.ts
@@ -17,10 +17,11 @@ import {
mdiRoomService,
mdiShuffleDisabled,
} from "@mdi/js";
-import { css, html, LitElement, PropertyValues } from "lit";
+import { LitElement, PropertyValues, css, html } from "lit";
import { customElement, property } from "lit/decorators";
-import { fireEvent } from "../../common/dom/fire_event";
import { ensureArray } from "../../common/array/ensure-array";
+import { fireEvent } from "../../common/dom/fire_event";
+import { ACTION_ICONS } from "../../data/action";
import { Condition, Trigger } from "../../data/automation";
import {
Action,
@@ -45,7 +46,6 @@ import "./hat-graph-branch";
import { BRANCH_HEIGHT, NODE_SIZE, SPACING } from "./hat-graph-const";
import "./hat-graph-node";
import "./hat-graph-spacer";
-import { ACTION_ICONS } from "../../data/action";
export interface NodeInfo {
path: string;
diff --git a/src/data/icons.ts b/src/data/icons.ts
index 5e8f4acae7cb..85b712384ce7 100644
--- a/src/data/icons.ts
+++ b/src/data/icons.ts
@@ -2,6 +2,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id";
import { computeStateDomain } from "../common/entity/compute_state_domain";
+import { stateIcon } from "../common/entity/state_icon";
import { HomeAssistant } from "../types";
import {
EntityRegistryDisplayEntry,
@@ -49,7 +50,7 @@ interface ComponentIcons {
}
interface ServiceIcons {
- [domain: string]: Record;
+ [service: string]: string;
}
export type IconCategory = "entity" | "entity_component" | "services";
@@ -127,26 +128,18 @@ export const getServiceIcons = async (
export const entityIcon = async (
hass: HomeAssistant,
- state: HassEntity,
- stateValue?: string
+ stateObj: HassEntity,
+ state?: string
) => {
- const entity = hass.entities?.[state.entity_id] as
+ const entry = hass.entities?.[stateObj.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
- if (entity?.icon) {
- return entity.icon;
+ if (entry?.icon) {
+ return entry.icon;
}
- const domain = computeStateDomain(state);
- const deviceClass = state.attributes.device_class;
+ const domain = computeStateDomain(stateObj);
- return getEntityIcon(
- hass,
- domain,
- deviceClass,
- stateValue ?? state.state,
- entity?.platform,
- entity?.translation_key
- );
+ return getEntityIcon(hass, domain, stateObj, state, entry);
};
export const entryIcon = async (
@@ -157,39 +150,41 @@ export const entryIcon = async (
return entry.icon;
}
const domain = computeDomain(entry.entity_id);
- return getEntityIcon(
- hass,
- domain,
- undefined,
- undefined,
- entry.platform,
- entry.translation_key
- );
+ return getEntityIcon(hass, domain, undefined, undefined, entry);
};
const getEntityIcon = async (
hass: HomeAssistant,
domain: string,
- deviceClass?: string,
- value?: string,
- platform?: string,
- translation_key?: string
+ stateObj?: HassEntity,
+ stateValue?: string,
+ entry?: EntityRegistryEntry | EntityRegistryDisplayEntry
) => {
+ const platform = entry?.platform;
+ const translation_key = entry?.translation_key;
+ const device_class = stateObj?.attributes.device_class;
+ const state = stateValue ?? stateObj?.state;
+
let icon: string | undefined;
if (translation_key && platform) {
const platformIcons = await getPlatformIcons(hass, platform);
if (platformIcons) {
const translations = platformIcons[domain]?.[translation_key];
- icon = (value && translations?.state?.[value]) || translations?.default;
+ icon = (state && translations?.state?.[state]) || translations?.default;
}
}
+
+ if (!icon && stateObj) {
+ icon = stateIcon(stateObj, state);
+ }
+
if (!icon) {
const entityComponentIcons = await getComponentIcons(hass, domain);
if (entityComponentIcons) {
const translations =
- (deviceClass && entityComponentIcons[deviceClass]) ||
+ (device_class && entityComponentIcons[device_class]) ||
entityComponentIcons._;
- icon = (value && translations?.state?.[value]) || translations?.default;
+ icon = (state && translations?.state?.[state]) || translations?.default;
}
}
return icon;
@@ -234,12 +229,30 @@ export const attributeIcon = async (
return icon;
};
-export const serviceIcon = async (hass: HomeAssistant, service: string) => {
+export const serviceIcon = async (
+ hass: HomeAssistant,
+ service: string
+): Promise => {
+ let icon: string | undefined;
const domain = computeDomain(service);
const serviceName = computeObjectId(service);
const serviceIcons = await getServiceIcons(hass, domain);
if (serviceIcons) {
- return serviceIcons[serviceName];
+ icon = serviceIcons[serviceName];
+ }
+ if (!icon) {
+ icon = await domainIcon(hass, domain);
+ }
+ return icon;
+};
+
+export const domainIcon = async (
+ hass: HomeAssistant,
+ domain: string
+): Promise => {
+ const entityComponentIcons = await getComponentIcons(hass, domain);
+ if (entityComponentIcons) {
+ return entityComponentIcons._?.default;
}
return undefined;
};
diff --git a/src/data/weather.ts b/src/data/weather.ts
index abded90d6d3b..8f02b7c5f334 100644
--- a/src/data/weather.ts
+++ b/src/data/weather.ts
@@ -8,7 +8,6 @@ import {
mdiWeatherLightning,
mdiWeatherLightningRainy,
mdiWeatherNight,
- mdiWeatherNightPartlyCloudy,
mdiWeatherPartlyCloudy,
mdiWeatherPouring,
mdiWeatherRainy,
@@ -520,13 +519,6 @@ export const getWeatherStateIcon = (
return undefined;
};
-export const weatherIcon = (state?: string, nightTime?: boolean): string =>
- !state
- ? undefined
- : nightTime && state === "partlycloudy"
- ? mdiWeatherNightPartlyCloudy
- : weatherIcons[state];
-
const EIGHT_HOURS = 28800000;
const DAY_IN_MILLISECONDS = 86400000;
diff --git a/src/dialogs/quick-bar/ha-quick-bar.ts b/src/dialogs/quick-bar/ha-quick-bar.ts
index 71da2ec0dcb6..da30fb7e9b4b 100644
--- a/src/dialogs/quick-bar/ha-quick-bar.ts
+++ b/src/dialogs/quick-bar/ha-quick-bar.ts
@@ -8,7 +8,7 @@ import {
mdiReload,
mdiServerNetwork,
} from "@mdi/js";
-import { LitElement, css, html, nothing } from "lit";
+import { LitElement, TemplateResult, css, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { ifDefined } from "lit/directives/if-defined";
import { styleMap } from "lit/directives/style-map";
@@ -17,9 +17,7 @@ import { canShowPage } from "../../common/config/can_show_page";
import { componentsWithService } from "../../common/config/components_with_service";
import { isComponentLoaded } from "../../common/config/is_component_loaded";
import { fireEvent } from "../../common/dom/fire_event";
-import { computeDomain } from "../../common/entity/compute_domain";
import { computeStateName } from "../../common/entity/compute_state_name";
-import { domainIcon } from "../../common/entity/domain_icon";
import { navigate } from "../../common/navigate";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import {
@@ -27,9 +25,9 @@ import {
fuzzyFilterSort,
} from "../../common/string/filter/sequence-matching";
import { debounce } from "../../common/util/debounce";
-import "../../components/ha-label";
import "../../components/ha-circular-progress";
import "../../components/ha-icon-button";
+import "../../components/ha-label";
import "../../components/ha-list-item";
import "../../components/ha-textfield";
import { fetchHassioAddonsInfo } from "../../data/hassio/addon";
@@ -56,7 +54,7 @@ interface CommandItem extends QuickBarItem {
interface EntityItem extends QuickBarItem {
altText: string;
- icon?: string;
+ icon?: TemplateResult;
}
const isCommandItem = (item: QuickBarItem): item is CommandItem =>
@@ -296,16 +294,14 @@ export class QuickBar extends LitElement {
graphic="icon"
>
${item.iconPath
- ? html``
- : html``}
+ ? html`
+
+ `
+ : html`${item.icon}`}
${item.primaryText}
${item.altText
? html`
@@ -476,10 +472,12 @@ export class QuickBar extends LitElement {
const entityItem = {
primaryText: computeStateName(entityState),
altText: entityId,
- icon: entityState.attributes.icon,
- iconPath: entityState.attributes.icon
- ? undefined
- : domainIcon(computeDomain(entityId), entityState),
+ icon: html`
+
+ `,
action: () => fireEvent(this, "hass-more-info", { entityId }),
};
diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts
index bd9a7681abef..4145a3397b50 100644
--- a/src/panels/config/automation/add-automation-element-dialog.ts
+++ b/src/panels/config/automation/add-automation-element-dialog.ts
@@ -1,4 +1,5 @@
import "@material/mwc-list/mwc-list";
+import "@material/web/divider/divider";
import { mdiClose, mdiContentPaste, mdiPlus } from "@mdi/js";
import Fuse, { IFuseOptions } from "fuse.js";
import {
@@ -16,17 +17,21 @@ import { repeat } from "lit/directives/repeat";
import { styleMap } from "lit/directives/style-map";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event";
-import { domainIconWithoutDefault } from "../../../common/entity/domain_icon";
+import { computeDomain } from "../../../common/entity/compute_domain";
import { stringCompare } from "../../../common/string/compare";
import { LocalizeFunc } from "../../../common/translations/localize";
+import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/ha-dialog";
import type { HaDialog } from "../../../components/ha-dialog";
import "../../../components/ha-dialog-header";
+import "../../../components/ha-domain-icon";
import "../../../components/ha-icon-button";
import "../../../components/ha-icon-button-prev";
import "../../../components/ha-icon-next";
-import "../../../components/ha-list-new";
import "../../../components/ha-list-item-new";
+import "../../../components/ha-list-new";
+import "../../../components/ha-service-icon";
+import "../../../components/search-input";
import {
ACTION_GROUPS,
ACTION_ICONS,
@@ -36,6 +41,7 @@ import {
} from "../../../data/action";
import { AutomationElementGroup } from "../../../data/automation";
import { CONDITION_GROUPS, CONDITION_ICONS } from "../../../data/condition";
+import { getServiceIcons } from "../../../data/icons";
import {
IntegrationManifest,
domainToName,
@@ -45,16 +51,10 @@ import { TRIGGER_GROUPS, TRIGGER_ICONS } from "../../../data/trigger";
import { HassDialog } from "../../../dialogs/make-dialog-manager";
import { haStyle, haStyleDialog } from "../../../resources/styles";
import { HomeAssistant } from "../../../types";
-import { brandsUrl } from "../../../util/brands-url";
import {
AddAutomationElementDialogParams,
PASTE_VALUE,
} from "./show-add-automation-element-dialog";
-import { computeDomain } from "../../../common/entity/compute_domain";
-import { deepEqual } from "../../../common/util/deep-equal";
-import "../../../components/search-input";
-import "@material/web/divider/divider";
-import { getServiceIcons } from "../../../data/icons";
const TYPES = {
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
@@ -74,7 +74,6 @@ interface ListItem {
description: string;
iconPath?: string;
icon?: TemplateResult;
- image?: string;
group: boolean;
}
@@ -318,17 +317,15 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
(!domainUsed && manifest?.integration_type === "entity") ||
!["helper", "entity"].includes(manifest?.integration_type || "")))
) {
- const icon = domainIconWithoutDefault(domain);
result.push({
group: true,
- iconPath: icon,
- image: !icon
- ? brandsUrl({
- domain,
- type: "icon",
- darkOptimized: this.hass.themes?.darkMode,
- })
- : undefined,
+ icon: html`
+
+ `,
key: `${SERVICE_PREFIX}${domain}`,
name: domainToName(localize, domain, manifest),
description: "",
@@ -364,10 +361,12 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
for (const service of services_keys) {
result.push({
group: false,
- icon: html``,
+ icon: html`
+
+ `,
key: `${SERVICE_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
@@ -578,13 +577,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
slot="start"
.path=${item.iconPath}
>`
- : html``}
+ : nothing}
${item.group
? html``
: html`
+
${
this.narrow
? html`
-
+
+ ${this._renderIcon(isBrowser, stateObj)}
+
`
: html`
-
+
+ ${this._renderIcon(isBrowser, stateObj)}
+
`
}
-
${this.hass.localize("ui.components.media-browser.web-browser")}
-
+
${this._mediaPlayerEntities.map(
(source) => html`
-
${computeStateName(source)}
-
+
`
)}
@@ -382,6 +377,23 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
`;
}
+ private _renderIcon(isBrowser: boolean, stateObj?: MediaPlayerEntity) {
+ if (isBrowser) {
+ return html``;
+ }
+ if (stateObj) {
+ return html`
+
+ `;
+ }
+ return html`
+
+ `;
+ }
+
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (changedProps.has("entityId")) {
@@ -571,9 +583,10 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
--mdc-theme-primary: var(--secondary-text-color);
}
- mwc-button[slot="trigger"] {
+ ha-button-menu ha-button[slot="trigger"] {
+ line-height: 1;
--mdc-theme-primary: var(--primary-text-color);
- --mdc-icon-size: 36px;
+ --mdc-icon-size: 16px;
}
.info {
@@ -650,10 +663,6 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
margin: 16px 0 16px 16px;
}
- ha-button-menu mwc-button {
- line-height: 1;
- }
-
:host([narrow]) {
height: 57px;
}
@@ -696,10 +705,15 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
left: 0;
}
- mwc-list-item[selected] {
+ ha-list-item[selected] {
font-weight: bold;
}
+ span[slot="icon"] {
+ display: flex;
+ align-items: center;
+ }
+
ha-svg-icon[slot="trailingIcon"] {
margin-inline-start: 8px !important;
margin-inline-end: 0px !important;