Compare commits

..

3 Commits

Author SHA1 Message Date
Aidan Timson 73e05237a4 C 2026-05-21 14:44:20 +01:00
Aidan Timson 6137d7ffff Add target UI 2026-05-21 14:42:18 +01:00
Aidan Timson 625324846f Add target UI 2026-05-21 14:39:37 +01:00
25 changed files with 756 additions and 914 deletions
+1 -1
View File
@@ -86,7 +86,7 @@
"core-js": "3.49.0",
"cropperjs": "1.6.2",
"culori": "4.0.2",
"date-fns": "4.2.1",
"date-fns": "4.2.0",
"deep-clone-simple": "1.1.1",
"deep-freeze": "0.0.1",
"dialog-polyfill": "0.5.6",
+2 -8
View File
@@ -1,4 +1,3 @@
import { createContext } from "@lit/context";
import type {
Connection,
HassEntityAttributeBase,
@@ -491,12 +490,12 @@ export const migrateAutomationTrigger = (
export const flattenTriggers = (
triggers: undefined | Trigger | Trigger[]
): Exclude<Trigger, TriggerList>[] => {
): Trigger[] => {
if (!triggers) {
return [];
}
const flatTriggers: Exclude<Trigger, TriggerList>[] = [];
const flatTriggers: Trigger[] = [];
ensureArray(triggers).forEach((t) => {
if ("triggers" in t) {
@@ -615,7 +614,6 @@ export interface BaseSidebarConfig {
export interface TriggerSidebarConfig extends BaseSidebarConfig {
save: (value: Trigger) => void;
editId: () => void;
rename: () => void;
disable: () => void;
duplicate: () => void;
@@ -699,7 +697,3 @@ export interface ShowAutomationEditorParams {
data?: Partial<AutomationConfig>;
expanded?: boolean;
}
export const automationConfigContext = createContext<
AutomationConfig | undefined
>("automationConfig");
-31
View File
@@ -27,7 +27,6 @@ import type {
LegacyTrigger,
Trigger,
} from "./automation";
import { flattenTriggers } from "./automation";
import { getConditionDomain, getConditionObjectId } from "./condition";
import type {
DeviceCondition,
@@ -108,36 +107,6 @@ const formatNumericLimitValue = (
: value;
};
export interface TriggerInfo {
id: string;
label: string;
triggerType: string;
count: number;
}
export const getTriggerInfos = (
triggers: Trigger[] | undefined,
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[]
): TriggerInfo[] => {
if (!triggers) {
return [];
}
const map = new Map<string, TriggerInfo>();
for (const t of flattenTriggers(triggers)) {
if (isTriggerList(t) || !t.id || map.get(t.id)) {
continue;
}
map.set(t.id, {
id: t.id,
label: describeTrigger(t, hass, entityRegistry),
triggerType: t.trigger,
count: 1,
});
}
return Array.from(map.values());
};
export const describeTrigger = (
trigger: Trigger,
hass: HomeAssistant,
+3 -70
View File
@@ -2,23 +2,16 @@ import { computeAreaName } from "../../common/entity/compute_area_name";
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
import { computeDomain } from "../../common/entity/compute_domain";
import { getDeviceArea } from "../../common/entity/context/get_device_context";
import type { LocalizeFunc } from "../../common/translations/localize";
import { computeRTL } from "../../common/util/compute_rtl";
import type { HaDevicePickerDeviceFilterFunc } from "../../components/device/ha-device-picker";
import type { PickerComboBoxItem } from "../../components/ha-picker-combo-box";
import type { FuseWeightedKey } from "../../resources/fuseMultiTerm";
import type { HomeAssistant } from "../../types";
import type { ConfigEntry } from "../config_entries";
import type { HaEntityPickerEntityFilterFunc } from "../entity/entity";
import type {
EntityRegistryDisplayEntry,
EntityRegistryEntry,
} from "../entity/entity_registry";
import { domainToName } from "../integration";
import {
getDeviceEntityDisplayLookup,
type DeviceEntityDisplayLookup,
type DeviceRegistryEntry,
} from "./device_registry";
export interface DevicePickerItem extends PickerComboBoxItem {
@@ -26,46 +19,6 @@ export interface DevicePickerItem extends PickerComboBoxItem {
domain_name?: string;
}
export interface DeviceAreaLabel {
areaName?: string;
viaDeviceName?: string;
viaDeviceAreaName?: string;
}
export const computeDeviceAreaLabel = (
device: DeviceRegistryEntry,
areas: HomeAssistant["areas"],
devices: HomeAssistant["devices"],
states: HomeAssistant["states"],
localize: LocalizeFunc,
language: HomeAssistant["language"],
translationMetadata: HomeAssistant["translationMetadata"],
viaDeviceEntities?: EntityRegistryEntry[] | EntityRegistryDisplayEntry[]
): DeviceAreaLabel => {
const area = getDeviceArea(device, areas);
const viaDevice = device.via_device_id
? devices[device.via_device_id]
: undefined;
const viaDeviceName = viaDevice
? computeDeviceNameDisplay(viaDevice, localize, states, viaDeviceEntities)
: undefined;
const viaDeviceArea = viaDevice ? getDeviceArea(viaDevice, areas) : undefined;
const viaDeviceAreaName = viaDeviceArea
? computeAreaName(viaDeviceArea)
: undefined;
const isRTL = computeRTL(language, translationMetadata.translations);
const areaName = area
? computeAreaName(area)
: viaDeviceAreaName
? `${viaDeviceAreaName}${isRTL ? " ◂ " : " ▸ "}${viaDeviceName}`
: viaDeviceName || undefined;
return { areaName, viaDeviceName, viaDeviceAreaName };
};
export const deviceComboBoxKeys: FuseWeightedKey[] = [
{
name: "search_labels.deviceName",
@@ -83,14 +36,6 @@ export const deviceComboBoxKeys: FuseWeightedKey[] = [
name: "search_labels.domain",
weight: 4,
},
{
name: "search_labels.viaDeviceName",
weight: 3,
},
{
name: "search_labels.viaDeviceArea",
weight: 3,
},
];
export const getDevices = (
@@ -204,19 +149,9 @@ export const getDevices = (
deviceEntityLookup[device.id]
);
const { areaName, viaDeviceName, viaDeviceAreaName } =
computeDeviceAreaLabel(
device,
hass.areas,
hass.devices,
hass.states,
hass.localize,
hass.language,
hass.translationMetadata,
device.via_device_id
? deviceEntityLookup[device.via_device_id]
: undefined
);
const area = getDeviceArea(device, hass.areas);
const areaName = area ? computeAreaName(area) : undefined;
const configEntry = device.primary_config_entry
? configEntryLookup?.[device.primary_config_entry]
@@ -239,8 +174,6 @@ export const getDevices = (
areaName: areaName || null,
domain: domain || null,
domainName: domainName || null,
viaDeviceName: viaDeviceName || null,
viaDeviceArea: viaDeviceAreaName || null,
},
sorting_label: [primary, areaName, domainName].filter(Boolean).join("_"),
};
-6
View File
@@ -8,7 +8,6 @@ import type {
Trigger,
TriggerList,
} from "./automation";
import { flattenTriggers } from "./automation";
import type { Selector, TargetSelector } from "./selector";
export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
@@ -57,11 +56,6 @@ export const TRIGGER_COLLECTIONS: AutomationElementGroupCollection[] = [
export const isTriggerList = (trigger: Trigger): trigger is TriggerList =>
"triggers" in trigger;
export const getTriggerIds = (triggers: Trigger[]): string[] =>
flattenTriggers(triggers)
.map((trigger) => trigger.id)
.filter((id): id is string => !!id);
export interface TriggerDescription {
target?: TargetSelector["target"];
fields: Record<
+44 -12
View File
@@ -51,25 +51,59 @@ interface ActionDefinition {
icon: string;
}
const ADD_TO_TARGET_PLACEHOLDER = "__HA_ADD_TO_TARGET__";
export const ADD_TO_ACTION_ICONS: Record<AddToActionKey, string> = {
automation_trigger: "mdi:robot-outline",
automation_condition: "mdi:playlist-check",
automation_action: "mdi:play-circle-outline",
script_action: "mdi:script-text-outline",
scene: "mdi:palette",
};
export const DEFAULT_ACTION_DEFS: ActionDefinition[] = [
{
translation_key: "automation_trigger",
icon: "mdi:robot-outline",
icon: ADD_TO_ACTION_ICONS.automation_trigger,
},
{
translation_key: "automation_condition",
icon: "mdi:playlist-check",
icon: ADD_TO_ACTION_ICONS.automation_condition,
},
{
translation_key: "automation_action",
icon: "mdi:play-circle-outline",
icon: ADD_TO_ACTION_ICONS.automation_action,
},
{
translation_key: "script_action",
icon: "mdi:script-text-outline",
icon: ADD_TO_ACTION_ICONS.script_action,
},
];
export const getAddToActionLabel = (
localize: LocalizeFunc,
key: AddToActionKey,
target: string
): string =>
localize(`ui.dialogs.more_info_control.add_to.actions.${key}`, { target });
export const getAddToActionLabelParts = (
localize: LocalizeFunc,
key: AddToActionKey
): [string, string] => {
const label = getAddToActionLabel(localize, key, ADD_TO_TARGET_PLACEHOLDER);
const placeholderIndex = label.indexOf(ADD_TO_TARGET_PLACEHOLDER);
if (placeholderIndex === -1) {
return [label, ""];
}
return [
label.slice(0, placeholderIndex),
label.slice(placeholderIndex + ADD_TO_TARGET_PLACEHOLDER.length),
];
};
export const getDefaultAddToActions = (
states: HomeAssistant["states"],
localize: LocalizeFunc,
@@ -81,14 +115,12 @@ export const getDefaultAddToActions = (
type: "default",
key: def.translation_key,
enabled: true,
name: localize(
`ui.dialogs.more_info_control.add_to.actions.${def.translation_key}`,
{
target:
states[entityId] !== undefined
? formatEntityName(states[entityId], undefined)
: entityId,
}
name: getAddToActionLabel(
localize,
def.translation_key,
states[entityId] !== undefined
? formatEntityName(states[entityId], undefined)
: entityId
),
icon: def.icon,
})
+61 -17
View File
@@ -5,17 +5,17 @@ import "../../components/ha-icon";
import "../../components/ha-spinner";
import "../../components/item/ha-list-item-button";
import "../../components/list/ha-list-base";
import type { HaListItemButton } from "../../components/item/ha-list-item-button";
import "../../panels/config/automation/target/ha-automation-target-badge";
import { showToast } from "../../util/toast";
import type { HASSDomCurrentTargetEvent } from "../../common/dom/fire_event";
import { fireEvent } from "../../common/dom/fire_event";
import type { HomeAssistant } from "../../types";
import {
type EntityAddToAction,
type AddToActionKey,
type EntityAddToActions,
addToActionHandler,
getDefaultAddToActions,
getAddToActionLabelParts,
} from "./add-to";
@customElement("ha-more-info-add-to")
@@ -65,14 +65,18 @@ export class HaMoreInfoAddTo extends LitElement {
}
}
private async _actionSelected(
ev: HASSDomCurrentTargetEvent<
HaListItemButton & {
action: EntityAddToAction;
}
>
) {
const action = ev.currentTarget.action;
private async _actionSelected(ev: Event) {
const item = ev.currentTarget as HTMLElement;
const actions =
item.dataset.actionSource === "external"
? this._externalActions
: this._defaultActions;
const action = actions[Number(item.dataset.actionIndex)];
if (!action) {
return;
}
if (!action.enabled) {
return;
}
@@ -110,16 +114,25 @@ export class HaMoreInfoAddTo extends LitElement {
addToActionHandler(action.key, { entity_id: this.entityId });
}
private _renderActionItems(actions: EntityAddToActions) {
private _renderActionItems(
actions: EntityAddToActions,
source: "default" | "external"
) {
return actions.map(
(action) => html`
(action, index) => html`
<ha-list-item-button
aria-label=${action.name}
data-action-index=${index}
data-action-source=${source}
.disabled=${!action.enabled}
.action=${action}
@click=${this._actionSelected}
>
<ha-icon slot="start" .icon=${action.icon}></ha-icon>
<span slot="headline">${action.name}</span>
<span slot="headline" class="action-label">
${action.type === "default"
? this._renderDefaultActionLabel(action.key)
: action.name}
</span>
${action.description
? html`<span slot="supporting-text">${action.description}</span>`
: nothing}
@@ -128,6 +141,30 @@ export class HaMoreInfoAddTo extends LitElement {
);
}
private _renderDefaultActionLabel(key: AddToActionKey) {
const [beforeTarget, afterTarget] = getAddToActionLabelParts(
this.hass.localize,
key
);
return html`${beforeTarget}${this._renderTarget()}${afterTarget}`;
}
private _renderTarget() {
return html`<ha-automation-target-badge
target-type="entity"
.targetId=${this.entityId}
.label=${this._targetLabel}
></ha-automation-target-badge>`;
}
private get _targetLabel(): string {
const stateObj = this.hass.states[this.entityId];
return stateObj
? this.hass.formatEntityName(stateObj, undefined)
: this.entityId;
}
protected async firstUpdated() {
await this._loadActions();
this._loading = false;
@@ -154,7 +191,7 @@ export class HaMoreInfoAddTo extends LitElement {
return html`
<ha-list-base>
${this._renderActionItems(this._defaultActions)}
${this._renderActionItems(this._defaultActions, "default")}
</ha-list-base>
${this._externalActions.length
? html`
@@ -164,7 +201,7 @@ export class HaMoreInfoAddTo extends LitElement {
)}
</h2>
<ha-list-base>
${this._renderActionItems(this._externalActions)}
${this._renderActionItems(this._externalActions, "external")}
</ha-list-base>
`
: nothing}
@@ -197,6 +234,13 @@ export class HaMoreInfoAddTo extends LitElement {
display: flex;
align-items: center;
}
.action-label {
display: inline-flex;
align-items: center;
gap: var(--ha-space-1);
flex-wrap: wrap;
}
`;
}
@@ -9,7 +9,6 @@ import {
} from "lit/decorators";
import { classMap } from "lit/directives/class-map";
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/ha-svg-icon";
@@ -21,7 +20,7 @@ import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import { haStyleScrollbar } from "../../../../resources/styles";
import type { HomeAssistant } from "../../../../types";
import type { AddAutomationElementListItem } from "../add-automation-element-dialog";
import { getTargetIcon } from "../target/get_target_icon";
import "../target/ha-automation-target-badge";
type Target = [string, string | undefined, string | undefined];
@@ -108,7 +107,10 @@ export class HaAutomationAddItems extends LitElement {
items,
(item) => item.key,
(item) => html`
<ha-list-item-button .value=${item.key} @click=${this._selected}>
<ha-list-item-button
data-value=${item.key}
@click=${this._selected}
>
<div slot="headline" class=${this.target ? "item-headline" : ""}>
${item.name}${this._renderTarget(this.target)}
</div>
@@ -154,33 +156,22 @@ export class HaAutomationAddItems extends LitElement {
`;
}
private _renderTarget = memoizeOne((target?: Target) => {
private _renderTarget(target?: Target) {
if (!target) {
return nothing;
}
return html`<div class="selected-target">
${getTargetIcon(
{
entities: this.hass.entities,
devices: this.hass.devices,
areas: this.hass.areas,
floors: this.hass.floors,
},
this.hass.states,
target[0],
target[1],
this.configEntryLookup,
this.getLabel
)}
<div class="label">${target[2]}</div>
</div>`;
});
return html`<ha-automation-target-badge
.targetType=${target[0]}
.targetId=${target[1]}
.label=${target[2]}
></ha-automation-target-badge>`;
}
private _selected(ev) {
const item = ev.currentTarget;
const item = ev.currentTarget as HTMLElement;
fireEvent(this, "value-changed", {
value: item.value,
value: item.dataset.value,
});
}
@@ -301,42 +292,6 @@ export class HaAutomationAddItems extends LitElement {
ha-svg-icon.plus {
color: var(--primary-color);
}
.selected-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);
border: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-quiet);
color: var(--ha-color-on-neutral-normal);
overflow: hidden;
}
.selected-target .label {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.selected-target ha-icon,
.selected-target ha-svg-icon,
.selected-target ha-domain-icon {
display: flex;
padding: var(--ha-space-1) 0;
}
.selected-target ha-floor-icon {
display: flex;
height: 32px;
width: 32px;
align-items: center;
}
.selected-target ha-domain-icon {
filter: grayscale(100%);
}
`,
];
}
@@ -25,7 +25,7 @@ import type {
} from "home-assistant-js-websocket";
import { dump } from "js-yaml";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { LitElement, html, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import memoizeOne from "memoize-one";
@@ -44,7 +44,6 @@ import type { HaAutomationRow } from "../../../../components/automation/ha-autom
import "../../../../components/automation/ha-automation-row-event-chip";
import "../../../../components/automation/ha-automation-row-live-test";
import type { LiveTestState } from "../../../../components/automation/ha-automation-row-live-test";
import "../../../../components/ha-alert";
import "../../../../components/ha-card";
import "../../../../components/ha-condition-icon";
import "../../../../components/ha-dropdown";
@@ -52,26 +51,18 @@ import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-tooltip";
import "../../../../components/ha-trigger-icon";
import type {
AutomationClipboard,
AutomationConfig,
Condition,
ConditionSidebarConfig,
PlatformCondition,
TriggerCondition,
} from "../../../../data/automation";
import {
automationConfigContext,
isCondition,
subscribeCondition,
testCondition,
} from "../../../../data/automation";
import {
describeCondition,
getTriggerInfos,
} from "../../../../data/automation_i18n";
import { describeCondition } from "../../../../data/automation_i18n";
import type { ConditionDescriptions } from "../../../../data/condition";
import { CONDITION_BUILDING_BLOCKS } from "../../../../data/condition";
import {
@@ -91,7 +82,6 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import "../ha-trigger-id-chip";
import { overflowStyles, rowStyles } from "../styles";
import "../target/ha-automation-row-targets";
import "./ha-automation-condition-editor";
@@ -165,10 +155,6 @@ export default class HaAutomationConditionRow extends LitElement {
@state() private _liveTestResult: LiveTestState = "unknown";
@state()
@consume({ context: automationConfigContext, subscribe: true })
private _automationConfig?: AutomationConfig;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg: EntityRegistryEntry[] = [];
@@ -227,13 +213,9 @@ export default class HaAutomationConditionRow extends LitElement {
.condition=${this.condition.condition}
></ha-condition-icon>
<h3 slot="header">
${this.condition.condition === "trigger"
? this._renderTriggerConditionDescription(
this.condition as TriggerCondition
)
: capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
${capitalizeFirstLetter(
describeCondition(this.condition, this.hass, this._entityReg)
)}
${target !== undefined || (descriptionHasTarget && !this._isNew)
? this._renderTargets(
target,
@@ -547,11 +529,9 @@ export default class HaAutomationConditionRow extends LitElement {
>${this._renderRow()}
<ha-automation-row-live-test
slot="icons"
.state=${this.condition.condition !== "trigger"
? this._liveTestResult
: "unknown"}
.state=${this._liveTestResult}
.label=${this.hass.localize(
`ui.panel.config.automation.editor.conditions.live_test_state.${this.condition.condition !== "trigger" ? this._liveTestResult : "unknown"}`
`ui.panel.config.automation.editor.conditions.live_test_state.${this._liveTestResult}`
)}
></ha-automation-row-live-test
></ha-automation-row>`
@@ -584,67 +564,6 @@ export default class HaAutomationConditionRow extends LitElement {
`;
}
private _getTriggerInfos = memoizeOne(getTriggerInfos);
private _renderTriggerConditionDescription(condition: TriggerCondition) {
const ids = ensureArray(condition.id ?? []).filter((id) => id !== "");
const prefix = capitalizeFirstLetter(
this.hass
.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.description.full",
{ id: "" }
)
.trim()
);
if (!ids.length) {
return html`${prefix}
<div class="trigger warning">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.description.no_trigger"
)}
</div>`;
}
const triggers = ensureArray(this._automationConfig?.triggers || []);
const triggerInfos = this._getTriggerInfos(
triggers,
this.hass,
this._entityReg
);
const infoById = new Map(triggerInfos.map((info) => [info.id, info]));
return html`${prefix}
${ids
.filter((id) => infoById.get(id))
.map((id) => {
const info = infoById.get(id)!;
const triggerIcon = html`<ha-trigger-icon
.slot=${ids.length < 4 ? "start" : ""}
.hass=${this.hass}
.trigger=${info.triggerType}
></ha-trigger-icon>`;
return html`
<div class="trigger">
${ids.length < 4 ? triggerIcon : nothing}
<ha-trigger-id-chip id=${`trigger-${id}`} .triggerId=${id}>
</ha-trigger-id-chip>
${ids.length < 4
? html`<span>${info.label}</span>`
: html`<ha-tooltip .for=${`trigger-${id}`}></ha-tooltip>`}
${ids.length >= 4
? html`<ha-tooltip .for=${`trigger-${id}`}>
${ids.length >= 4
? html`<div>${triggerIcon}${info.label}</div>`
: nothing}
</ha-tooltip>`
: nothing}
</div>
`;
})}`;
}
private _renderTargets = memoizeOne(
(
target?: HassServiceTarget,
@@ -1188,26 +1107,7 @@ export default class HaAutomationConditionRow extends LitElement {
}
static get styles(): CSSResultGroup {
return [
rowStyles,
overflowStyles,
css`
.trigger {
display: flex;
align-items: center;
gap: var(--ha-space-2);
background-color: var(--ha-color-fill-neutral-normal-resting);
border-radius: var(--ha-border-radius-md);
padding-inline: var(--ha-space-2);
color: var(--ha-color-on-neutral-normal);
height: 32px;
}
.trigger.warning {
background-color: var(--ha-color-fill-warning-normal-resting);
color: var(--ha-color-on-warning-normal);
}
`,
];
return [rowStyles, overflowStyles];
}
}
@@ -1,30 +1,26 @@
import { consume } from "@lit/context";
import { css, html, LitElement } from "lit";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import { html, LitElement } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { ensureArray } from "../../../../../common/array/ensure-array";
import { fireEvent } from "../../../../../common/dom/fire_event";
import "../../../../../components/ha-alert";
import "../../../../../components/ha-form/ha-form";
import type { SchemaUnion } from "../../../../../components/ha-form/types";
import "../../../../../components/ha-select";
import "../../../../../components/item/ha-list-item-option";
import type { HaListItemOption } from "../../../../../components/item/ha-list-item-option";
import "../../../../../components/list/ha-list-selectable";
import type { HaListSelectable } from "../../../../../components/list/ha-list-selectable";
import type { HaListSelectedDetail } from "../../../../../components/list/types";
import {
automationConfigContext,
flattenTriggers,
type AutomationConfig,
type Trigger,
type TriggerCondition,
} from "../../../../../data/automation";
import {
getTriggerInfos,
type TriggerInfo,
} from "../../../../../data/automation_i18n";
import { fullEntitiesContext } from "../../../../../data/context";
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
import type { HomeAssistant } from "../../../../../types";
import "../../ha-trigger-id-chip";
const getTriggersIds = (triggers: Trigger[]): string[] => {
const triggerIds = flattenTriggers(triggers)
.map((t) => ("id" in t ? t.id : undefined))
.filter(Boolean) as string[];
return Array.from(new Set(triggerIds));
};
@customElement("ha-automation-condition-trigger")
export class HaTriggerCondition extends LitElement {
@@ -34,25 +30,9 @@ export class HaTriggerCondition extends LitElement {
@property({ type: Boolean }) public disabled = false;
@state()
@consume({ context: automationConfigContext, subscribe: true })
private _automationConfig?: AutomationConfig;
@state() private _triggerIds: string[] = [];
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
private _entityReg: EntityRegistryEntry[] = [];
private _triggerInfos = memoizeOne(
(
triggers: AutomationConfig["triggers"] | undefined,
entityReg: EntityRegistryEntry[]
): TriggerInfo[] =>
getTriggerInfos(
triggers ? ensureArray(triggers) : undefined,
this.hass,
entityReg
)
);
private _unsub?: UnsubscribeFunc;
public static get defaultConfig(): TriggerCondition {
return {
@@ -61,106 +41,89 @@ export class HaTriggerCondition extends LitElement {
};
}
private _schema = memoizeOne(
(triggerIds: string[]) =>
[
{
name: "id",
selector: {
select: {
multiple: true,
options: triggerIds,
},
},
required: true,
},
] as const
);
connectedCallback() {
super.connectedCallback();
const details = { callback: (config) => this._automationUpdated(config) };
fireEvent(this, "subscribe-automation-config", details);
this._unsub = (details as any).unsub;
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._unsub) {
this._unsub();
}
}
protected render() {
const selectedIds: (string | number)[] = ensureArray(
this.condition.id || []
);
const triggerInfos = this._triggerInfos(
this._automationConfig?.triggers,
this._entityReg
);
if (!triggerInfos.length && !selectedIds.length) {
return html`
<ha-alert alert-type="info">
${this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
)}
</ha-alert>
`;
if (!this._triggerIds.length) {
return this.hass.localize(
"ui.panel.config.automation.editor.conditions.type.trigger.no_triggers"
);
}
const schema = this._schema(this._triggerIds);
return html`
<ha-list-selectable @ha-list-selected=${this._valueChanged} multi>
${this._renderOptions(selectedIds, triggerInfos)}
</ha-list-selectable>
<ha-form
.schema=${schema}
.data=${this.condition}
.hass=${this.hass}
.disabled=${this.disabled}
.computeLabel=${this._computeLabelCallback}
@value-changed=${this._valueChanged}
></ha-form>
`;
}
private _renderOptions(
selectedIds: (string | number)[],
triggerInfos: TriggerInfo[]
) {
return html`
${triggerInfos.map(
(info) => html`
<ha-list-item-option
.value=${info.id}
.selected=${selectedIds.includes(info.id)}
appearance="checkbox"
>
<div class="option" slot="headline">
<ha-trigger-id-chip
id=${`trigger-${info.id}`}
.triggerId=${info.id}
>
</ha-trigger-id-chip>
${info.label}
</div>
</ha-list-item-option>
`
)}
`;
private _computeLabelCallback = (
schema: SchemaUnion<ReturnType<typeof this._schema>>
): string =>
this.hass.localize(
`ui.panel.config.automation.editor.conditions.type.trigger.${schema.name}`
);
private _automationUpdated(config?: AutomationConfig) {
this._triggerIds = config?.triggers
? getTriggersIds(ensureArray(config.triggers))
: [];
}
private _valueChanged(ev: CustomEvent<HaListSelectedDetail>): void {
private _valueChanged(ev: CustomEvent): void {
ev.stopPropagation();
if (
!ev.detail.diff ||
(!ev.detail.diff?.added.size && !ev.detail.diff?.removed.size)
) {
return;
}
const newValue = ev.detail.value;
const ids = ensureArray(this.condition.id || []);
const valueSet = ev.detail.diff.added.size
? ev.detail.diff.added
: ev.detail.diff.removed;
const index = valueSet.values().next().value;
if (index === undefined) {
return;
}
const triggerId = (
(ev.currentTarget as HaListSelectable).items[index] as HaListItemOption
).value;
if (triggerId === undefined || triggerId === "") {
return;
}
if (ev.detail.diff.added.size) {
ids.push(triggerId);
} else {
const removeIndex = ids.indexOf(triggerId);
if (removeIndex > -1) {
ids.splice(removeIndex, 1);
if (typeof newValue.id === "string") {
if (!this._triggerIds.some((id) => id === newValue.id)) {
newValue.id = "";
}
} else if (Array.isArray(newValue.id)) {
newValue.id = newValue.id.filter((_id) =>
this._triggerIds.some((id) => id === _id)
);
if (!newValue.id.length) {
newValue.id = "";
}
}
fireEvent(this, "value-changed", { value: { ...this.condition, id: ids } });
fireEvent(this, "value-changed", { value: newValue });
}
static styles = css`
.option {
display: flex;
align-items: center;
gap: var(--ha-space-1);
color: var(--ha-color-on-neutral-normal);
}
`;
}
declare global {
@@ -1,5 +1,4 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { provide } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiCog,
@@ -21,9 +20,10 @@ import {
mdiTransitConnection,
mdiUndo,
} from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { customElement, property, query } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
import { fireEvent } from "../../../common/dom/fire_event";
@@ -31,7 +31,6 @@ import { goBack, navigate } from "../../../common/navigate";
import { promiseTimeout } from "../../../common/util/promise-timeout";
import "../../../components/ha-button";
import "../../../components/ha-dropdown";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
import "../../../components/ha-dropdown-item";
import "../../../components/ha-icon";
import "../../../components/ha-icon-button";
@@ -46,7 +45,6 @@ import type {
Trigger,
} from "../../../data/automation";
import {
automationConfigContext,
deleteAutomation,
fetchAutomationFileConfig,
getAutomationEditorInitData,
@@ -74,12 +72,13 @@ import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
import { haStyle } from "../../../resources/styles";
import type { Entries, ValueChangedEvent } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showEditorToast } from "./editor-toast";
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
import { ADD_AUTOMATION_ELEMENT_QUERY_PARAM } from "./show-add-automation-element-dialog";
import "./blueprint-automation-editor";
import { showEditorToast } from "./editor-toast";
import type { EditorDomainHooks } from "./ha-automation-script-editor-mixin";
import {
AutomationScriptEditorMixin,
@@ -87,7 +86,7 @@ import {
} from "./ha-automation-script-editor-mixin";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import { ADD_AUTOMATION_ELEMENT_QUERY_PARAM } from "./show-add-automation-element-dialog";
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
declare global {
interface HTMLElementTagNameMap {
@@ -95,6 +94,10 @@ declare global {
}
// for fire event
interface HASSDomEvents {
"subscribe-automation-config": {
callback: (config: AutomationConfig) => void;
unsub?: UnsubscribeFunc;
};
"ui-mode-not-available": Error;
"move-down": undefined;
"move-up": undefined;
@@ -122,9 +125,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
@provide({ context: automationConfigContext })
@state()
protected config?: AutomationConfig;
private _configSubscriptions: Record<
string,
(config?: AutomationConfig) => void
> = {};
private _configSubscriptionsId = 1;
private _newAutomationId?: string;
@@ -398,7 +404,10 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
</ha-svg-icon>
</ha-dropdown-item>
</ha-dropdown>
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
<div
class=${this.mode === "yaml" ? "yaml-mode" : ""}
@subscribe-automation-config=${this._subscribeAutomationConfig}
>
${this.mode === "gui"
? html`
<div>
@@ -629,6 +638,12 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
) {
this._setEntityId();
}
if (changedProps.has("config")) {
Object.values(this._configSubscriptions).forEach((sub) =>
sub(this.config)
);
}
}
private _setEntityId() {
@@ -1006,6 +1021,15 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
}
}
private _subscribeAutomationConfig(ev) {
const id = this._configSubscriptionsId++;
this._configSubscriptions[id] = ev.detail.callback;
ev.detail.unsub = () => {
delete this._configSubscriptions[id];
};
ev.detail.callback(this.config);
}
protected supportedShortcuts(): SupportedShortcuts {
return {
s: () => this._handleSaveAutomation(),
@@ -1,62 +0,0 @@
import { mdiPound } from "@mdi/js";
import { css, html, LitElement } from "lit";
import { customElement, property } from "lit/decorators";
import "../../../components/ha-svg-icon";
/**
* Home Assistant trigger ID chip component
*
* @element ha-trigger-id-chip
* @extends {LitElement}
*
* @summary
* A small chip that displays an automation trigger ID prefixed with a hash icon.
*
* @slot start - Optional content rendered before the hash icon (usually an icon).
*
* @attr {string} trigger-id - The trigger ID to display.
* @attr {boolean} warning - Renders the chip with warning colors.
*/
@customElement("ha-trigger-id-chip")
export class HaTriggerIdChip extends LitElement {
@property({ attribute: "trigger-id" }) public triggerId!: string;
@property({ type: Boolean, reflect: true }) public warning = false;
protected render() {
return html`
<slot name="start"></slot>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
<span>${this.triggerId}</span>
`;
}
static styles = css`
:host {
background-color: var(--card-background-color);
border-radius: var(--ha-border-radius-sm);
border: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-normal);
--mdc-icon-size: 16px;
display: inline-flex;
gap: var(--ha-space-1);
align-items: center;
color: var(--ha-color-on-neutral-normal);
padding: 0 var(--ha-space-1);
font-weight: var(--ha-font-weight-medium);
line-height: 20px;
height: 20px;
}
:host([warning]) {
border-color: var(--ha-color-border-warning-normal);
color: var(--ha-color-on-warning-normal);
background-color: var(--ha-color-fill-warning-quiet-resting);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-trigger-id-chip": HaTriggerIdChip;
}
}
@@ -32,11 +32,11 @@ import {
normalizeAutomationConfig,
} from "../../../data/automation";
import { getActionType, type Action } from "../../../data/script";
import { showEditorToast } from "./editor-toast";
import "./action/ha-automation-action";
import type HaAutomationAction from "./action/ha-automation-action";
import "./condition/ha-automation-condition";
import type HaAutomationCondition from "./condition/ha-automation-condition";
import { showEditorToast } from "./editor-toast";
import { ManualEditorMixin } from "./ha-manual-editor-mixin";
import { showPasteReplaceDialog } from "./paste-replace-dialog/show-dialog-paste-replace";
import { manualEditorStyles, saveFabStyles } from "./styles";
@@ -14,15 +14,13 @@ import {
mdiStopCircleOutline,
} from "@mdi/js";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, query, state } from "lit/decorators";
import { keyed } from "lit/directives/keyed";
import { fireEvent } from "../../../../common/dom/fire_event";
import { handleStructError } from "../../../../common/structs/handle-errors";
import type { HaDropdownSelectEvent } from "../../../../components/ha-dropdown";
import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import type {
LegacyTrigger,
Trigger,
@@ -37,7 +35,6 @@ import {
import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import "../ha-automation-comment";
import "../ha-trigger-id-chip";
import { overflowStyles, sidebarEditorStyles } from "../styles";
import "../trigger/ha-automation-trigger-editor";
import type HaAutomationTriggerEditor from "../trigger/ha-automation-trigger-editor";
@@ -60,6 +57,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@property({ type: Number, attribute: "sidebar-key" })
public sidebarKey?: number;
@state() private _requestShowId = false;
@state() private _warnings?: string[];
@query(".sidebar-editor")
@@ -67,6 +66,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
protected willUpdate(changedProperties: PropertyValues<this>) {
if (changedProperties.has("config")) {
this._requestShowId = false;
this._warnings = undefined;
if (this.config) {
this.yamlMode = this.config.yamlMode;
@@ -111,21 +111,11 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@wa-select=${this._handleDropdownSelect}
>
<span slot="title">${title}</span>
<div slot="subtitle" class="subtitle">
${subtitle}
${"id" in this.config.config
? html`<ha-trigger-id-chip
id="trigger-id-chip"
.triggerId=${(
this.config.config as Exclude<Trigger, TriggerList>
).id}
>
</ha-trigger-id-chip>`
: nothing}
${rowDisabled
? `(${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")})`
: nothing}
</div>
<span slot="subtitle"
>${subtitle}${rowDisabled
? ` (${this.hass.localize("ui.panel.config.automation.editor.actions.disabled")})`
: ""}</span
>
<ha-dropdown-item
slot="menu-items"
value="rename"
@@ -157,16 +147,18 @@ export default class HaAutomationSidebarTrigger extends LitElement {
</div>
</ha-dropdown-item>`
: nothing}
${type !== "list"
? html` <ha-dropdown-item
${!this.yamlMode &&
!("id" in this.config.config) &&
!this._requestShowId
? html`<ha-dropdown-item
slot="menu-items"
value="edit_id"
value="show_id"
.disabled=${this.disabled || type === "list"}
>
<ha-svg-icon slot="icon" .path=${mdiIdentifier}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.triggers.${"id" in this.config.config ? "edit" : "add"}_id`
"ui.panel.config.automation.editor.triggers.edit_id"
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
@@ -343,6 +335,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
@value-changed=${this._valueChangedSidebar}
@yaml-changed=${this._yamlChangedSidebar}
.uiSupported=${this.config.uiSupported}
.showId=${this._requestShowId}
.yamlMode=${this.yamlMode}
.disabled=${this.disabled}
@ui-mode-not-available=${this._handleUiModeNotAvailable}
@@ -393,6 +386,10 @@ export default class HaAutomationSidebarTrigger extends LitElement {
fireEvent(this, "toggle-yaml-mode");
};
private _showTriggerId = () => {
this._requestShowId = true;
};
private _handleDropdownSelect(ev: HaDropdownSelectEvent) {
const action = ev.detail?.item?.value;
@@ -407,8 +404,8 @@ export default class HaAutomationSidebarTrigger extends LitElement {
case "edit_comment":
this.config.editComment();
break;
case "edit_id":
this.config.editId();
case "show_id":
this._showTriggerId();
break;
case "duplicate":
this.config.duplicate();
@@ -434,16 +431,7 @@ export default class HaAutomationSidebarTrigger extends LitElement {
}
}
static styles = [
sidebarEditorStyles,
overflowStyles,
css`
.subtitle {
display: flex;
gap: var(--ha-space-1);
}
`,
];
static styles = [sidebarEditorStyles, overflowStyles];
}
declare global {
@@ -0,0 +1,251 @@
import { consume, type ContextType } from "@lit/context";
import { mdiAlert, mdiCodeBraces, mdiShape } from "@mdi/js";
import { css, html, LitElement, nothing, type TemplateResult } from "lit";
import { customElement, property, state } from "lit/decorators";
import { classMap } from "lit/directives/class-map";
import { transform } from "../../../../common/decorators/transform";
import { isTemplate } from "../../../../common/string/has-template";
import "../../../../components/ha-svg-icon";
import type { ConfigEntry } from "../../../../data/config_entries";
import {
configEntriesContext,
internationalizationContext,
labelsContext,
registriesContext,
statesContext,
} from "../../../../data/context";
import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
import type { TargetType } from "../../../../data/target";
import { getTargetIcon } from "./get_target_icon";
import { getTargetText } from "./get_target_text";
const TARGET_TYPES = ["entity", "device", "area", "label", "floor"] as const;
const isTargetType = (targetType: string): targetType is TargetType =>
TARGET_TYPES.includes(targetType as TargetType);
@customElement("ha-automation-target-badge")
export class HaAutomationTargetBadge extends LitElement {
@property({ attribute: "target-type" })
public targetType!: string;
@property({ attribute: "target-id" })
public targetId?: string;
@property()
public label?: string;
@property({ type: Boolean })
public warning = false;
@property({ type: Boolean })
public error = false;
@property({ type: Boolean })
public interactive = false;
@property({ attribute: false })
public countTemplate: unknown = nothing;
@state()
@consume({ context: internationalizationContext, subscribe: true })
private _i18n!: ContextType<typeof internationalizationContext>;
@state()
@consume({ context: registriesContext, subscribe: true })
private _registries!: ContextType<typeof registriesContext>;
@state()
@consume({ context: labelsContext, subscribe: true })
private _labelRegistry!: LabelRegistryEntry[];
@state()
@consume({ context: configEntriesContext, subscribe: true })
@transform<ConfigEntry[], Record<string, ConfigEntry>>({
transformer: function (value) {
return value
? Object.fromEntries(value.map((entry) => [entry.entry_id, entry]))
: undefined;
},
})
private _configEntryLookup?: Record<string, ConfigEntry>;
@consume({ context: statesContext, subscribe: true })
private _states!: ContextType<typeof statesContext>;
protected render() {
const { icon, label, warning } = this._targetInfo();
return html`<div
class=${classMap({
target: true,
warning,
error: this.error,
interactive: this.interactive,
})}
>
${icon}
<div class="label">${label}${this.countTemplate}</div>
</div>`;
}
private _targetInfo(): {
icon: TemplateResult | typeof nothing;
label: string;
warning: boolean;
} {
const targetId = this.targetId;
if (!targetId) {
return {
icon: nothing,
label: this.label || "",
warning: this.warning,
};
}
let iconPath: string | undefined;
let label = this.label;
let warning = this.warning;
if (!isTargetType(this.targetType)) {
return {
icon: nothing,
label: label || targetId,
warning,
};
}
if (this.targetType === "entity" && ["all", "none"].includes(targetId)) {
iconPath = mdiShape;
label = this._i18n.localize(
`ui.panel.config.automation.editor.target_summary.${targetId as "all" | "none"}_entities`
);
} else if (isTemplate(targetId)) {
iconPath = mdiCodeBraces;
label = this._i18n.localize(
"ui.panel.config.automation.editor.target_summary.template"
);
} else if (!this._checkTargetExists(targetId)) {
iconPath = mdiAlert;
label = label || this._targetText(this.targetType, targetId);
warning = true;
} else {
label = label || this._targetText(this.targetType, targetId);
}
const icon = iconPath
? html`<ha-svg-icon .path=${iconPath}></ha-svg-icon>`
: getTargetIcon(
this._registries,
this._states,
this.targetType,
targetId,
this._configEntryLookup || {},
this._getLabel
);
return {
icon,
label: label || targetId,
warning,
};
}
private _targetText(targetType: TargetType, targetId: string): string {
return getTargetText(
this._registries,
this._states,
this._i18n.localize,
targetType,
targetId,
this._getLabel
);
}
private _getLabel = (id: string) =>
this._labelRegistry?.find(({ label_id }) => label_id === id);
private _checkTargetExists(targetId: string): boolean {
if (this.targetType === "floor") {
return !!this._registries.floors[targetId];
}
if (this.targetType === "area") {
return !!this._registries.areas[targetId];
}
if (this.targetType === "device") {
return !!this._registries.devices[targetId];
}
if (this.targetType === "entity") {
return !!this._states[targetId];
}
return !!this._getLabel(targetId);
}
static styles = css`
:host {
display: inline-flex;
max-width: 100%;
vertical-align: middle;
}
.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);
border: var(--ha-border-width-sm) solid
var(--ha-color-border-neutral-quiet);
overflow: hidden;
height: 32px;
max-width: 100%;
}
.target.warning {
background: var(--ha-color-fill-warning-normal-resting);
color: var(--ha-color-on-warning-normal);
}
.target.error {
background: var(--ha-color-fill-danger-normal-resting);
color: var(--ha-color-on-danger-normal);
}
.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;
}
.target.interactive {
cursor: pointer;
}
.target.interactive:hover {
background: var(--ha-color-fill-neutral-normal-hover);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-target-badge": HaAutomationTargetBadge;
}
}
@@ -1,119 +0,0 @@
import { consume, type ContextType } from "@lit/context";
import type { CSSResultGroup } from "lit";
import { LitElement, css, html, nothing } from "lit";
import { customElement, state } from "lit/decorators";
import "../../../../components/ha-alert";
import "../../../../components/ha-button";
import "../../../../components/ha-dialog";
import "../../../../components/ha-dialog-footer";
import "../../../../components/input/ha-input";
import type { HaInput } from "../../../../components/input/ha-input";
import { internationalizationContext } from "../../../../data/context";
import { DialogMixin } from "../../../../dialogs/dialog-mixin";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import type { EditTriggerIdDialogParams } from "./show-edit-trigger-id";
@customElement("ha-automation-edit-trigger-id-dialog")
class HaAutomationEditTriggerIdDialog extends DialogMixin<EditTriggerIdDialogParams>(
LitElement
) {
@state() private _newId = "";
@state()
@consume({ context: internationalizationContext, subscribe: true })
protected _i18n!: ContextType<typeof internationalizationContext>;
connectedCallback() {
super.connectedCallback();
this._setInitialId();
}
private _setInitialId() {
if (this.params?.id) {
this._newId = this.params.id;
}
}
protected render() {
if (!this.params) {
return nothing;
}
const title = this._i18n.localize(
`ui.panel.config.automation.editor.triggers.${
this.params.id ? "edit_id" : "add_id"
}`
);
return html`
<ha-dialog open header-title=${title}>
<ha-input
autofocus
.label=${this._i18n.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this._newId}
@input=${this._idChanged}
@keydown=${this._handleKeyDown}
></ha-input>
<ha-alert alert-type="info">
${this._i18n.localize(
"ui.panel.config.automation.editor.triggers.id_description"
)}
</ha-alert>
<ha-dialog-footer slot="footer">
<ha-button
slot="secondaryAction"
appearance="plain"
@click=${this.closeDialog}
>
${this._i18n.localize("ui.common.cancel")}
</ha-button>
<ha-button slot="primaryAction" @click=${this._save}>
${this._i18n.localize("ui.common.save")}
</ha-button>
</ha-dialog-footer>
</ha-dialog>
`;
}
private _idChanged(ev: InputEvent) {
const target = ev.target as HaInput;
this._newId = target.value ?? "";
}
private _handleKeyDown(ev: KeyboardEvent) {
if (ev.key === "Enter") {
ev.preventDefault();
this._save();
}
}
private _save(): void {
const trimmed = this._newId.trim();
this.params!.onUpdate(trimmed || undefined);
this.closeDialog();
}
static get styles(): CSSResultGroup {
return [
haStyle,
haStyleDialog,
css`
ha-input {
width: 100%;
}
ha-alert {
display: block;
margin-top: var(--ha-space-6);
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-edit-trigger-id-dialog": HaAutomationEditTriggerIdDialog;
}
}
@@ -6,6 +6,7 @@ import { dynamicElement } from "../../../../common/dom/dynamic-element-directive
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../components/ha-yaml-editor";
import "../../../../components/input/ha-input";
import type { Trigger } from "../../../../data/automation";
import { migrateAutomationTrigger } from "../../../../data/automation";
import type { TriggerDescription } from "../../../../data/trigger";
@@ -30,6 +31,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
@property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false;
@property({ type: Boolean, attribute: "show-id" }) public showId = false;
@property({ attribute: false }) public description?: TriggerDescription;
@query("ha-yaml-editor") public yamlEditor?: HaYamlEditor;
@@ -39,6 +42,8 @@ export default class HaAutomationTriggerEditor extends LitElement {
const yamlMode = this.yamlMode || !this.uiSupported;
const showId = "id" in this.trigger || this.showId;
return html`
<div
class=${classMap({
@@ -72,6 +77,18 @@ export default class HaAutomationTriggerEditor extends LitElement {
></ha-yaml-editor>
`
: html`
${showId && !isTriggerList(this.trigger)
? html`
<ha-input
.label=${this.hass.localize(
"ui.panel.config.automation.editor.triggers.id"
)}
.value=${this.trigger.id || ""}
.disabled=${this.disabled}
@change=${this._idChanged}
></ha-input>
`
: nothing}
<div @value-changed=${this._onUiChanged}>
${this.description
? html`<ha-automation-trigger-platform
@@ -91,6 +108,24 @@ export default class HaAutomationTriggerEditor extends LitElement {
`;
}
private _idChanged(ev: CustomEvent) {
if (isTriggerList(this.trigger)) return;
const newId = (ev.target as any).value;
if (newId === (this.trigger.id ?? "")) {
return;
}
const value = { ...this.trigger };
if (!newId) {
delete value.id;
} else {
value.id = newId;
}
fireEvent(this, "value-changed", {
value,
});
}
private _onYamlChange(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
@@ -125,6 +160,9 @@ export default class HaAutomationTriggerEditor extends LitElement {
border-top: 1px solid var(--divider-color);
border-bottom: 1px solid var(--divider-color);
}
ha-input {
margin-bottom: var(--ha-space-3);
}
`,
];
}
@@ -11,7 +11,6 @@ import {
mdiContentPaste,
mdiDelete,
mdiDotsVertical,
mdiIdentifier,
mdiPlayCircleOutline,
mdiPlaylistEdit,
mdiPlusCircleMultipleOutline,
@@ -49,7 +48,6 @@ import "../../../../components/ha-dropdown-item";
import "../../../../components/ha-expansion-panel";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-svg-icon";
import "../../../../components/ha-tooltip";
import { TRIGGER_ICONS } from "../../../../components/ha-trigger-icon";
import type {
AutomationClipboard,
@@ -75,12 +73,10 @@ import type { HomeAssistant } from "../../../../types";
import { isMac } from "../../../../util/is_mac";
import { showEditorToast } from "../editor-toast";
import "../ha-automation-editor-warning";
import "../ha-trigger-id-chip";
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 { showEditTriggerIdDialog } from "./show-edit-trigger-id";
import "./types/ha-automation-trigger-calendar";
import "./types/ha-automation-trigger-conversation";
import "./types/ha-automation-trigger-device";
@@ -248,14 +244,6 @@ export default class HaAutomationTriggerRow extends LitElement {
.trigger=${(this.trigger as Exclude<Trigger, TriggerList>).trigger}
></ha-trigger-icon>`}
<h3 slot="header">
${type !== "list" && (this.trigger as Exclude<Trigger, TriggerList>).id
? html`<ha-trigger-id-chip
id="trigger-id-chip"
slot="leading-icon"
.triggerId=${(this.trigger as Exclude<Trigger, TriggerList>).id}
>
</ha-trigger-id-chip>`
: nothing}
${describeTrigger(this.trigger, this.hass, this._entityReg)}
${target !== undefined || (descriptionHasTarget && !this._isNew)
? this._renderTargets(
@@ -333,17 +321,6 @@ export default class HaAutomationTriggerRow extends LitElement {
)}
</ha-dropdown-item>`
: nothing}
${type !== "list"
? html`<ha-dropdown-item value="edit_id" .disabled=${this.disabled}>
<ha-svg-icon slot="icon" .path=${mdiIdentifier}></ha-svg-icon>
<div class="overflow-label">
${this.hass.localize(
`ui.panel.config.automation.editor.triggers.${"id" in this.trigger ? "edit" : "add"}_id`
)}
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
</div>
</ha-dropdown-item>`
: nothing}
<wa-divider></wa-divider>
<ha-dropdown-item value="duplicate" .disabled=${this.disabled}>
@@ -717,7 +694,6 @@ export default class HaAutomationTriggerRow extends LitElement {
this.focus();
}
},
editId: this._editTriggerId,
rename: () => {
this._renameTrigger();
},
@@ -829,34 +805,6 @@ export default class HaAutomationTriggerRow extends LitElement {
});
}
private _editTriggerId = () => {
if (isTriggerList(this.trigger)) {
return;
}
const trigger = this.trigger as Exclude<Trigger, TriggerList>;
showEditTriggerIdDialog(this, {
id: trigger.id,
onUpdate: (newId) => {
if (newId === (trigger.id ?? undefined)) {
return;
}
const value: Trigger = { ...trigger };
if (newId) {
value.id = newId;
} else {
delete value.id;
}
fireEvent(this, "value-changed", {
value,
});
if (this._selected && this.optionsInSidebar) {
this.openSidebar(value); // refresh sidebar
}
},
});
};
private _renameTrigger = async (): Promise<void> => {
if (isTriggerList(this.trigger)) return;
const alias = await showPromptDialog(this, {
@@ -1041,9 +989,6 @@ export default class HaAutomationTriggerRow extends LitElement {
case "edit_comment":
this._editCommentTrigger();
break;
case "edit_id":
this._editTriggerId();
break;
case "duplicate":
this._duplicateTrigger();
break;
@@ -8,7 +8,6 @@ import type { PropertyValues } from "lit";
import { html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
import { repeat } from "lit/directives/repeat";
import { ensureArray } from "../../../../common/array/ensure-array";
import { fireEvent } from "../../../../common/dom/fire_event";
import { stopPropagation } from "../../../../common/dom/stop_propagation";
import "../../../../components/ha-button";
@@ -25,12 +24,12 @@ import type { TriggerDescriptions } from "../../../../data/trigger";
import { isTriggerList, subscribeTriggers } from "../../../../data/trigger";
import { SubscribeMixin } from "../../../../mixins/subscribe-mixin";
import { EDITOR_SAVE_FAB_TOAST_BOTTOM_OFFSET } from "../editor-toast";
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
import {
getAddAutomationElementTargetFromQuery,
PASTE_VALUE,
showAddAutomationElementDialog,
} from "../show-add-automation-element-dialog";
import { AutomationSortableListMixin } from "../ha-automation-sortable-list-mixin";
import { automationRowsStyles } from "../styles";
import "./ha-automation-trigger-row";
import type HaAutomationTriggerRow from "./ha-automation-trigger-row";
@@ -68,38 +67,6 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
this.highlightedTriggers = items;
}
protected override pasteItem(ev: CustomEvent) {
if (this.root && ev.detail.item) {
const pasted = deepClone(ev.detail.item) as Trigger;
ev.detail.item = pasted;
}
super.pasteItem(ev);
}
protected override insertAfter(ev: CustomEvent) {
// Only dedupe when a single trigger is being inserted.
const incoming = ensureArray(ev.detail.value) as Trigger[];
if (this.root && incoming.length === 1) {
const trigger = deepClone(incoming[0]);
ev.detail.value = trigger;
}
super.insertAfter(ev);
}
protected override duplicateItem(ev: CustomEvent) {
if (this.root) {
const index = (ev.target as any).index;
const duplicated = deepClone(this.triggers[index]);
fireEvent(this, "value-changed", {
// @ts-expect-error Requires library bump to ES2023
value: this.triggers.toSpliced(index + 1, 0, duplicated),
});
ev.stopPropagation();
return;
}
super.duplicateItem(ev);
}
public disconnectedCallback() {
super.disconnectedCallback();
this._unsubscribe();
@@ -246,28 +213,23 @@ export default class HaAutomationTrigger extends AutomationSortableListMixin<Tri
private _addTrigger = (value: string, target?: HassServiceTarget) => {
let triggers: Trigger[];
if (value === PASTE_VALUE) {
const pasted = deepClone(this._clipboard!.trigger!);
triggers = this.triggers.concat(pasted);
triggers = this.triggers.concat(deepClone(this._clipboard!.trigger!));
} else if (isDynamic(value)) {
triggers = this.triggers.concat({
trigger: getValueFromDynamic(value),
target,
});
} else {
let newTrigger: Trigger;
if (isDynamic(value)) {
newTrigger = {
trigger: getValueFromDynamic(value),
target,
};
} else {
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
const elClass = customElements.get(
`ha-automation-trigger-${trigger}`
) as CustomElementConstructor & {
defaultConfig: Trigger;
};
newTrigger = {
...elClass.defaultConfig,
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
};
}
triggers = this.triggers.concat(newTrigger);
const trigger = value as Exclude<Trigger, TriggerList>["trigger"];
const elClass = customElements.get(
`ha-automation-trigger-${trigger}`
) as CustomElementConstructor & {
defaultConfig: Trigger;
};
triggers = this.triggers.concat({
...elClass.defaultConfig,
...(target?.entity_id ? { entity_id: target.entity_id } : {}),
});
}
this.focusLastItemOnChange = true;
fireEvent(this, "value-changed", { value: triggers });
@@ -1,22 +0,0 @@
import type { LitElement } from "lit";
import { fireEvent } from "../../../../common/dom/fire_event";
export const loadEditTriggerIdDialog = () =>
import("./ha-automation-edit-trigger-id-dialog");
export interface EditTriggerIdDialogParams {
id?: string;
onUpdate: (newId: string | undefined) => void;
}
export const showEditTriggerIdDialog = (
element: LitElement,
dialogParams: EditTriggerIdDialogParams
): void => {
fireEvent(element, "show-dialog", {
parentElement: element,
dialogTag: "ha-automation-edit-trigger-id-dialog",
dialogImport: loadEditTriggerIdDialog,
dialogParams,
});
};
@@ -1,6 +1,6 @@
import "@home-assistant/webawesome/dist/components/divider/divider";
import { consume } from "@lit/context";
import { mdiCog, mdiContentCopy } from "@mdi/js";
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
import type { PropertyValues } from "lit";
import { css, html, LitElement, nothing } from "lit";
import { customElement, property, state } from "lit/decorators";
@@ -13,10 +13,9 @@ import "../../../../../components/ha-dropdown-item";
import "../../../../../components/ha-icon-button";
import "../../../../../components/input/ha-input";
import type { HaInput } from "../../../../../components/input/ha-input";
import {
automationConfigContext,
type AutomationConfig,
type WebhookTrigger,
import type {
AutomationConfig,
WebhookTrigger,
} from "../../../../../data/automation";
import type { HomeAssistant } from "../../../../../types";
import { showEditorToast } from "../../editor-toast";
@@ -34,9 +33,9 @@ export class HaWebhookTrigger extends LitElement {
@property({ type: Boolean }) public disabled = false;
@consume({ context: automationConfigContext, subscribe: true })
@state()
private _config?: AutomationConfig;
@state() private _config?: AutomationConfig;
private _unsub?: UnsubscribeFunc;
public static get defaultConfig(): WebhookTrigger {
return {
@@ -47,6 +46,24 @@ export class HaWebhookTrigger extends LitElement {
};
}
connectedCallback() {
super.connectedCallback();
const details = {
callback: (config) => {
this._config = config;
},
};
fireEvent(this, "subscribe-automation-config", details);
this._unsub = (details as any).unsub;
}
disconnectedCallback() {
super.disconnectedCallback();
if (this._unsub) {
this._unsub();
}
}
private _generateWebhookId(): string {
// The webhook_id should be treated like a password. Generate a default
// value that would be hard for someone to guess. This generates a
@@ -2,16 +2,10 @@ import { css, html, LitElement, nothing } from "lit";
import type { CSSResultGroup, PropertyValues } from "lit";
import { consume, type ContextType } from "@lit/context";
import { customElement, state } from "lit/decorators";
import {
mdiPalette,
mdiPlayCircleOutline,
mdiPlaylistCheck,
mdiRobotOutline,
mdiScriptTextOutline,
} from "@mdi/js";
import { computeDeviceNameDisplay } from "../../../../common/entity/compute_device_name";
import { fireEvent } from "../../../../common/dom/fire_event";
import "../../../../components/ha-adaptive-dialog";
import "../../../../components/ha-icon";
import "../../../../components/ha-list";
import "../../../../components/ha-list-item";
import "../../../../components/ha-spinner";
@@ -38,10 +32,14 @@ import { showScriptEditor } from "../../../../data/script";
import type { SceneEntities } from "../../../../data/scene";
import { showSceneEditor } from "../../../../data/scene";
import {
ADD_TO_ACTION_ICONS,
addToActionHandler,
type AddToActionKey,
getAddToActionLabel,
getAddToActionLabelParts,
} from "../../../../dialogs/more-info/add-to";
import { haStyle, haStyleDialog } from "../../../../resources/styles";
import "../../automation/target/ha-automation-target-badge";
import type { DeviceAddToDialogParams } from "./show-dialog-device-add-to";
@customElement("dialog-device-add-to")
@@ -151,11 +149,7 @@ export class DialogDeviceAddTo extends LitElement {
if (!this._params) {
return nothing;
}
const deviceName = computeDeviceNameDisplay(
this._params.device,
this._i18n.localize,
this._states
);
const deviceName = this._deviceName;
return html`
<h3 class="section-header">
@@ -169,39 +163,51 @@ export class DialogDeviceAddTo extends LitElement {
data-type="automation_trigger"
@click=${this._handleNewAction}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${mdiRobotOutline}></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"automation_trigger",
deviceName
)}
>
<ha-icon
slot="graphic"
.icon=${ADD_TO_ACTION_ICONS.automation_trigger}
></ha-icon>
${this._renderActionLabel("automation_trigger")}
</ha-list-item>
<ha-list-item
graphic="icon"
data-type="automation_condition"
@click=${this._handleNewAction}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${mdiPlaylistCheck}></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"automation_condition",
deviceName
)}
>
<ha-icon
slot="graphic"
.icon=${ADD_TO_ACTION_ICONS.automation_condition}
></ha-icon>
${this._renderActionLabel("automation_condition")}
</ha-list-item>
<ha-list-item
graphic="icon"
data-type="automation_action"
@click=${this._handleNewAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_action",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"automation_action",
deviceName
)}
>
<ha-icon
slot="graphic"
.icon=${ADD_TO_ACTION_ICONS.automation_action}
></ha-icon>
${this._renderActionLabel("automation_action")}
</ha-list-item>
</ha-list>
<h3 class="section-header">
@@ -213,15 +219,17 @@ export class DialogDeviceAddTo extends LitElement {
data-type="script_action"
@click=${this._handleNewAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiScriptTextOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.script_action",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"script_action",
deviceName
)}
>
<ha-icon
slot="graphic"
.icon=${ADD_TO_ACTION_ICONS.script_action}
></ha-icon>
${this._renderActionLabel("script_action")}
</ha-list-item>
</ha-list>
${this._renderSceneSection(deviceName)}
@@ -242,11 +250,7 @@ export class DialogDeviceAddTo extends LitElement {
return nothing;
}
const deviceName = computeDeviceNameDisplay(
this._params.device,
this._i18n.localize,
this._states
);
const deviceName = this._deviceName;
const hasTriggers = Boolean(this._triggers?.length);
const hasConditions = Boolean(this._conditions?.length);
@@ -279,15 +283,17 @@ export class DialogDeviceAddTo extends LitElement {
data-type="trigger"
@click=${this._handleLegacyAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiRobotOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_trigger",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"automation_trigger",
deviceName
)}
>
<ha-icon
slot="graphic"
.icon=${ADD_TO_ACTION_ICONS.automation_trigger}
></ha-icon>
${this._renderActionLabel("automation_trigger")}
</ha-list-item>
`
: nothing}
@@ -298,15 +304,17 @@ export class DialogDeviceAddTo extends LitElement {
data-type="condition"
@click=${this._handleLegacyAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiPlaylistCheck}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_condition",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"automation_condition",
deviceName
)}
>
<ha-icon
slot="graphic"
.icon=${ADD_TO_ACTION_ICONS.automation_condition}
></ha-icon>
${this._renderActionLabel("automation_condition")}
</ha-list-item>
`
: nothing}
@@ -317,15 +325,17 @@ export class DialogDeviceAddTo extends LitElement {
data-type="automation_action"
@click=${this._handleLegacyAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiPlayCircleOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.automation_action",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"automation_action",
deviceName
)}
>
<ha-icon
slot="graphic"
.icon=${ADD_TO_ACTION_ICONS.automation_action}
></ha-icon>
${this._renderActionLabel("automation_action")}
</ha-list-item>
`
: nothing}
@@ -351,15 +361,17 @@ export class DialogDeviceAddTo extends LitElement {
data-type="script_action"
@click=${this._handleLegacyAction}
data-dialog="close"
>
<ha-svg-icon
slot="graphic"
.path=${mdiScriptTextOutline}
></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.script_action",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"script_action",
deviceName
)}
>
<ha-icon
slot="graphic"
.icon=${ADD_TO_ACTION_ICONS.script_action}
></ha-icon>
${this._renderActionLabel("script_action")}
</ha-list-item>
</ha-list>
`
@@ -390,17 +402,48 @@ export class DialogDeviceAddTo extends LitElement {
graphic="icon"
@click=${this._handleCreateScene}
data-dialog="close"
>
<ha-svg-icon slot="graphic" .path=${mdiPalette}></ha-svg-icon>
${this._i18n.localize(
"ui.dialogs.more_info_control.add_to.actions.scene",
{ target: deviceName }
aria-label=${getAddToActionLabel(
this._i18n.localize,
"scene",
deviceName
)}
>
<ha-icon slot="graphic" .icon=${ADD_TO_ACTION_ICONS.scene}></ha-icon>
${this._renderActionLabel("scene")}
</ha-list-item>
</ha-list>
`;
}
private _renderActionLabel(key: AddToActionKey) {
const [beforeTarget, afterTarget] = getAddToActionLabelParts(
this._i18n.localize,
key
);
return html`<span class="action-label">
${beforeTarget}${this._renderDeviceTarget()}${afterTarget}
</span>`;
}
private _renderDeviceTarget() {
return html`<ha-automation-target-badge
target-type="device"
.targetId=${this._params?.device.id}
.label=${this._deviceName}
></ha-automation-target-badge>`;
}
private get _deviceName(): string {
return this._params
? computeDeviceNameDisplay(
this._params.device,
this._i18n.localize,
this._states
)
: "";
}
private _handleNewAction(ev: Event) {
if (!this._params) {
return;
@@ -477,6 +520,13 @@ export class DialogDeviceAddTo extends LitElement {
font-weight: var(--ha-font-weight-medium);
color: var(--secondary-text-color);
}
.action-label {
display: inline-flex;
align-items: center;
gap: var(--ha-space-1);
flex-wrap: wrap;
}
`,
];
}
@@ -18,7 +18,6 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
import { computeDeviceNameDisplay } from "../../../common/entity/compute_device_name";
import { computeFloorName } from "../../../common/entity/compute_floor_name";
import { computeStateDomain } from "../../../common/entity/compute_state_domain";
import { getDeviceArea } from "../../../common/entity/context/get_device_context";
import {
PROTOCOL_INTEGRATIONS,
protocolIntegrationPicked,
@@ -59,7 +58,6 @@ import {
deserializeFilters,
serializeFilters,
} from "../../../data/data_table_filters";
import { computeDeviceAreaLabel } from "../../../data/device/device_picker";
import type {
DeviceEntityLookup,
DeviceRegistryEntry,
@@ -460,29 +458,17 @@ export class HaConfigDeviceDashboard extends LitElement {
.map((lbl) => labelReg!.find((label) => label.label_id === lbl))
.filter((entry): entry is LabelRegistryEntry => entry !== undefined);
const { areaName } = computeDeviceAreaLabel(
device,
this.hass.areas,
this.hass.devices,
this.hass.states,
this.hass.localize,
this.hass.language,
this.hass.translationMetadata,
device.via_device_id
? deviceEntityLookup[device.via_device_id]
: undefined
);
const floorArea =
getDeviceArea(device, areas) ??
(device.via_device_id && this.hass.devices[device.via_device_id]
? getDeviceArea(this.hass.devices[device.via_device_id], areas)
: undefined);
const floorId = floorArea?.floor_id;
const floorName =
floorId && this.hass.floors?.[floorId]
? computeFloorName(this.hass.floors[floorId])
: undefined;
let floorName;
if (
device.area_id &&
areas[device.area_id]?.floor_id &&
this.hass.floors
) {
const floorId = areas[device.area_id].floor_id;
if (this.hass.floors[floorId!]) {
floorName = computeFloorName(this.hass.floors[floorId!]);
}
}
return {
...device,
@@ -498,7 +484,10 @@ export class HaConfigDeviceDashboard extends LitElement {
manufacturer:
device.manufacturer ||
`<${localize("ui.panel.config.devices.data_table.unknown")}>`,
area: areaName,
area:
device.area_id && areas[device.area_id]
? areas[device.area_id].name
: undefined,
floor: floorName,
integration: deviceEntries.length
? deviceEntries
+2 -5
View File
@@ -5164,9 +5164,7 @@
},
"id": "Trigger ID",
"optional": "Optional",
"add_id": "Add trigger ID",
"edit_id": "Edit trigger ID",
"id_description": "Use trigger IDs in a Triggered by condition to run different actions depending on which trigger started the automation.",
"edit_id": "Edit ID",
"duplicate": "[%key:ui::common::duplicate%]",
"re_order": "Re-order",
"rename": "Rename",
@@ -5596,8 +5594,7 @@
"id": "Trigger",
"description": {
"picker": "Tests if the automation has been triggered by a specific trigger.",
"full": "If triggered by {id}",
"no_trigger": "No trigger selected"
"full": "If triggered by {id}"
}
},
"zone": {
+5 -5
View File
@@ -6666,10 +6666,10 @@ __metadata:
languageName: node
linkType: hard
"date-fns@npm:4.2.1":
version: 4.2.1
resolution: "date-fns@npm:4.2.1"
checksum: 10/c2731946a2d022935b3641062cd64bcdd62a65066115fee3490ba360a17fbf42134918493c43acb2c5c995b9ea4a2c7f505f0d9c02e73351cd2899b13e081729
"date-fns@npm:4.2.0":
version: 4.2.0
resolution: "date-fns@npm:4.2.0"
checksum: 10/6f50c3da98286a93807ad7c1b790ba753d6ea17d028cf0ffe39aa208693d940100a9069ec444f9436dc010de6a94242b8cb459e86e92e8d1948c553f4d141f71
languageName: node
linkType: hard
@@ -8635,7 +8635,7 @@ __metadata:
core-js: "npm:3.49.0"
cropperjs: "npm:1.6.2"
culori: "npm:4.0.2"
date-fns: "npm:4.2.1"
date-fns: "npm:4.2.0"
deep-clone-simple: "npm:1.1.1"
deep-freeze: "npm:0.0.1"
del: "npm:8.0.1"