diff --git a/src/components/ha-service-control.ts b/src/components/ha-service-control.ts
index 12b5c8bd5a0a..7ec32d9b733d 100644
--- a/src/components/ha-service-control.ts
+++ b/src/components/ha-service-control.ts
@@ -9,8 +9,8 @@ import {
CSSResultGroup,
html,
LitElement,
- PropertyValues,
nothing,
+ PropertyValues,
} from "lit";
import { customElement, property, query, state } from "lit/decorators";
import memoizeOne from "memoize-one";
@@ -19,6 +19,7 @@ import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id";
import { supportsFeature } from "../common/entity/supports-feature";
+import { nestedArrayMove } from "../common/util/array-move";
import {
fetchIntegrationManifest,
IntegrationManifest,
@@ -31,6 +32,7 @@ import {
expandDeviceTarget,
Selector,
} from "../data/selector";
+import { ReorderModeMixin } from "../state/reorder-mode-mixin";
import { HomeAssistant, ValueChangedEvent } from "../types";
import { documentationUrl } from "../util/documentation-url";
import "./ha-checkbox";
@@ -40,8 +42,6 @@ import "./ha-service-picker";
import "./ha-settings-row";
import "./ha-yaml-editor";
import type { HaYamlEditor } from "./ha-yaml-editor";
-import { nestedArrayMove } from "../common/util/array-move";
-import { ReorderModeMixin } from "../state/reorder-mode-mixin";
const attributeFilter = (values: any[], attribute: any) => {
if (typeof attribute === "object") {
diff --git a/src/components/ha-service-icon.ts b/src/components/ha-service-icon.ts
new file mode 100644
index 000000000000..780d3b81ac90
--- /dev/null
+++ b/src/components/ha-service-icon.ts
@@ -0,0 +1,57 @@
+import { mdiRoomService } from "@mdi/js";
+import { html, LitElement, nothing } from "lit";
+import { customElement, property } from "lit/decorators";
+import { until } from "lit/directives/until";
+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";
+import "./ha-svg-icon";
+
+@customElement("ha-service-icon")
+export class HaServiceIcon extends LitElement {
+ @property({ attribute: false }) public hass!: HomeAssistant;
+
+ @property() public service?: string;
+
+ @property() public icon?: string;
+
+ protected render() {
+ if (this.icon) {
+ return html``;
+ }
+
+ if (!this.service) {
+ return nothing;
+ }
+
+ if (!this.hass) {
+ return this._renderFallback();
+ }
+
+ const icon = serviceIcon(this.hass, this.service).then((icn) => {
+ if (icn) {
+ return html``;
+ }
+ return this._renderFallback();
+ });
+
+ return html`${until(icon)}`;
+ }
+
+ private _renderFallback() {
+ return html`
+
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ "ha-service-icon": HaServiceIcon;
+ }
+}
diff --git a/src/components/ha-service-picker.ts b/src/components/ha-service-picker.ts
index 2cfc0deee223..c370928521e1 100644
--- a/src/components/ha-service-picker.ts
+++ b/src/components/ha-service-picker.ts
@@ -1,4 +1,3 @@
-import "@material/mwc-list/mwc-list-item";
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -8,16 +7,9 @@ import { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration";
import { HomeAssistant } from "../types";
import "./ha-combo-box";
-
-const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
- item
-) =>
- html`
- ${item.name}
- ${item.name === item.service ? "" : item.service}
- `;
+import "./ha-list-item";
+import "./ha-service-icon";
+import { getServiceIcons } from "../data/icons";
@customElement("ha-service-picker")
class HaServicePicker extends LitElement {
@@ -32,9 +24,24 @@ class HaServicePicker extends LitElement {
protected willUpdate() {
if (!this.hasUpdated) {
this.hass.loadBackendTranslation("services");
+ getServiceIcons(this.hass);
}
}
+ private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
+ (item) =>
+ html`
+
+ ${item.name}
+ ${item.name === item.service ? "" : item.service}
+ `;
+
protected render() {
return html`
= {
entity: {},
entity_component: undefined,
+ services: {},
};
interface IconResources {
@@ -46,7 +48,11 @@ interface ComponentIcons {
};
}
-export type IconCategory = "entity" | "entity_component";
+interface ServiceIcons {
+ [domain: string]: Record;
+}
+
+export type IconCategory = "entity" | "entity_component" | "services";
export const getHassIcons = async (
hass: HomeAssistant,
@@ -64,7 +70,7 @@ export const getPlatformIcons = async (
integration: string,
force = false
): Promise => {
- if (!force && integration && integration in resources.entity) {
+ if (!force && integration in resources.entity) {
return resources.entity[integration];
}
const result = getHassIcons(hass, "entity", integration);
@@ -88,6 +94,37 @@ export const getComponentIcons = async (
return resources.entity_component.then((res) => res[domain]);
};
+export const getServiceIcons = async (
+ hass: HomeAssistant,
+ domain?: string,
+ force = false
+): Promise => {
+ if (!domain) {
+ if (!force && resources.services.all) {
+ return resources.services.all;
+ }
+ resources.services.all = getHassIcons(hass, "services", domain).then(
+ (res) => {
+ resources.services = res.resources;
+ return res?.resources;
+ }
+ );
+ return resources.services.all;
+ }
+ if (!force && domain && domain in resources.services) {
+ return resources.services[domain];
+ }
+ if (resources.services.all && !force) {
+ await resources.services.all;
+ if (domain in resources.services) {
+ return resources.services[domain];
+ }
+ }
+ const result = getHassIcons(hass, "services", domain);
+ resources.services[domain] = result.then((res) => res?.resources[domain]);
+ return resources.services[domain];
+};
+
export const entityIcon = async (
hass: HomeAssistant,
state: HassEntity,
@@ -96,7 +133,6 @@ export const entityIcon = async (
const entity = hass.entities?.[state.entity_id] as
| EntityRegistryDisplayEntry
| undefined;
-
if (entity?.icon) {
return entity.icon;
}
@@ -197,3 +233,13 @@ export const attributeIcon = async (
}
return icon;
};
+
+export const serviceIcon = async (hass: HomeAssistant, service: string) => {
+ const domain = computeDomain(service);
+ const serviceName = computeObjectId(service);
+ const serviceIcons = await getServiceIcons(hass, domain);
+ if (serviceIcons) {
+ return serviceIcons[serviceName];
+ }
+ return undefined;
+};
diff --git a/src/panels/config/automation/action/ha-automation-action-row.ts b/src/panels/config/automation/action/ha-automation-action-row.ts
index 69e811b0ba4d..49c99daecb55 100644
--- a/src/panels/config/automation/action/ha-automation-action-row.ts
+++ b/src/panels/config/automation/action/ha-automation-action-row.ts
@@ -29,13 +29,12 @@ import { classMap } from "lit/directives/class-map";
import { storage } from "../../../../common/decorators/storage";
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
import { fireEvent } from "../../../../common/dom/fire_event";
-import { computeDomain } from "../../../../common/entity/compute_domain";
-import { domainIconWithoutDefault } from "../../../../common/entity/domain_icon";
import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
import { handleStructError } from "../../../../common/structs/handle-errors";
import "../../../../components/ha-alert";
import "../../../../components/ha-button-menu";
import "../../../../components/ha-card";
+import "../../../../components/ha-service-icon";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
@@ -203,16 +202,18 @@ export default class HaAutomationActionRow extends LitElement {
: ""}
-
+ ${type === "service" &&
+ "service" in this.action &&
+ this.action.service
+ ? html``
+ : html``}
${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action)
)}
diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts
index f67a3a359650..bd9a7681abef 100644
--- a/src/panels/config/automation/add-automation-element-dialog.ts
+++ b/src/panels/config/automation/add-automation-element-dialog.ts
@@ -5,6 +5,7 @@ import {
CSSResultGroup,
LitElement,
PropertyValues,
+ TemplateResult,
css,
html,
nothing,
@@ -53,6 +54,7 @@ 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 },
@@ -70,7 +72,8 @@ interface ListItem {
key: string;
name: string;
description: string;
- icon?: string;
+ iconPath?: string;
+ icon?: TemplateResult;
image?: string;
group: boolean;
}
@@ -124,6 +127,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
this.hass.loadBackendTranslation("services");
this._fetchManifests();
this._calculateUsedDomains();
+ getServiceIcons(this.hass);
}
this._fullScreen = matchMedia(
"all and (max-width: 450px), all and (max-height: 500px)"
@@ -174,7 +178,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
options.members ? "groups" : "type"
}.${key}.description${options.members ? "" : ".picker"}`
),
- icon: options.icon || TYPES[type].icons[key],
+ iconPath: options.icon || TYPES[type].icons[key],
});
private _getFilteredItems = memoizeOne(
@@ -317,7 +321,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
const icon = domainIconWithoutDefault(domain);
result.push({
group: true,
- icon,
+ iconPath: icon,
image: !icon
? brandsUrl({
domain,
@@ -358,17 +362,12 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
const services_keys = Object.keys(services[dmn]);
for (const service of services_keys) {
- const icon = domainIconWithoutDefault(dmn);
result.push({
group: false,
- icon,
- image: !icon
- ? brandsUrl({
- domain: dmn,
- type: "icon",
- darkOptimized: this.hass.themes?.darkMode,
- })
- : undefined,
+ icon: html``,
key: `${SERVICE_PREFIX}${dmn}.${service}`,
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
@@ -573,17 +572,19 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
${item.name}
${item.description}
${item.icon
- ? html``
- : html``}
+ ? html`${item.icon}`
+ : item.iconPath
+ ? html``
+ : html``}
${item.group
? html``
: html`