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`