1
0
mirror of https://github.com/home-assistant/frontend.git synced 2025-12-20 02:38:53 +00:00

Automation editor show targets within rows (#28510)

* Automation editor show targets within rows

* review

* Fix expandable row icons

* Use state icon instead of state-badge

* Fix target wrap

* Use default font weight for automation rows

* Remove comma from targets in row
This commit is contained in:
Wendelin
2025-12-16 08:42:38 +01:00
committed by GitHub
parent ca06269a91
commit c93942919b
16 changed files with 510 additions and 331 deletions

View File

@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
<div class="action"> <div class="action">
<span> <span>
${this._action ${this._action
? describeAction(this.hass, [], [], {}, this._action) ? describeAction(this.hass, [], this._action)
: "<invalid YAML>"} : "<invalid YAML>"}
</span> </span>
<ha-yaml-editor <ha-yaml-editor
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${ACTIONS.map( ${ACTIONS.map(
(conf) => html` (conf) => html`
<div class="action"> <div class="action">
<span>${describeAction(this.hass, [], [], {}, conf as any)}</span> <span>${describeAction(this.hass, [], conf as any)}</span>
<pre>${dump(conf)}</pre> <pre>${dump(conf)}</pre>
</div> </div>
` `

View File

@@ -52,8 +52,10 @@ export class HaAutomationRow extends LitElement {
<slot name="leading-icon"></slot> <slot name="leading-icon"></slot>
</div> </div>
<slot class="header" name="header"></slot> <slot class="header" name="header"></slot>
<div class="icons">
<slot name="icons"></slot> <slot name="icons"></slot>
</div> </div>
</div>
`; `;
} }
@@ -118,12 +120,11 @@ export class HaAutomationRow extends LitElement {
} }
.row { .row {
display: flex; display: flex;
padding: var(--ha-space-0) var(--ha-space-2); padding: 0 var(--ha-space-3);
min-height: 48px; min-height: 48px;
align-items: center; align-items: flex-start;
cursor: pointer; cursor: pointer;
overflow: hidden; overflow: hidden;
font-weight: var(--ha-font-weight-medium);
outline: none; outline: none;
border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
} }
@@ -140,11 +141,15 @@ export class HaAutomationRow extends LitElement {
background-color: var(--ha-color-fill-neutral-loud-resting); background-color: var(--ha-color-fill-neutral-loud-resting);
border-radius: var(--ha-border-radius-md); border-radius: var(--ha-border-radius-md);
padding: var(--ha-space-1); padding: var(--ha-space-1);
margin-top: 10px;
display: flex; display: flex;
justify-content: center; justify-content: center;
align-items: center; align-items: center;
transform: rotate(45deg); transform: rotate(45deg);
} }
.leading-icon-wrapper {
padding-top: var(--ha-space-3);
}
::slotted([slot="leading-icon"]) { ::slotted([slot="leading-icon"]) {
color: var(--ha-color-on-neutral-quiet); color: var(--ha-color-on-neutral-quiet);
} }
@@ -172,6 +177,10 @@ export class HaAutomationRow extends LitElement {
overflow-wrap: anywhere; overflow-wrap: anywhere;
margin: var(--ha-space-0) var(--ha-space-3); margin: var(--ha-space-0) var(--ha-space-3);
} }
.icons {
display: flex;
align-items: center;
}
:host([sort-selected]) .row { :host([sort-selected]) .row {
outline: solid; outline: solid;
outline-color: rgba(var(--rgb-accent-color), 0.6); outline-color: rgba(var(--rgb-accent-color), 0.6);

View File

@@ -47,6 +47,7 @@ export class HaDomainIcon 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 this._renderFallback();
}); });

View File

