mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-26 02:36:37 +00:00
Add support for service icons (#19507)
* Add service icons * Fix lint --------- Co-authored-by: Paul Bottein <paul.bottein@gmail.com>
This commit is contained in:
parent
c2d71ac789
commit
2925ef3db0
@ -9,8 +9,8 @@ import {
|
|||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
html,
|
html,
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
|
||||||
nothing,
|
nothing,
|
||||||
|
PropertyValues,
|
||||||
} from "lit";
|
} from "lit";
|
||||||
import { customElement, property, query, state } from "lit/decorators";
|
import { customElement, property, query, state } from "lit/decorators";
|
||||||
import memoizeOne from "memoize-one";
|
import memoizeOne from "memoize-one";
|
||||||
@ -19,6 +19,7 @@ import { fireEvent } from "../common/dom/fire_event";
|
|||||||
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 { supportsFeature } from "../common/entity/supports-feature";
|
import { supportsFeature } from "../common/entity/supports-feature";
|
||||||
|
import { nestedArrayMove } from "../common/util/array-move";
|
||||||
import {
|
import {
|
||||||
fetchIntegrationManifest,
|
fetchIntegrationManifest,
|
||||||
IntegrationManifest,
|
IntegrationManifest,
|
||||||
@ -31,6 +32,7 @@ import {
|
|||||||
expandDeviceTarget,
|
expandDeviceTarget,
|
||||||
Selector,
|
Selector,
|
||||||
} from "../data/selector";
|
} from "../data/selector";
|
||||||
|
import { ReorderModeMixin } from "../state/reorder-mode-mixin";
|
||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
|
import { HomeAssistant, ValueChangedEvent } from "../types";
|
||||||
import { documentationUrl } from "../util/documentation-url";
|
import { documentationUrl } from "../util/documentation-url";
|
||||||
import "./ha-checkbox";
|
import "./ha-checkbox";
|
||||||
@ -40,8 +42,6 @@ import "./ha-service-picker";
|
|||||||
import "./ha-settings-row";
|
import "./ha-settings-row";
|
||||||
import "./ha-yaml-editor";
|
import "./ha-yaml-editor";
|
||||||
import type { HaYamlEditor } from "./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) => {
|
const attributeFilter = (values: any[], attribute: any) => {
|
||||||
if (typeof attribute === "object") {
|
if (typeof attribute === "object") {
|
||||||
|
57
src/components/ha-service-icon.ts
Normal file
57
src/components/ha-service-icon.ts
Normal file
@ -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`<ha-icon .icon=${this.icon}></ha-icon>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.service) {
|
||||||
|
return nothing;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.hass) {
|
||||||
|
return this._renderFallback();
|
||||||
|
}
|
||||||
|
|
||||||
|
const icon = serviceIcon(this.hass, this.service).then((icn) => {
|
||||||
|
if (icn) {
|
||||||
|
return html`<ha-icon .icon=${icn}></ha-icon>`;
|
||||||
|
}
|
||||||
|
return this._renderFallback();
|
||||||
|
});
|
||||||
|
|
||||||
|
return html`${until(icon)}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderFallback() {
|
||||||
|
return html`
|
||||||
|
<ha-svg-icon
|
||||||
|
.path=${domainIconWithoutDefault(computeDomain(this.service!)) ||
|
||||||
|
mdiRoomService}
|
||||||
|
></ha-svg-icon>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-service-icon": HaServiceIcon;
|
||||||
|
}
|
||||||
|
}
|
@ -1,4 +1,3 @@
|
|||||||
import "@material/mwc-list/mwc-list-item";
|
|
||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
|
||||||
import { html, LitElement } from "lit";
|
import { html, LitElement } from "lit";
|
||||||
import { customElement, property, state } from "lit/decorators";
|
import { customElement, property, state } from "lit/decorators";
|
||||||
@ -8,16 +7,9 @@ import { LocalizeFunc } from "../common/translations/localize";
|
|||||||
import { domainToName } from "../data/integration";
|
import { domainToName } from "../data/integration";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import "./ha-combo-box";
|
import "./ha-combo-box";
|
||||||
|
import "./ha-list-item";
|
||||||
const rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> = (
|
import "./ha-service-icon";
|
||||||
item
|
import { getServiceIcons } from "../data/icons";
|
||||||
) =>
|
|
||||||
html`<mwc-list-item twoline>
|
|
||||||
<span>${item.name}</span>
|
|
||||||
<span slot="secondary"
|
|
||||||
>${item.name === item.service ? "" : item.service}</span
|
|
||||||
>
|
|
||||||
</mwc-list-item>`;
|
|
||||||
|
|
||||||
@customElement("ha-service-picker")
|
@customElement("ha-service-picker")
|
||||||
class HaServicePicker extends LitElement {
|
class HaServicePicker extends LitElement {
|
||||||
@ -32,9 +24,24 @@ class HaServicePicker extends LitElement {
|
|||||||
protected willUpdate() {
|
protected willUpdate() {
|
||||||
if (!this.hasUpdated) {
|
if (!this.hasUpdated) {
|
||||||
this.hass.loadBackendTranslation("services");
|
this.hass.loadBackendTranslation("services");
|
||||||
|
getServiceIcons(this.hass);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private _rowRenderer: ComboBoxLitRenderer<{ service: string; name: string }> =
|
||||||
|
(item) =>
|
||||||
|
html`<ha-list-item twoline graphic="icon">
|
||||||
|
<ha-service-icon
|
||||||
|
slot="graphic"
|
||||||
|
.hass=${this.hass}
|
||||||
|
.service=${item.service}
|
||||||
|
></ha-service-icon>
|
||||||
|
<span>${item.name}</span>
|
||||||
|
<span slot="secondary"
|
||||||
|
>${item.name === item.service ? "" : item.service}</span
|
||||||
|
>
|
||||||
|
</ha-list-item>`;
|
||||||
|
|
||||||
protected render() {
|
protected render() {
|
||||||
return html`
|
return html`
|
||||||
<ha-combo-box
|
<ha-combo-box
|
||||||
@ -47,7 +54,7 @@ class HaServicePicker extends LitElement {
|
|||||||
)}
|
)}
|
||||||
.value=${this.value}
|
.value=${this.value}
|
||||||
.disabled=${this.disabled}
|
.disabled=${this.disabled}
|
||||||
.renderer=${rowRenderer}
|
.renderer=${this._rowRenderer}
|
||||||
item-value-path="service"
|
item-value-path="service"
|
||||||
item-label-path="name"
|
item-label-path="name"
|
||||||
allow-custom-value
|
allow-custom-value
|
||||||
|
@ -1,15 +1,17 @@
|
|||||||
import { HassEntity } from "home-assistant-js-websocket";
|
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 { computeStateDomain } from "../common/entity/compute_state_domain";
|
||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
import {
|
import {
|
||||||
EntityRegistryDisplayEntry,
|
EntityRegistryDisplayEntry,
|
||||||
EntityRegistryEntry,
|
EntityRegistryEntry,
|
||||||
} from "./entity_registry";
|
} from "./entity_registry";
|
||||||
import { computeDomain } from "../common/entity/compute_domain";
|
|
||||||
|
|
||||||
const resources: Record<IconCategory, any> = {
|
const resources: Record<IconCategory, any> = {
|
||||||
entity: {},
|
entity: {},
|
||||||
entity_component: undefined,
|
entity_component: undefined,
|
||||||
|
services: {},
|
||||||
};
|
};
|
||||||
|
|
||||||
interface IconResources {
|
interface IconResources {
|
||||||
@ -46,7 +48,11 @@ interface ComponentIcons {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type IconCategory = "entity" | "entity_component";
|
interface ServiceIcons {
|
||||||
|
[domain: string]: Record<string, string>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type IconCategory = "entity" | "entity_component" | "services";
|
||||||
|
|
||||||
export const getHassIcons = async (
|
export const getHassIcons = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
@ -64,7 +70,7 @@ export const getPlatformIcons = async (
|
|||||||
integration: string,
|
integration: string,
|
||||||
force = false
|
force = false
|
||||||
): Promise<PlatformIcons> => {
|
): Promise<PlatformIcons> => {
|
||||||
if (!force && integration && integration in resources.entity) {
|
if (!force && integration in resources.entity) {
|
||||||
return resources.entity[integration];
|
return resources.entity[integration];
|
||||||
}
|
}
|
||||||
const result = getHassIcons(hass, "entity", integration);
|
const result = getHassIcons(hass, "entity", integration);
|
||||||
@ -88,6 +94,37 @@ export const getComponentIcons = async (
|
|||||||
return resources.entity_component.then((res) => res[domain]);
|
return resources.entity_component.then((res) => res[domain]);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getServiceIcons = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
domain?: string,
|
||||||
|
force = false
|
||||||
|
): Promise<ServiceIcons> => {
|
||||||
|
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 (
|
export const entityIcon = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
state: HassEntity,
|
state: HassEntity,
|
||||||
@ -96,7 +133,6 @@ export const entityIcon = async (
|
|||||||
const entity = hass.entities?.[state.entity_id] as
|
const entity = hass.entities?.[state.entity_id] as
|
||||||
| EntityRegistryDisplayEntry
|
| EntityRegistryDisplayEntry
|
||||||
| undefined;
|
| undefined;
|
||||||
|
|
||||||
if (entity?.icon) {
|
if (entity?.icon) {
|
||||||
return entity.icon;
|
return entity.icon;
|
||||||
}
|
}
|
||||||
@ -197,3 +233,13 @@ export const attributeIcon = async (
|
|||||||
}
|
}
|
||||||
return icon;
|
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;
|
||||||
|
};
|
||||||
|
@ -29,13 +29,12 @@ import { classMap } from "lit/directives/class-map";
|
|||||||
import { storage } from "../../../../common/decorators/storage";
|
import { storage } from "../../../../common/decorators/storage";
|
||||||
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
import { dynamicElement } from "../../../../common/dom/dynamic-element-directive";
|
||||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
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 { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter";
|
||||||
import { handleStructError } from "../../../../common/structs/handle-errors";
|
import { handleStructError } from "../../../../common/structs/handle-errors";
|
||||||
import "../../../../components/ha-alert";
|
import "../../../../components/ha-alert";
|
||||||
import "../../../../components/ha-button-menu";
|
import "../../../../components/ha-button-menu";
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/ha-service-icon";
|
||||||
import "../../../../components/ha-expansion-panel";
|
import "../../../../components/ha-expansion-panel";
|
||||||
import "../../../../components/ha-icon-button";
|
import "../../../../components/ha-icon-button";
|
||||||
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
|
||||||
@ -203,16 +202,18 @@ export default class HaAutomationActionRow extends LitElement {
|
|||||||
: ""}
|
: ""}
|
||||||
<ha-expansion-panel leftChevron>
|
<ha-expansion-panel leftChevron>
|
||||||
<h3 slot="header">
|
<h3 slot="header">
|
||||||
<ha-svg-icon
|
${type === "service" &&
|
||||||
class="action-icon"
|
"service" in this.action &&
|
||||||
.path=${type === "service" &&
|
this.action.service
|
||||||
"service" in this.action &&
|
? html`<ha-service-icon
|
||||||
this.action.service
|
class="action-icon"
|
||||||
? domainIconWithoutDefault(
|
.hass=${this.hass}
|
||||||
computeDomain(this.action.service as string)
|
.service=${this.action.service}
|
||||||
) || ACTION_ICONS[type!]
|
></ha-service-icon>`
|
||||||
: ACTION_ICONS[type!]}
|
: html`<ha-svg-icon
|
||||||
></ha-svg-icon>
|
class="action-icon"
|
||||||
|
.path=${ACTION_ICONS[type!]}
|
||||||
|
></ha-svg-icon>`}
|
||||||
${capitalizeFirstLetter(
|
${capitalizeFirstLetter(
|
||||||
describeAction(this.hass, this._entityReg, this.action)
|
describeAction(this.hass, this._entityReg, this.action)
|
||||||
)}
|
)}
|
||||||
|
@ -5,6 +5,7 @@ import {
|
|||||||
CSSResultGroup,
|
CSSResultGroup,
|
||||||
LitElement,
|
LitElement,
|
||||||
PropertyValues,
|
PropertyValues,
|
||||||
|
TemplateResult,
|
||||||
css,
|
css,
|
||||||
html,
|
html,
|
||||||
nothing,
|
nothing,
|
||||||
@ -53,6 +54,7 @@ import { computeDomain } from "../../../common/entity/compute_domain";
|
|||||||
import { deepEqual } from "../../../common/util/deep-equal";
|
import { deepEqual } from "../../../common/util/deep-equal";
|
||||||
import "../../../components/search-input";
|
import "../../../components/search-input";
|
||||||
import "@material/web/divider/divider";
|
import "@material/web/divider/divider";
|
||||||
|
import { getServiceIcons } from "../../../data/icons";
|
||||||
|
|
||||||
const TYPES = {
|
const TYPES = {
|
||||||
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
|
trigger: { groups: TRIGGER_GROUPS, icons: TRIGGER_ICONS },
|
||||||
@ -70,7 +72,8 @@ interface ListItem {
|
|||||||
key: string;
|
key: string;
|
||||||
name: string;
|
name: string;
|
||||||
description: string;
|
description: string;
|
||||||
icon?: string;
|
iconPath?: string;
|
||||||
|
icon?: TemplateResult;
|
||||||
image?: string;
|
image?: string;
|
||||||
group: boolean;
|
group: boolean;
|
||||||
}
|
}
|
||||||
@ -124,6 +127,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
this.hass.loadBackendTranslation("services");
|
this.hass.loadBackendTranslation("services");
|
||||||
this._fetchManifests();
|
this._fetchManifests();
|
||||||
this._calculateUsedDomains();
|
this._calculateUsedDomains();
|
||||||
|
getServiceIcons(this.hass);
|
||||||
}
|
}
|
||||||
this._fullScreen = matchMedia(
|
this._fullScreen = matchMedia(
|
||||||
"all and (max-width: 450px), all and (max-height: 500px)"
|
"all and (max-width: 450px), all and (max-height: 500px)"
|
||||||
@ -174,7 +178,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
options.members ? "groups" : "type"
|
options.members ? "groups" : "type"
|
||||||
}.${key}.description${options.members ? "" : ".picker"}`
|
}.${key}.description${options.members ? "" : ".picker"}`
|
||||||
),
|
),
|
||||||
icon: options.icon || TYPES[type].icons[key],
|
iconPath: options.icon || TYPES[type].icons[key],
|
||||||
});
|
});
|
||||||
|
|
||||||
private _getFilteredItems = memoizeOne(
|
private _getFilteredItems = memoizeOne(
|
||||||
@ -317,7 +321,7 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
const icon = domainIconWithoutDefault(domain);
|
const icon = domainIconWithoutDefault(domain);
|
||||||
result.push({
|
result.push({
|
||||||
group: true,
|
group: true,
|
||||||
icon,
|
iconPath: icon,
|
||||||
image: !icon
|
image: !icon
|
||||||
? brandsUrl({
|
? brandsUrl({
|
||||||
domain,
|
domain,
|
||||||
@ -358,17 +362,12 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
const services_keys = Object.keys(services[dmn]);
|
const services_keys = Object.keys(services[dmn]);
|
||||||
|
|
||||||
for (const service of services_keys) {
|
for (const service of services_keys) {
|
||||||
const icon = domainIconWithoutDefault(dmn);
|
|
||||||
result.push({
|
result.push({
|
||||||
group: false,
|
group: false,
|
||||||
icon,
|
icon: html`<ha-service-icon
|
||||||
image: !icon
|
.hass=${this.hass}
|
||||||
? brandsUrl({
|
.service=${`${dmn}.${service}`}
|
||||||
domain: dmn,
|
></ha-service-icon>`,
|
||||||
type: "icon",
|
|
||||||
darkOptimized: this.hass.themes?.darkMode,
|
|
||||||
})
|
|
||||||
: undefined,
|
|
||||||
key: `${SERVICE_PREFIX}${dmn}.${service}`,
|
key: `${SERVICE_PREFIX}${dmn}.${service}`,
|
||||||
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
|
name: `${domain ? "" : `${domainToName(localize, dmn)}: `}${
|
||||||
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
|
this.hass.localize(`component.${dmn}.services.${service}.name`) ||
|
||||||
@ -573,17 +572,19 @@ class DialogAddAutomationElement extends LitElement implements HassDialog {
|
|||||||
<div slot="headline">${item.name}</div>
|
<div slot="headline">${item.name}</div>
|
||||||
<div slot="supporting-text">${item.description}</div>
|
<div slot="supporting-text">${item.description}</div>
|
||||||
${item.icon
|
${item.icon
|
||||||
? html`<ha-svg-icon
|
? html`<span slot="start">${item.icon}</span>`
|
||||||
slot="start"
|
: item.iconPath
|
||||||
.path=${item.icon}
|
? html`<ha-svg-icon
|
||||||
></ha-svg-icon>`
|
slot="start"
|
||||||
: html`<img
|
.path=${item.iconPath}
|
||||||
alt=""
|
></ha-svg-icon>`
|
||||||
slot="start"
|
: html`<img
|
||||||
src=${item.image!}
|
alt=""
|
||||||
crossorigin="anonymous"
|
slot="start"
|
||||||
referrerpolicy="no-referrer"
|
src=${item.image!}
|
||||||
/>`}
|
crossorigin="anonymous"
|
||||||
|
referrerpolicy="no-referrer"
|
||||||
|
/>`}
|
||||||
${item.group
|
${item.group
|
||||||
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
? html`<ha-icon-next slot="end"></ha-icon-next>`
|
||||||
: html`<ha-svg-icon
|
: html`<ha-svg-icon
|
||||||
|
Loading…
x
Reference in New Issue
Block a user