From c93942919b2875d28010551103965f1c0574cf80 Mon Sep 17 00:00:00 2001 From: Wendelin <12148533+wendevlin@users.noreply.github.com> Date: Tue, 16 Dec 2025 08:42:38 +0100 Subject: [PATCH] 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 --- .../src/pages/automation/describe-action.ts | 4 +- src/components/ha-automation-row.ts | 17 +- src/components/ha-domain-icon.ts | 1 + src/components/trace/ha-trace-path-details.ts | 13 +- src/components/trace/hat-trace-timeline.ts | 46 +--- src/data/script_i18n.ts | 124 +-------- .../action/ha-automation-action-row.ts | 48 ++-- .../add-automation-element-dialog.ts | 38 +-- .../ha-automation-add-items.ts | 95 +------ .../condition/ha-automation-condition-row.ts | 15 + src/panels/config/automation/styles.ts | 6 + .../automation/target/get_target_icon.ts | 69 +++++ .../automation/target/get_target_text.ts | 68 +++++ .../target/ha-automation-row-targets.ts | 260 ++++++++++++++++++ .../trigger/ha-automation-trigger-row.ts | 20 +- src/translations/en.json | 17 +- 16 files changed, 510 insertions(+), 331 deletions(-) create mode 100644 src/panels/config/automation/target/get_target_icon.ts create mode 100644 src/panels/config/automation/target/get_target_text.ts create mode 100644 src/panels/config/automation/target/ha-automation-row-targets.ts diff --git a/gallery/src/pages/automation/describe-action.ts b/gallery/src/pages/automation/describe-action.ts index e16cf3df79..d959a9586e 100644 --- a/gallery/src/pages/automation/describe-action.ts +++ b/gallery/src/pages/automation/describe-action.ts @@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
${this._action - ? describeAction(this.hass, [], [], {}, this._action) + ? describeAction(this.hass, [], this._action) : ""} html`
- ${describeAction(this.hass, [], [], {}, conf as any)} + ${describeAction(this.hass, [], conf as any)}
${dump(conf)}
` diff --git a/src/components/ha-automation-row.ts b/src/components/ha-automation-row.ts index 02d748e595..82e4e03ca4 100644 --- a/src/components/ha-automation-row.ts +++ b/src/components/ha-automation-row.ts @@ -52,7 +52,9 @@ export class HaAutomationRow extends LitElement {
- +
+ +
`; } @@ -118,12 +120,11 @@ export class HaAutomationRow extends LitElement { } .row { display: flex; - padding: var(--ha-space-0) var(--ha-space-2); + padding: 0 var(--ha-space-3); min-height: 48px; - align-items: center; + align-items: flex-start; cursor: pointer; overflow: hidden; - font-weight: var(--ha-font-weight-medium); outline: none; 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); border-radius: var(--ha-border-radius-md); padding: var(--ha-space-1); + margin-top: 10px; display: flex; justify-content: center; align-items: center; transform: rotate(45deg); } + .leading-icon-wrapper { + padding-top: var(--ha-space-3); + } ::slotted([slot="leading-icon"]) { color: var(--ha-color-on-neutral-quiet); } @@ -172,6 +177,10 @@ export class HaAutomationRow extends LitElement { overflow-wrap: anywhere; margin: var(--ha-space-0) var(--ha-space-3); } + .icons { + display: flex; + align-items: center; + } :host([sort-selected]) .row { outline: solid; outline-color: rgba(var(--rgb-accent-color), 0.6); diff --git a/src/components/ha-domain-icon.ts b/src/components/ha-domain-icon.ts index 7d30d2f4a9..d41a0799de 100644 --- a/src/components/ha-domain-icon.ts +++ b/src/components/ha-domain-icon.ts @@ -47,6 +47,7 @@ export class HaDomainIcon extends LitElement { if (icn) { return html``; } + return this._renderFallback(); }); diff --git a/src/components/trace/ha-trace-path-details.ts b/src/components/trace/ha-trace-path-details.ts index de7969a760..84e55a66e4 100644 --- a/src/components/trace/ha-trace-path-details.ts +++ b/src/components/trace/ha-trace-path-details.ts @@ -6,13 +6,8 @@ import { customElement, property, state } from "lit/decorators"; import { classMap } from "lit/directives/class-map"; import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; import { describeCondition, describeTrigger } from "../../data/automation_i18n"; -import { - floorsContext, - fullEntitiesContext, - labelsContext, -} from "../../data/context"; +import { fullEntitiesContext, labelsContext } from "../../data/context"; 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 { describeAction } from "../../data/script_i18n"; @@ -63,10 +58,6 @@ export class HaTracePathDetails extends LitElement { @consume({ context: labelsContext, subscribe: true }) _labelReg!: LabelRegistryEntry[]; - @state() - @consume({ context: floorsContext, subscribe: true }) - _floorReg!: Record; - protected render(): TemplateResult { return html`
@@ -193,8 +184,6 @@ export class HaTracePathDetails extends LitElement { ${describeAction( this.hass, this._entityReg, - this._labelReg, - this._floorReg, currentDetail )} ` diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index 44c3df45ad..38a0bf7cb4 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -15,14 +15,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim import { relativeTime } from "../../common/datetime/relative_time"; import { fireEvent } from "../../common/dom/fire_event"; import { toggleAttribute } from "../../common/dom/toggle_attribute"; -import { - floorsContext, - fullEntitiesContext, - labelsContext, -} from "../../data/context"; +import { fullEntitiesContext } from "../../data/context"; 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 { ChooseAction, @@ -197,8 +191,6 @@ class ActionRenderer { constructor( private hass: HomeAssistant, private entityReg: EntityRegistryEntry[], - private labelReg: LabelRegistryEntry[], - private floorReg: Record, private entries: TemplateResult[], private trace: AutomationTraceExtended, private logbookRenderer: LogbookRenderer, @@ -313,14 +305,7 @@ class ActionRenderer { this._renderEntry( path, - describeAction( - this.hass, - this.entityReg, - this.labelReg, - this.floorReg, - data, - actionType - ), + describeAction(this.hass, this.entityReg, data, actionType), undefined, data.enabled === false ); @@ -485,13 +470,7 @@ class ActionRenderer { const name = repeatConfig.alias || - describeAction( - this.hass, - this.entityReg, - this.labelReg, - this.floorReg, - repeatConfig - ); + describeAction(this.hass, this.entityReg, repeatConfig); this._renderEntry(repeatPath, name, undefined, disabled); @@ -585,14 +564,7 @@ class ActionRenderer { this._renderEntry( sequencePath, sequenceConfig.alias || - describeAction( - this.hass, - this.entityReg, - this.labelReg, - this.floorReg, - sequenceConfig, - "sequence" - ), + describeAction(this.hass, this.entityReg, sequenceConfig, "sequence"), undefined, sequenceConfig.enabled === false ); @@ -683,14 +655,6 @@ export class HaAutomationTracer extends LitElement { @consume({ context: fullEntitiesContext, subscribe: true }) _entityReg!: EntityRegistryEntry[]; - @state() - @consume({ context: labelsContext, subscribe: true }) - _labelReg!: LabelRegistryEntry[]; - - @state() - @consume({ context: floorsContext, subscribe: true }) - _floorReg!: Record; - protected render() { if (!this.trace) { return nothing; @@ -707,8 +671,6 @@ export class HaAutomationTracer extends LitElement { const actionRenderer = new ActionRenderer( this.hass, this._entityReg, - this._labelReg, - this._floorReg, entries, this.trace, logbookRenderer, diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index ecc1959ab9..b9c711f6f6 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -1,7 +1,6 @@ import { ensureArray } from "../common/array/ensure-array"; import { formatNumericDuration } from "../common/datetime/format_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 { formatListWithAnds } from "../common/string/format-list"; import { isTemplate } from "../common/string/has-template"; @@ -10,13 +9,7 @@ import type { Condition } from "./automation"; import { describeCondition } from "./automation_i18n"; import { localizeDeviceAutomationAction } from "./device/device_automation"; 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 type { LabelRegistryEntry } from "./label/label_registry"; import type { ActionType, ActionTypes, @@ -41,8 +34,6 @@ const actionTranslationBaseKey = export const describeAction = ( hass: HomeAssistant, entityRegistry: EntityRegistryEntry[], - labelRegistry: LabelRegistryEntry[], - floorRegistry: Record, action: ActionTypes[T], actionType?: T, ignoreAlias = false @@ -51,8 +42,6 @@ export const describeAction = ( const description = tryDescribeAction( hass, entityRegistry, - labelRegistry, - floorRegistry, action, actionType, ignoreAlias @@ -75,8 +64,6 @@ export const describeAction = ( const tryDescribeAction = ( hass: HomeAssistant, entityRegistry: EntityRegistryEntry[], - labelRegistry: LabelRegistryEntry[], - floorRegistry: Record, action: ActionTypes[T], actionType?: T, ignoreAlias = false @@ -100,107 +87,6 @@ const tryDescribeAction = ( { 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 ( @@ -229,26 +115,20 @@ const tryDescribeAction = ( if (config.metadata) { return hass.localize( - targets.length - ? `${actionTranslationBaseKey}.service.description.service_name` - : `${actionTranslationBaseKey}.service.description.service_name_no_targets`, + `${actionTranslationBaseKey}.service.description.service_name_no_targets`, { domain: domainToName(hass.localize, domain), name: service || config.action, - targets: formatListWithAnds(hass.locale, targets), } ); } return hass.localize( - targets.length - ? `${actionTranslationBaseKey}.service.description.service_based_on_name` - : `${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`, + `${actionTranslationBaseKey}.service.description.service_based_on_name_no_targets`, { name: service ? `${domainToName(hass.localize, domain)}: ${service}` : config.action, - targets: formatListWithAnds(hass.locale, targets), } ); } 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 16d9b5ac3a..6f7f150688 100644 --- a/src/panels/config/automation/action/ha-automation-action-row.ts +++ b/src/panels/config/automation/action/ha-automation-action-row.ts @@ -17,6 +17,7 @@ import { mdiStopCircleOutline, } from "@mdi/js"; import deepClone from "deep-clone-simple"; +import type { HassServiceTarget } from "home-assistant-js-websocket"; import { dump } from "js-yaml"; import type { PropertyValues, TemplateResult } from "lit"; import { LitElement, html, nothing } from "lit"; @@ -53,18 +54,13 @@ import type { } from "../../../../data/automation"; import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition"; import { validateConfig } from "../../../../data/config"; -import { - floorsContext, - fullEntitiesContext, - labelsContext, -} from "../../../../data/context"; +import { fullEntitiesContext } from "../../../../data/context"; 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 { Action, NonConditionAction, RepeatAction, + ServiceAction, } from "../../../../data/script"; import { getActionType, isAction } from "../../../../data/script"; import { describeAction } from "../../../../data/script_i18n"; @@ -78,6 +74,7 @@ import { isMac } from "../../../../util/is_mac"; import { showToast } from "../../../../util/toast"; import "../ha-automation-editor-warning"; import { overflowStyles, rowStyles } from "../styles"; +import "../target/ha-automation-row-targets"; import "./ha-automation-action-editor"; import type HaAutomationActionEditor from "./ha-automation-action-editor"; import "./types/ha-automation-action-choose"; @@ -176,14 +173,6 @@ export default class HaAutomationActionRow extends LitElement { @consume({ context: fullEntitiesContext, subscribe: true }) _entityReg!: EntityRegistryEntry[]; - @state() - @consume({ context: labelsContext, subscribe: true }) - _labelReg!: LabelRegistryEntry[]; - - @state() - @consume({ context: floorsContext, subscribe: true }) - _floorReg!: Record; - @state() private _uiModeAvailable = true; @state() private _yamlMode = false; @@ -263,14 +252,11 @@ export default class HaAutomationActionRow extends LitElement { `}

${capitalizeFirstLetter( - describeAction( - this.hass, - this._entityReg, - this._labelReg, - this._floorReg, - this.action - ) + describeAction(this.hass, this._entityReg, this.action) )} + ${type === "service" && "target" in this.action + ? this._renderTargets((this.action as ServiceAction).target) + : nothing}

@@ -556,6 +542,14 @@ export default class HaAutomationActionRow extends LitElement { `; } + private _renderTargets = memoizeOne( + (target?: HassServiceTarget) => + html`` + ); + private _onValueChange(event: CustomEvent) { // reload sidebar if sort, deleted,... happend if (this._selected && this.optionsInSidebar) { @@ -668,15 +662,7 @@ export default class HaAutomationActionRow extends LitElement { ), inputType: "string", placeholder: capitalizeFirstLetter( - describeAction( - this.hass, - this._entityReg, - this._labelReg, - this._floorReg, - this.action, - undefined, - true - ) + describeAction(this.hass, this._entityReg, this.action, undefined, true) ), defaultValue: this.action.alias, confirmText: this.hass.localize("ui.common.submit"), diff --git a/src/panels/config/automation/add-automation-element-dialog.ts b/src/panels/config/automation/add-automation-element-dialog.ts index c97a7c861b..bb446b5e76 100644 --- a/src/panels/config/automation/add-automation-element-dialog.ts +++ b/src/panels/config/automation/add-automation-element-dialog.ts @@ -18,7 +18,6 @@ import memoizeOne from "memoize-one"; import { fireEvent } from "../../../common/dom/fire_event"; import { mainWindow } from "../../../common/dom/get_main_window"; import { computeAreaName } from "../../../common/entity/compute_area_name"; -import { computeDeviceName } from "../../../common/entity/compute_device_name"; import { computeDomain } from "../../../common/entity/compute_domain"; import { computeEntityNameList } from "../../../common/entity/compute_entity_name_display"; 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 type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog"; import { PASTE_VALUE } from "./show-add-automation-element-dialog"; +import { getTargetText } from "./target/get_target_text"; const TYPES = { trigger: { collections: TRIGGER_COLLECTIONS, icons: TRIGGER_ICONS }, @@ -1393,8 +1393,8 @@ class DialogAddAutomationElement } ); - private _getLabel = memoizeOne((labelId) => - this._labelRegistry?.find(({ label_id }) => label_id === labelId) + private _getLabel = memoizeOne((id: string) => + this._labelRegistry?.find(({ label_id }) => label_id === id) ); private _getDomainType(domain: string) { @@ -1926,32 +1926,12 @@ class DialogAddAutomationElement } if (targetId) { - if (targetType === "floor") { - return computeFloorName(this.hass.floors[targetId]) || targetId; - } - if (targetType === "area") { - return computeAreaName(this.hass.areas[targetId]) || targetId; - } - 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 getTargetText( + this.hass, + targetType as "floor" | "area" | "device" | "entity" | "label", + targetId, + this._getLabel + ); } return undefined; diff --git a/src/panels/config/automation/add-automation-element/ha-automation-add-items.ts b/src/panels/config/automation/add-automation-element/ha-automation-add-items.ts index 510b8d9067..1018f4fc2a 100644 --- a/src/panels/config/automation/add-automation-element/ha-automation-add-items.ts +++ b/src/panels/config/automation/add-automation-element/ha-automation-add-items.ts @@ -1,10 +1,5 @@ -import { - mdiInformationOutline, - mdiLabel, - mdiPlus, - mdiTextureBox, -} from "@mdi/js"; -import { LitElement, css, html, nothing, type TemplateResult } from "lit"; +import { mdiInformationOutline, mdiPlus } from "@mdi/js"; +import { LitElement, css, html, nothing } from "lit"; import { customElement, eventOptions, @@ -17,17 +12,15 @@ import { repeat } from "lit/directives/repeat"; import memoizeOne from "memoize-one"; import { fireEvent } from "../../../../common/dom/fire_event"; 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-item"; import "../../../../components/ha-svg-icon"; import "../../../../components/ha-tooltip"; import type { ConfigEntry } from "../../../../data/config_entries"; +import type { LabelRegistryEntry } from "../../../../data/label/label_registry"; import type { HomeAssistant } from "../../../../types"; import type { AddAutomationElementListItem } from "../add-automation-element-dialog"; +import { getTargetIcon } from "../target/get_target_icon"; type Target = [string, string | undefined, string | undefined]; @@ -50,7 +43,7 @@ export class HaAutomationAddItems extends LitElement { @property({ attribute: false }) public getLabel!: ( id: string - ) => { name: string; icon?: string } | undefined; + ) => LabelRegistryEntry | undefined; @property({ attribute: false }) public configEntryLookup: Record< string, @@ -164,72 +157,17 @@ export class HaAutomationAddItems extends LitElement { } return html`
- ${this._getSelectedTargetIcon(target[0], target[1])} + ${getTargetIcon( + this.hass, + target[0], + target[1], + this.configEntryLookup, + this.getLabel + )}
${target[2]}
`; }); - private _getSelectedTargetIcon( - targetType: string, - targetId: string | undefined - ): TemplateResult | typeof nothing { - if (!targetId) { - return nothing; - } - - if (targetType === "floor") { - return html``; - } - - if (targetType === "area" && this.hass.areas[targetId]) { - const area = this.hass.areas[targetId]; - if (area.icon) { - return html``; - } - return html``; - } - - 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``; - } - } - - if (targetType === "entity" && this.hass.states[targetId]) { - const stateObj = this.hass.states[targetId]; - if (stateObj) { - return html``; - } - } - - if (targetType === "label") { - const label = this.getLabel(targetId); - if (label?.icon) { - return html``; - } - return html``; - } - - return nothing; - } - private _selected(ev) { const item = ev.currentTarget; fireEvent(this, "value-changed", { @@ -335,10 +273,6 @@ export class HaAutomationAddItems extends LitElement { border-bottom: 1px solid var(--ha-color-border-neutral-quiet); } - ha-icon-next { - width: var(--ha-space-6); - } - ha-svg-icon.plus { color: var(--primary-color); } @@ -362,16 +296,11 @@ export class HaAutomationAddItems extends LitElement { .selected-target ha-icon, .selected-target ha-svg-icon, - .selected-target state-badge, .selected-target ha-domain-icon { display: flex; padding: var(--ha-space-1) 0; } - .selected-target state-badge { - --mdc-icon-size: 24px; - } - .selected-target state-badge, .selected-target ha-floor-icon { display: flex; height: 32px; diff --git a/src/panels/config/automation/condition/ha-automation-condition-row.ts b/src/panels/config/automation/condition/ha-automation-condition-row.ts index ee41c0074a..a3314d53f9 100644 --- a/src/panels/config/automation/condition/ha-automation-condition-row.ts +++ b/src/panels/config/automation/condition/ha-automation-condition-row.ts @@ -16,6 +16,7 @@ import { mdiStopCircleOutline, } from "@mdi/js"; import deepClone from "deep-clone-simple"; +import type { HassServiceTarget } from "home-assistant-js-websocket"; import { dump } from "js-yaml"; import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; import { LitElement, css, html, nothing } from "lit"; @@ -43,6 +44,7 @@ import type { AutomationClipboard, Condition, ConditionSidebarConfig, + PlatformCondition, } from "../../../../data/automation"; import { isCondition, testCondition } from "../../../../data/automation"; import { describeCondition } from "../../../../data/automation_i18n"; @@ -60,6 +62,7 @@ import { isMac } from "../../../../util/is_mac"; import { showToast } from "../../../../util/toast"; import "../ha-automation-editor-warning"; import { overflowStyles, rowStyles } from "../styles"; +import "../target/ha-automation-row-targets"; import "./ha-automation-condition-editor"; import type HaAutomationConditionEditor from "./ha-automation-condition-editor"; import "./types/ha-automation-condition-and"; @@ -191,6 +194,10 @@ export default class HaAutomationConditionRow extends LitElement { ${capitalizeFirstLetter( describeCondition(this.condition, this.hass, this._entityReg) )} + ${"target" in + (this.conditionDescriptions[this.condition.condition] || {}) + ? this._renderTargets((this.condition as PlatformCondition).target) + : nothing} @@ -475,6 +482,14 @@ export default class HaAutomationConditionRow extends LitElement { `; } + private _renderTargets = memoizeOne( + (target?: HassServiceTarget) => + html`` + ); + protected firstUpdated(changedProperties: PropertyValues): void { super.firstUpdated(changedProperties); diff --git a/src/panels/config/automation/styles.ts b/src/panels/config/automation/styles.ts index 0158869d14..9928d664d7 100644 --- a/src/panels/config/automation/styles.ts +++ b/src/panels/config/automation/styles.ts @@ -14,6 +14,12 @@ export const rowStyles = css` h3 { font-size: 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 { diff --git a/src/panels/config/automation/target/get_target_icon.ts b/src/panels/config/automation/target/get_target_icon.ts new file mode 100644 index 0000000000..454d4a9113 --- /dev/null +++ b/src/panels/config/automation/target/get_target_icon.ts @@ -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, + getLabel?: (id: string) => LabelRegistryEntry | undefined +): TemplateResult | typeof nothing => { + if (!targetId) { + return nothing; + } + + if (targetType === "floor" && hass.floors[targetId]) { + return html``; + } + + if (targetType === "area") { + const area = hass.areas[targetId]; + if (area?.icon) { + return html``; + } + return html``; + } + + 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``; + } + } + + if (targetType === "entity" && hass.states[targetId]) { + return html``; + } + + if (targetType === "label" && getLabel) { + const label = getLabel(targetId); + if (label?.icon) { + return html``; + } + return html``; + } + + return nothing; +}; diff --git a/src/panels/config/automation/target/get_target_text.ts b/src/panels/config/automation/target/get_target_text.ts new file mode 100644 index 0000000000..fbe2951b4f --- /dev/null +++ b/src/panels/config/automation/target/get_target_text.ts @@ -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; +}; diff --git a/src/panels/config/automation/target/ha-automation-row-targets.ts b/src/panels/config/automation/target/ha-automation-row-targets.ts new file mode 100644 index 0000000000..8a3e681a1c --- /dev/null +++ b/src/panels/config/automation/target/ha-automation-row-targets.ts @@ -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; + + protected render() { + const length = Object.keys(this.target || {}).length; + if (!length) { + return html` +
+ ${this.localize( + "ui.panel.config.automation.editor.target_summary.no_target" + )} +
+
`; + } + 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` + ${this._renderTarget(targetType, targetId)} + ` + ); + } + + return html` + +
+ ${this.localize( + "ui.panel.config.automation.editor.target_summary.targets", + { + count: totalLength, + } + )} +
+
`; + } + + 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`
+ ${icon} +
${label}
+
`; + } + + 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``, + 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``, + 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; + } +} diff --git a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts index 1930b55fc4..99fa2ba44a 100644 --- a/src/panels/config/automation/trigger/ha-automation-trigger-row.ts +++ b/src/panels/config/automation/trigger/ha-automation-trigger-row.ts @@ -15,7 +15,10 @@ import { mdiRenameBox, mdiStopCircleOutline, } 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 type { CSSResultGroup, PropertyValues, TemplateResult } 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 type { AutomationClipboard, + PlatformTrigger, Trigger, TriggerList, TriggerSidebarConfig, @@ -64,6 +68,7 @@ import { isMac } from "../../../../util/is_mac"; import { showToast } from "../../../../util/toast"; import "../ha-automation-editor-warning"; import { overflowStyles, rowStyles } from "../styles"; +import "../target/ha-automation-row-targets"; import "./ha-automation-trigger-editor"; import type HaAutomationTriggerEditor from "./ha-automation-trigger-editor"; import "./types/ha-automation-trigger-calendar"; @@ -205,6 +210,11 @@ export default class HaAutomationTriggerRow extends LitElement { >`}

${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}

@@ -450,6 +460,14 @@ export default class HaAutomationTriggerRow extends LitElement { `; } + private _renderTargets = memoizeOne( + (target?: HassServiceTarget) => + html`` + ); + protected willUpdate(changedProperties) { // on yaml toggle --> clear warnings if (changedProperties.has("yamlMode")) { diff --git a/src/translations/en.json b/src/translations/en.json index 1c45b6a134..05a1383dbc 100644 --- a/src/translations/en.json +++ b/src/translations/en.json @@ -4077,6 +4077,13 @@ "services": "Services", "helpers": "Helpers", "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": { "name": "Triggers", "header": "When", @@ -4588,11 +4595,11 @@ "service": "Perform an action", "target_template": "templated {name}", "target_every_entity": "every entity", - "target_unknown_entity": "unknown entity", - "target_unknown_device": "unknown device", - "target_unknown_area": "unknown area", - "target_unknown_floor": "unknown floor", - "target_unknown_label": "unknown label" + "target_unknown_entity": "Unknown entity", + "target_unknown_device": "Unknown device", + "target_unknown_area": "Unknown area", + "target_unknown_floor": "Unknown floor", + "target_unknown_label": "Unknown label" } }, "play_media": {