@@ -6,13 +6,8 @@ import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map"; import { classMap } from "lit/directives/class-map";
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
import { describeCondition, describeTrigger } from "../../data/automation_i18n"; import { describeCondition, describeTrigger } from "../../data/automation_i18n";
import { import { fullEntitiesContext, labelsContext } from "../../data/context";
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry"; import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { LabelRegistryEntry } from "../../data/label/label_registry"; import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { LogbookEntry } from "../../data/logbook"; import type { LogbookEntry } from "../../data/logbook";
import { describeAction } from "../../data/script_i18n"; import { describeAction } from "../../data/script_i18n";
@@ -63,10 +58,6 @@ export class HaTracePathDetails extends LitElement {
@consume({ context: labelsContext, subscribe: true }) @consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[]; _labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="padded-box trace-info"> <div class="padded-box trace-info">
@@ -193,8 +184,6 @@ export class HaTracePathDetails extends LitElement {
${describeAction( ${describeAction(
this.hass, this.hass,
this._entityReg, this._entityReg,
this._labelReg,
this._floorReg,
currentDetail currentDetail
)} )}
</h2>` </h2>`

View File

@@ -15,14 +15,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
import { relativeTime } from "../../common/datetime/relative_time"; import { relativeTime } from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event"; import { fireEvent } from "../../common/dom/fire_event";
import { toggleAttribute } from "../../common/dom/toggle_attribute"; import { toggleAttribute } from "../../common/dom/toggle_attribute";
import { import { fullEntitiesContext } from "../../data/context";
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../data/context";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry"; import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { FloorRegistryEntry } from "../../data/floor_registry";
import type { LabelRegistryEntry } from "../../data/label/label_registry";
import type { LogbookEntry } from "../../data/logbook"; import type { LogbookEntry } from "../../data/logbook";
import type { import type {
ChooseAction, ChooseAction,
@@ -197,8 +191,6 @@ class ActionRenderer {
constructor( constructor(
private hass: HomeAssistant, private hass: HomeAssistant,
private entityReg: EntityRegistryEntry[], private entityReg: EntityRegistryEntry[],
private labelReg: LabelRegistryEntry[],
private floorReg: Record<string, FloorRegistryEntry>,
private entries: TemplateResult[], private entries: TemplateResult[],
private trace: AutomationTraceExtended, private trace: AutomationTraceExtended,
private logbookRenderer: LogbookRenderer, private logbookRenderer: LogbookRenderer,
@@ -313,14 +305,7 @@ class ActionRenderer {
this._renderEntry( this._renderEntry(
path, path,
describeAction( describeAction(this.hass, this.entityReg, data, actionType),
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
data,
actionType
),
undefined, undefined,
data.enabled === false data.enabled === false
); );
@@ -485,13 +470,7 @@ class ActionRenderer {
const name = const name =
repeatConfig.alias || repeatConfig.alias ||
describeAction( describeAction(this.hass, this.entityReg, repeatConfig);
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
repeatConfig
);
this._renderEntry(repeatPath, name, undefined, disabled); this._renderEntry(repeatPath, name, undefined, disabled);
@@ -585,14 +564,7 @@ class ActionRenderer {
this._renderEntry( this._renderEntry(
sequencePath, sequencePath,
sequenceConfig.alias || sequenceConfig.alias ||
describeAction( describeAction(this.hass, this.entityReg, sequenceConfig, "sequence"),
this.hass,
this.entityReg,
this.labelReg,
this.floorReg,
sequenceConfig,
"sequence"
),
undefined, undefined,
sequenceConfig.enabled === false sequenceConfig.enabled === false
); );
@@ -683,14 +655,6 @@ export class HaAutomationTracer extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>;
protected render() { protected render() {
if (!this.trace) { if (!this.trace) {
return nothing; return nothing;
@@ -707,8 +671,6 @@ export class HaAutomationTracer extends LitElement {
const actionRenderer = new ActionRenderer( const actionRenderer = new ActionRenderer(
this.hass, this.hass,
this._entityReg, this._entityReg,
this._labelReg,
this._floorReg,
entries, entries,
this.trace, this.trace,
logbookRenderer, logbookRenderer,

View File

@@ -1,7 +1,6 @@
import { ensureArray } from "../common/array/ensure-array"; import { ensureArray } from "../common/array/ensure-array";
import { formatNumericDuration } from "../common/datetime/format_duration"; import { formatNumericDuration } from "../common/datetime/format_duration";
import secondsToDuration from "../common/datetime/seconds_to_duration"; import secondsToDuration from "../common/datetime/seconds_to_duration";
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { formatListWithAnds } from "../common/string/format-list"; import { formatListWithAnds } from "../common/string/format-list";
import { isTemplate } from "../common/string/has-template"; import { isTemplate } from "../common/string/has-template";
@@ -10,13 +9,7 @@ import type { Condition } from "./automation";
import { describeCondition } from "./automation_i18n"; import { describeCondition } from "./automation_i18n";
import { localizeDeviceAutomationAction } from "./device/device_automation"; import { localizeDeviceAutomationAction } from "./device/device_automation";
import type { EntityRegistryEntry } from "./entity/entity_registry"; import type { EntityRegistryEntry } from "./entity/entity_registry";
import {
computeEntityRegistryName,
entityRegistryById,
} from "./entity/entity_registry";
import type { FloorRegistryEntry } from "./floor_registry";
import { domainToName } from "./integration"; import { domainToName } from "./integration";
import type { LabelRegistryEntry } from "./label/label_registry";
import type { import type {
ActionType, ActionType,
ActionTypes, ActionTypes,
@@ -41,8 +34,6 @@ const actionTranslationBaseKey =
export const describeAction = <T extends ActionType>( export const describeAction = <T extends ActionType>(
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: Record<string, FloorRegistryEntry>,
action: ActionTypes[T], action: ActionTypes[T],
actionType?: T, actionType?: T,
ignoreAlias = false ignoreAlias = false
@@ -51,8 +42,6 @@ export const describeAction = <T extends ActionType>(
const description = tryDescribeAction( const description = tryDescribeAction(
hass, hass,
entityRegistry, entityRegistry,
labelRegistry,
floorRegistry,
action, action,
actionType, actionType,
ignoreAlias ignoreAlias
@@ -75,8 +64,6 @@ export const describeAction = <T extends ActionType>(
const tryDescribeAction = <T extends ActionType>( const tryDescribeAction = <T extends ActionType>(
hass: HomeAssistant, hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[], entityRegistry: EntityRegistryEntry[],
labelRegistry: LabelRegistryEntry[],
floorRegistry: Record<string, FloorRegistryEntry>,
action: ActionTypes[T], action: ActionTypes[T],
actionType?: T, actionType?: T,
ignoreAlias = false ignoreAlias = false
@@ -100,107 +87,6 @@ const tryDescribeAction = <T extends ActionType>(
{ name: "target" } { name: "target" }
) )
); );
} else if (targetOrData) {
for (const [key, name] of Object.entries({
area_id: "areas",
device_id: "devices",
entity_id: "entities",
floor_id: "floors",
label_id: "labels",
})) {
if (!(key in targetOrData)) {
continue;
}
const keyConf: string[] = ensureArray(targetOrData[key]) || [];
for (const targetThing of keyConf) {
if (isTemplate(targetThing)) {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_template`,
{ name }
)
);
break;
} else if (key === "entity_id") {
if (targetThing.includes(".")) {
const state = hass.states[targetThing];
if (state) {
targets.push(computeStateName(state));
} else {
targets.push(targetThing);
}
} else {
const entityReg = entityRegistryById(entityRegistry)[targetThing];
if (entityReg) {
targets.push(
computeEntityRegistryName(hass, entityReg) || targetThing
);
} else if (targetThing === "all") {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_every_entity`
)
);
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_entity`
)
);
}
}
} else if (key === "device_id") {
const device = hass.devices[targetThing];
if (device) {
targets.push(computeDeviceNameDisplay(device, hass));
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_device`
)
);
}
} else if (key === "area_id") {
const area = hass.areas[targetThing];
if (area?.name) {
targets.push(area.name);
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_area`
)
);
}
} else if (key === "floor_id") {
const floor = floorRegistry[targetThing] ?? undefined;
if (floor?.name) {
targets.push(floor.name);
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_floor`
)
);
}
} else if (key === "label_id") {
const label = labelRegistry.find(
(lbl) => lbl.label_id === targetThing
);
if (label?.name) {
targets.push(label.name);
} else {
targets.push(
hass.localize(
`${actionTranslationBaseKey}.service.description.target_unknown_label`
)
);
}
} else {
targets.push(targetThing);
}
}
}
} }
if ( if (
@@ -229,26 +115,20 @@ const tryDescribeAction = <T extends ActionType>(
if (config.metadata) { if (config.metadata) {
return hass.localize( return hass.localize(
targets.length `${actionTranslationBaseKey}.service.description.service_name_no_targets`,
? `${actionTranslationBaseKey}.service.description.service_name`
: `${actionTranslationBaseKey}.service.description.service_name_no_targets`,
{ {
domain: domainToName(hass.localize, domain), domain: domainToName(hass.localize, domain),
name: service || config.action, name: service || config.action,
targets: formatListWithAnds(hass.locale, targets),
} }
); );
} }
return hass.localize( return hass.localize(
targets.length `${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`,
? `${actionTranslationBaseKey}.service.description.service_based_on_name`
: `${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`,
{ {
name: service name: service
? `${domainToName(hass.localize, domain)}: ${service}` ? `${domainToName(hass.localize, domain)}: ${service}`
: config.action, : config.action,
targets: formatListWithAnds(hass.locale, targets),
} }
); );
} }

View File

@@ -17,6 +17,7 @@ import {
mdiStopCircleOutline, mdiStopCircleOutline,
} from "@mdi/js"; } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import type { PropertyValues, TemplateResult } from "lit"; import type { PropertyValues, TemplateResult } from "lit";
import { LitElement, html, nothing } from "lit"; import { LitElement, html, nothing } from "lit";
@@ -53,18 +54,13 @@ import type {
} from "../../../../data/automation"; } from "../../../../data/automation";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition"; import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import { validateConfig } from "../../../../data/config"; import { validateConfig } from "../../../../data/config";
import { import { fullEntitiesContext } from "../../../../data/context";
floorsContext,
fullEntitiesContext,
labelsContext,
} from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry"; import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { FloorRegistryEntry } from "../../../../data/floor_registry";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import type { import type {
Action, Action,
NonConditionAction, NonConditionAction,
RepeatAction, RepeatAction,
ServiceAction,
} from "../../../../data/script"; } from "../../../../data/script";
import { getActionType, isAction } from "../../../../data/script"; import { getActionType, isAction } from "../../../../data/script";
import { describeAction } from "../../../../data/script_i18n"; import { describeAction } from "../../../../data/script_i18n";
@@ -78,6 +74,7 @@ import { isMac } from "../../../../util/is_mac";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning"; import "../ha-automation-editor-warning";
import { overflowStyles, rowStyles } from "../styles"; import { overflowStyles, rowStyles } from "../styles";
import "../target/ha-automation-row-targets";
import "./ha-automation-action-editor"; import "./ha-automation-action-editor";
import type HaAutomationActionEditor from "./ha-automation-action-editor"; import type HaAutomationActionEditor from "./ha-automation-action-editor";
import "./types/ha-automation-action-choose"; import "./types/ha-automation-action-choose";
@@ -176,14 +173,6 @@ export default class HaAutomationActionRow extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true }) @consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[]; _entityReg!: EntityRegistryEntry[];
@state()
@consume({ context: labelsContext, subscribe: true })
_labelReg!: LabelRegistryEntry[];
@state()
@consume({ context: floorsContext, subscribe: true })
_floorReg!: Record<string, FloorRegistryEntry>;
@state() private _uiModeAvailable = true; @state() private _uiModeAvailable = true;
@state() private _yamlMode = false; @state() private _yamlMode = false;
@@ -263,14 +252,11 @@ export default class HaAutomationActionRow extends LitElement {
`} `}
<h3 slot="header"> <h3 slot="header">
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeAction( describeAction(this.hass, this._entityReg, this.action)
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action
)
)} )}
${type === "service" && "target" in this.action
? this._renderTargets((this.action as ServiceAction).target)
: nothing}
</h3> </h3>
<slot name="icons" slot="icons"></slot> <slot name="icons" slot="icons"></slot>
@@ -556,6 +542,14 @@ export default class HaAutomationActionRow extends LitElement {
`; `;
} }
private _renderTargets = memoizeOne(
(target?: HassServiceTarget) =>
html`<ha-automation-row-targets
.hass=${this.hass}
.target=${target}
></ha-automation-row-targets>`
);
private _onValueChange(event: CustomEvent) { private _onValueChange(event: CustomEvent) {
// reload sidebar if sort, deleted,... happend // reload sidebar if sort, deleted,... happend
if (this._selected && this.optionsInSidebar) { if (this._selected && this.optionsInSidebar) {
@@ -668,15 +662,7 @@ export default class HaAutomationActionRow extends LitElement {
), ),
inputType: "string", inputType: "string",
placeholder: capitalizeFirstLetter( placeholder: capitalizeFirstLetter(
describeAction( describeAction(this.hass, this._entityReg, this.action, undefined, true)
this.hass,
this._entityReg,
this._labelReg,
this._floorReg,
this.action,
undefined,
true
)
), ),
defaultValue: this.action.alias, defaultValue: this.action.alias,
confirmText: this.hass.localize("ui.common.submit"), confirmText: this.hass.localize("ui.common.submit"),

View File

@@ -18,7 +18,6 @@ import memoizeOne from "memoize-one";
import { fireEvent } from "../../../common/dom/fire_event"; import { fireEvent } from "../../../common/dom/fire_event";
import { mainWindow } from "../../../common/dom/get_main_window"; import { mainWindow } from "../../../common/dom/get_main_window";
import { computeAreaName } from "../../../common/entity/compute_area_name"; import { computeAreaName } from "../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../common/entity/compute_device_name";
import { computeDomain } from "../../../common/entity/compute_domain"; import { computeDomain } from "../../../common/entity/compute_domain";
import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display"; import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display";
import { computeFloorName } from "../../../common/entity/compute_floor_name"; import { computeFloorName } from "../../../common/entity/compute_floor_name";
@@ -123,6 +122,7 @@ import "./add-automation-element/ha-automation-add-items";
import "./add-automation-element/ha-automation-add-search"; import "./add-automation-element/ha-automation-add-search";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog"; import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import { PASTE_VALUE } from "./show-add-automation-element-dialog"; import { PASTE_VALUE } from "./show-add-automation-element-dialog";
import { getTargetText } from "./target/get_target_text";
const TYPES = { const TYPES = {
trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS }, trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS },
@@ -1393,8 +1393,8 @@ class DialogAddAutomationElement
} }
); );
private _getLabel = memoizeOne((labelId) => private _getLabel = memoizeOne((id: string) =>
this._labelRegistry?.find(({ label_id }) => label_id === labelId) this._labelRegistry?.find(({ label_id }) => label_id === id)
); );
private _getDomainType(domain: string) { private _getDomainType(domain: string) {
@@ -1926,32 +1926,12 @@ class DialogAddAutomationElement
} }
if (targetId) { if (targetId) {
if (targetType === "floor") { return getTargetText(
return computeFloorName(this.hass.floors[targetId]) || targetId; this.hass,
} targetType as "floor" | "area" | "device" | "entity" | "label",
if (targetType === "area") { targetId,
return computeAreaName(this.hass.areas[targetId]) || targetId; this._getLabel
}
if (targetType === "device") {
return computeDeviceName(this.hass.devices[targetId]) || targetId;
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
const [entityName, deviceName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
); );
return entityName || deviceName || targetId;
}
if (targetType === "label") {
const label = this._getLabel(targetId);
return label?.name || targetId;
}
} }
return undefined; return undefined;

View File

@@ -1,10 +1,5 @@
import { import { mdiInformationOutline, mdiPlus } from "@mdi/js";
mdiInformationOutline, import { LitElement, css, html, nothing } from "lit";
mdiLabel,
mdiPlus,
mdiTextureBox,
} from "@mdi/js";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import { import {
customElement, customElement,
eventOptions, eventOptions,
@@ -17,17 +12,15 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event"; import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation"; import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list"; import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item"; import "../../../../components/ha-md-list-item";
import "../../../../components/ha-svg-icon"; import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip"; import "../../../../components/ha-tooltip";
import type { ConfigEntry } from "../../../../data/config_entries"; import type { ConfigEntry } from "../../../../data/config_entries";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import type { HomeAssistant } from "../../../../types"; import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog"; import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
import { getTargetIcon } from "../target/get_target_icon";
type Target = [string, string | undefined, string | undefined]; type Target = [string, string | undefined, string | undefined];
@@ -50,7 +43,7 @@ export class HaAutomationAddItems extends LitElement {
@property({ attribute: false }) public getLabel!: ( @property({ attribute: false }) public getLabel!: (
id: string id: string
) => { name: string; icon?: string } | undefined; ) => LabelRegistryEntry | undefined;
@property({ attribute: false }) public configEntryLookup: Record< @property({ attribute: false }) public configEntryLookup: Record<
string, string,
@@ -164,72 +157,17 @@ export class HaAutomationAddItems extends LitElement {
} }
return html`<div class="selected-target"> return html`<div class="selected-target">
${this._getSelectedTargetIcon(target[0], target[1])} ${getTargetIcon(
this.hass,
target[0],
target[1],
this.configEntryLookup,
this.getLabel
)}
<div class="label">${target[2]}</div> <div class="label">${target[2]}</div>
</div>`; </div>`;
}); });
private _getSelectedTargetIcon(
targetType: string,
targetId: string | undefined
): TemplateResult | typeof nothing {
if (!targetId) {
return nothing;
}
if (targetType === "floor") {
return html`<ha-floor-icon
.floor=${this.hass.floors[targetId]}
></ha-floor-icon>`;
}
if (targetType === "area" && this.hass.areas[targetId]) {
const area = this.hass.areas[targetId];
if (area.icon) {
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
}
if (targetType === "device" && this.hass.devices[targetId]) {
const device = this.hass.devices[targetId];
const configEntry = device.primary_config_entry
? this.configEntryLookup[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
if (domain) {
return html`<ha-domain-icon
slot="start"
.hass=${this.hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>`;
}
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
if (stateObj) {
return html`<state-badge
.stateObj=${stateObj}
.hass=${this.hass}
.stateColor=${false}
></state-badge>`;
}
}
if (targetType === "label") {
const label = this.getLabel(targetId);
if (label?.icon) {
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
}
return nothing;
}
private _selected(ev) { private _selected(ev) {
const item = ev.currentTarget; const item = ev.currentTarget;
fireEvent(this, "value-changed", { fireEvent(this, "value-changed", {
@@ -335,10 +273,6 @@ export class HaAutomationAddItems extends LitElement {
border-bottom: 1px solid var(--ha-color-border-neutral-quiet); border-bottom: 1px solid var(--ha-color-border-neutral-quiet);
} }
ha-icon-next {
width: var(--ha-space-6);
}
ha-svg-icon.plus { ha-svg-icon.plus {
color: var(--primary-color); color: var(--primary-color);
} }
@@ -362,16 +296,11 @@ export class HaAutomationAddItems extends LitElement {
.selected-target ha-icon, .selected-target ha-icon,
.selected-target ha-svg-icon, .selected-target ha-svg-icon,
.selected-target state-badge,
.selected-target ha-domain-icon { .selected-target ha-domain-icon {
display: flex; display: flex;
padding: var(--ha-space-1) 0; padding: var(--ha-space-1) 0;
} }
.selected-target state-badge {
--mdc-icon-size: 24px;
}
.selected-target state-badge,
.selected-target ha-floor-icon { .selected-target ha-floor-icon {
display: flex; display: flex;
height: 32px; height: 32px;

View File

@@ -16,6 +16,7 @@ import {
mdiStopCircleOutline, mdiStopCircleOutline,
} from "@mdi/js"; } from "@mdi/js";
import deepClone from "deep-clone-simple"; import deepClone from "deep-clone-simple";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
@@ -43,6 +44,7 @@ import type {
AutomationClipboard, AutomationClipboard,
Condition, Condition,
ConditionSidebarConfig, ConditionSidebarConfig,
PlatformCondition,
} from "../../../../data/automation"; } from "../../../../data/automation";
import { isCondition, testCondition } from "../../../../data/automation"; import { isCondition, testCondition } from "../../../../data/automation";
import { describeCondition } from "../../../../data/automation_i18n"; import { describeCondition } from "../../../../data/automation_i18n";
@@ -60,6 +62,7 @@ import { isMac } from "../../../../util/is_mac";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning"; import "../ha-automation-editor-warning";
import { overflowStyles, rowStyles } from "../styles"; import { overflowStyles, rowStyles } from "../styles";
import "../target/ha-automation-row-targets";
import "./ha-automation-condition-editor"; import "./ha-automation-condition-editor";
import type HaAutomationConditionEditor from "./ha-automation-condition-editor"; import type HaAutomationConditionEditor from "./ha-automation-condition-editor";
import "./types/ha-automation-condition-and"; import "./types/ha-automation-condition-and";
@@ -191,6 +194,10 @@ export default class HaAutomationConditionRow extends LitElement {
${capitalizeFirstLetter( ${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg) describeCondition(this.condition, this.hass, this._entityReg)
)} )}
${"target" in
(this.conditionDescriptions[this.condition.condition] || {})
? this._renderTargets((this.condition as PlatformCondition).target)
: nothing}
</h3> </h3>
<slot name="icons" slot="icons"></slot> <slot name="icons" slot="icons"></slot>
@@ -475,6 +482,14 @@ export default class HaAutomationConditionRow extends LitElement {
`; `;
} }
private _renderTargets = memoizeOne(
(target?: HassServiceTarget) =>
html`<ha-automation-row-targets
.hass=${this.hass}
.target=${target}
></ha-automation-row-targets>`
);
protected firstUpdated(changedProperties: PropertyValues): void { protected firstUpdated(changedProperties: PropertyValues): void {
super.firstUpdated(changedProperties); super.firstUpdated(changedProperties);

View File

@@ -14,6 +14,12 @@ export const rowStyles = css`
h3 { h3 {
font-size: inherit; font-size: inherit;
font-weight: inherit; font-weight: inherit;
display: flex;
align-items: center;
flex-wrap: wrap;
gap: var(--ha-space-2);
padding: var(--ha-space-2) 0;
min-height: 32px;
} }
ha-card { ha-card {

View File

@@ -0,0 +1,69 @@
import { mdiLabel, mdiTextureBox } from "@mdi/js";
import { html, nothing, type TemplateResult } from "lit";
import "../../../../components/ha-domain-icon";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon";
import "../../../../components/ha-state-icon";
import "../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../data/config_entries";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import type { HomeAssistant } from "../../../../types";
export const getTargetIcon = (
hass: HomeAssistant,
targetType: string,
targetId: string | undefined,
configEntryLookup: Record<string, ConfigEntry>,
getLabel?: (id: string) => LabelRegistryEntry | undefined
): TemplateResult | typeof nothing => {
if (!targetId) {
return nothing;
}
if (targetType === "floor" && hass.floors[targetId]) {
return html`<ha-floor-icon
.floor=${hass.floors[targetId]}
></ha-floor-icon>`;
}
if (targetType === "area") {
const area = hass.areas[targetId];
if (area?.icon) {
return html`<ha-icon .icon=${area.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiTextureBox}></ha-svg-icon>`;
}
if (targetType === "device" && hass.devices[targetId]) {
const device = hass.devices[targetId];
const configEntry = device.primary_config_entry
? configEntryLookup[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
if (domain) {
return html`<ha-domain-icon
.hass=${hass}
.domain=${domain}
brand-fallback
></ha-domain-icon>`;
}
}
if (targetType === "entity" && hass.states[targetId]) {
return html`<ha-state-icon
.hass=${hass}
.stateObj=${hass.states[targetId]}
></ha-state-icon>`;
}
if (targetType === "label" && getLabel) {
const label = getLabel(targetId);
if (label?.icon) {
return html`<ha-icon .icon=${label.icon}></ha-icon>`;
}
return html`<ha-svg-icon .path=${mdiLabel}></ha-svg-icon>`;
}
return nothing;
};

View File

@@ -0,0 +1,68 @@
import { computeAreaName } from "../../../../common/entity/compute_area_name";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
import { computeFloorName } from "../../../../common/entity/compute_floor_name";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import type { HomeAssistant } from "../../../../types";
export const getTargetText = (
hass: HomeAssistant,
targetType: "floor" | "area" | "device" | "entity" | "label",
targetId: string,
getLabel?: (id: string) => LabelRegistryEntry | undefined
): string => {
if (targetType === "floor") {
return (
(hass.floors[targetId] && computeFloorName(hass.floors[targetId])) ||
hass.localize(
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_floor"
)
);
}
if (targetType === "area") {
return (
(hass.areas[targetId] && computeAreaName(hass.areas[targetId])) ||
hass.localize(
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_area"
)
);
}
if (targetType === "device") {
return (
(hass.devices[targetId] && computeDeviceName(hass.devices[targetId])) ||
hass.localize(
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_device"
)
);
}
if (targetType === "entity" && hass.states[targetId]) {
const stateObj = hass.states[targetId];
const [entityName, deviceName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
hass.entities,
hass.devices,
hass.areas,
hass.floors
);
return entityName || deviceName || targetId;
}
if (targetType === "entity") {
return hass.localize(
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_entity"
);
}
if (targetType === "label" && getLabel) {
const label = getLabel(targetId);
return (
label?.name ||
hass.localize(
"ui.panel.config.automation.editor.actions.type.service.description.target_unknown_label"
)
);
}
return targetId;
};

View File

@@ -0,0 +1,260 @@
import { consume } from "@lit/context";
import { mdiAlert, mdiFormatListBulleted, mdiShape } from "@mdi/js";
import type { HassServiceTarget } from "home-assistant-js-websocket";
import { LitElement, css, html, nothing, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { until } from "lit/directives/until";
import { ensureArray } from "../../../../common/array/ensure-array";
import "../../../../components/ha-svg-icon";
import {
getConfigEntries,
type ConfigEntry,
} from "../../../../data/config_entries";
import {
areasContext,
devicesContext,
floorsContext,
labelsContext,
localizeContext,
statesContext,
} from "../../../../data/context";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import type { HomeAssistant } from "../../../../types";
import { getTargetIcon } from "./get_target_icon";
import { getTargetText } from "./get_target_text";
@customElement("ha-automation-row-targets")
export class HaAutomationRowTargets extends LitElement {
@property({ attribute: false })
public hass!: HomeAssistant;
@property({ attribute: false })
public target?: HassServiceTarget;
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
@state()
@consume({ context: floorsContext, subscribe: true })
private floors!: HomeAssistant["floors"];
@state()
@consume({ context: areasContext, subscribe: true })
private areas!: HomeAssistant["areas"];
@state()
@consume({ context: devicesContext, subscribe: true })
private devices!: HomeAssistant["devices"];
@state()
@consume({ context: statesContext, subscribe: true })
private states!: HomeAssistant["states"];
@state()
@consume({ context: labelsContext, subscribe: true })
private _labelRegistry!: LabelRegistryEntry[];
private _configEntryLookup?: Record<string, ConfigEntry>;
protected render() {
const length = Object.keys(this.target || {}).length;
if (!length) {
return html`<span class="target">
<div class="label">
${this.localize(
"ui.panel.config.automation.editor.target_summary.no_target"
)}
</div>
</span>`;
}
const totalLength = Object.values(this.target || {}).reduce(
(acc, val) => acc + ensureArray(val).length,
0
);
if (totalLength <= 5) {
const targets = Object.entries(this.target!).reduce<
["floor" | "area" | "device" | "entity" | "label", string][]
>((acc, [targetType, targetId]) => {
const type = targetType.replace("_id", "") as
| "floor"
| "area"
| "device"
| "entity"
| "label";
return [
...acc,
...ensureArray(targetId).map((id): [typeof type, string] => [
type,
id,
]),
];
}, []);
return targets.map(
([targetType, targetId]) =>
html`<span class="target-wrapper">
${this._renderTarget(targetType, targetId)}
</span>`
);
}
return html`<span class="target">
<ha-svg-icon .path=${mdiFormatListBulleted}></ha-svg-icon>
<div class="label">
${this.localize(
"ui.panel.config.automation.editor.target_summary.targets",
{
count: totalLength,
}
)}
</div>
</span>`;
}
private _getLabel = (id: string) =>
this._labelRegistry?.find(({ label_id }) => label_id === id);
private _checkTargetExists(
targetType: "floor" | "area" | "device" | "entity" | "label",
targetId: string
): boolean {
if (targetType === "floor") {
return !!this.floors[targetId];
}
if (targetType === "area") {
return !!this.areas[targetId];
}
if (targetType === "device") {
return !!this.devices[targetId];
}
if (targetType === "entity") {
return !!this.states[targetId];
}
if (targetType === "label") {
return !!this._getLabel(targetId);
}
return false;
}
private _renderTargetBadge(
icon: TemplateResult | typeof nothing,
label: string,
alert = false
) {
return html`<div class="target ${alert ? "alert" : ""}">
${icon}
<div class="label">${label}</div>
</div>`;
}
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
}
private _renderTarget(
targetType: "floor" | "area" | "device" | "entity" | "label",
targetId: string
) {
if (targetType === "entity" && ["all", "none"].includes(targetId)) {
return this._renderTargetBadge(
html`<ha-svg-icon .path=${mdiShape}></ha-svg-icon>`,
this.localize(
`ui.panel.config.automation.editor.target_summary.${targetId as "all" | "none"}_entities`
)
);
}
const exists = this._checkTargetExists(targetType, targetId);
if (!exists) {
return this._renderTargetBadge(
html`<ha-svg-icon .path=${mdiAlert}></ha-svg-icon>`,
getTargetText(this.hass, targetType, targetId, this._getLabel),
true
);
}
if (targetType === "device" && !this._configEntryLookup) {
const loadConfigEntries = this._loadConfigEntries().then(() =>
this._renderTargetBadge(
getTargetIcon(
this.hass,
targetType,
targetId,
this._configEntryLookup!
),
getTargetText(this.hass, targetType, targetId)
)
);
return html`${until(loadConfigEntries, nothing)}`;
}
return this._renderTargetBadge(
getTargetIcon(
this.hass,
targetType,
targetId,
this._configEntryLookup || {},
this._getLabel
),
getTargetText(this.hass, targetType, targetId, this._getLabel)
);
}
static styles = css`
:host {
display: contents;
min-height: 32px;
}
.target-wrapper {
display: inline-flex;
align-items: flex-end;
gap: var(--ha-space-1);
}
.target {
display: inline-flex;
gap: var(--ha-space-1);
justify-content: center;
align-items: center;
border-radius: var(--ha-border-radius-md);
background: var(--ha-color-fill-neutral-normal-resting);
padding: 0 var(--ha-space-2) 0 var(--ha-space-1);
color: var(--ha-color-on-neutral-normal);
overflow: hidden;
height: 32px;
}
.target.alert {
background: var(--ha-color-fill-warning-normal-resting);
color: var(--ha-color-on-warning-normal);
}
.target .label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.target ha-icon,
.target ha-svg-icon,
.target ha-domain-icon {
display: flex;
padding: var(--ha-space-1) 0;
}
.target ha-floor-icon {
display: flex;
height: 32px;
align-items: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-row-targets": HaAutomationRowTargets;
}
}

View File

@@ -15,7 +15,10 @@ import {
mdiRenameBox, mdiRenameBox,
mdiStopCircleOutline, mdiStopCircleOutline,
} from "@mdi/js"; } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket"; import type {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import { dump } from "js-yaml"; import { dump } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit"; import { LitElement, css, html, nothing } from "lit";
@@ -44,6 +47,7 @@ import "../../../../components/ha-svg-icon";
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon"; import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
import type { import type {
AutomationClipboard, AutomationClipboard,
PlatformTrigger,
Trigger, Trigger,
TriggerList, TriggerList,
TriggerSidebarConfig, TriggerSidebarConfig,
@@ -64,6 +68,7 @@ import { isMac } from "../../../../util/is_mac";
import { showToast } from "../../../../util/toast"; import { showToast } from "../../../../util/toast";
import "../ha-automation-editor-warning"; import "../ha-automation-editor-warning";
import { overflowStyles, rowStyles } from "../styles"; import { overflowStyles, rowStyles } from "../styles";
import "../target/ha-automation-row-targets";
import "./ha-automation-trigger-editor"; import "./ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor"; import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor";
import "./types/ha-automation-trigger-calendar"; import "./types/ha-automation-trigger-calendar";
@@ -205,6 +210,11 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-trigger-icon>`} ></ha-trigger-icon>`}
<h3 slot="header"> <h3 slot="header">
${describeTrigger(this.trigger, this.hass, this._entityReg)} ${describeTrigger(this.trigger, this.hass, this._entityReg)}
${type === "platform" &&
"target" in
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
? this._renderTargets((this.trigger as PlatformTrigger).target)
: nothing}
</h3> </h3>
<slot name="icons" slot="icons"></slot> <slot name="icons" slot="icons"></slot>
@@ -450,6 +460,14 @@ export default class HaAutomationTriggerRow extends LitElement {
`; `;
} }
private _renderTargets = memoizeOne(
(target?: HassServiceTarget) =>
html`<ha-automation-row-targets
.hass=${this.hass}
.target=${target}
></ha-automation-row-targets>`
);
protected willUpdate(changedProperties) { protected willUpdate(changedProperties) {
// on yaml toggle --> clear warnings // on yaml toggle --> clear warnings
if (changedProperties.has("yamlMode")) { if (changedProperties.has("yamlMode")) {

View File

@@ -4077,6 +4077,13 @@
"services": "Services", "services": "Services",
"helpers": "Helpers", "helpers": "Helpers",
"entity_hidden": "[%key:ui::panel::config::devices::entities::hidden%]", "entity_hidden": "[%key:ui::panel::config::devices::entities::hidden%]",
"target_summary": {
"no_target": "No target set",
"targets": "{count} {count, plural,\n one {target}\n other {targets}\n}",
"invalid": "Invalid target",
"all_entities": "All entities",
"none_entities": "No entities"
},
"triggers": { "triggers": {
"name": "Triggers", "name": "Triggers",
"header": "When", "header": "When",
@@ -4588,11 +4595,11 @@
"service": "Perform an action", "service": "Perform an action",
"target_template": "templated {name}", "target_template": "templated {name}",
"target_every_entity": "every entity", "target_every_entity": "every entity",
"target_unknown_entity": "unknown entity", "target_unknown_entity": "Unknown entity",
"target_unknown_device": "unknown device", "target_unknown_device": "Unknown device",
"target_unknown_area": "unknown area", "target_unknown_area": "Unknown area",
"target_unknown_floor": "unknown floor", "target_unknown_floor": "Unknown floor",
"target_unknown_label": "unknown label" "target_unknown_label": "Unknown label"
} }
}, },
"play_media": { "play_media": {