Compare commits

..

7 Commits

Author SHA1 Message Date
Aidan Timson
7572257821 Match expose config dashboard for assistants columns (#28956) 2026-01-14 11:43:56 +01:00
Pegasus
4703cf802f Change border-quiet token values from 80 to 90 (#28976) 2026-01-14 09:28:32 +00:00
ildar170975
55c2315329 ha-label-picker: remove valueRenderer (#28975) 2026-01-14 10:15:39 +01:00
Wendelin
7d7e95ac55 Improve device automation UI (#28967)
* Improve device automation rows

* Improve device automation type picker

* Update src/panels/config/automation/condition/ha-automation-condition-row.ts

---------

Co-authored-by: Petar Petrov <MindFreeze@users.noreply.github.com>
2026-01-14 07:06:56 +00:00
ildar170975
6d7694caff ha-label-picker, ha-category-picker: fix icon for "no items available" (#28973)
* remove NO_LABELS

* remove NO_CATEGORIES

* reverted removed icon
2026-01-14 08:42:24 +02:00
calm
d7b6243698 Fix tree view heading overlapping Show more button (#28872) (#28968) 2026-01-13 18:34:39 +01:00
calm
73feef9e92 Remove box-shadow from automation dialog "Show more" button (#28945) (#28960) 2026-01-13 17:31:55 +01:00
15 changed files with 139 additions and 174 deletions

View File

@@ -1,8 +1,9 @@
import { consume } from "@lit/context";
import { css, html, LitElement, nothing } from "lit";
import { property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../common/dom/fire_event";
import { stopPropagation } from "../../common/dom/stop_propagation";
import { caseInsensitiveStringCompare } from "../../common/string/compare";
import { fullEntitiesContext } from "../../data/context";
import type { DeviceAutomation } from "../../data/device/device_automation";
import {
@@ -11,11 +12,12 @@ import {
} from "../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../data/entity/entity_registry";
import type { HomeAssistant } from "../../types";
import "../ha-generic-picker";
import "../ha-md-select";
import "../ha-md-select-option";
import type { PickerValueRenderer } from "../ha-picker-field";
const NO_AUTOMATION_KEY = "NO_AUTOMATION";
const UNKNOWN_AUTOMATION_KEY = "UNKNOWN_AUTOMATION";
export abstract class HaDeviceAutomationPicker<
T extends DeviceAutomation,
@@ -28,7 +30,7 @@ export abstract class HaDeviceAutomationPicker<
@property({ type: Object }) public value?: T;
@state() private _automations: T[] = [];
@state() private _automations?: T[];
// Trigger an empty render so we start with a clean DOM.
// paper-listbox does not like changing things around.
@@ -44,12 +46,6 @@ export abstract class HaDeviceAutomationPicker<
);
}
protected get UNKNOWN_AUTOMATION_TEXT() {
return this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
);
}
private _localizeDeviceAutomation: (
hass: HomeAssistant,
entityRegistry: EntityRegistryEntry[],
@@ -75,7 +71,7 @@ export abstract class HaDeviceAutomationPicker<
}
private get _value() {
if (!this.value) {
if (!this.value || !this._automations) {
return "";
}
@@ -88,7 +84,7 @@ export abstract class HaDeviceAutomationPicker<
);
if (idx === -1) {
return UNKNOWN_AUTOMATION_KEY;
return this.value.alias || this.value.type || "unknown";
}
return `${this._automations[idx].device_id}_${idx}`;
@@ -99,37 +95,21 @@ export abstract class HaDeviceAutomationPicker<
return nothing;
}
const value = this._value;
return html`
<ha-md-select
.label=${this.label}
.value=${value}
@change=${this._automationChanged}
@closed=${stopPropagation}
.disabled=${this._automations.length === 0}
>
${value === NO_AUTOMATION_KEY
? html`<ha-md-select-option .value=${NO_AUTOMATION_KEY}>
${this.NO_AUTOMATION_TEXT}
</ha-md-select-option>`
: nothing}
${value === UNKNOWN_AUTOMATION_KEY
? html`<ha-md-select-option .value=${UNKNOWN_AUTOMATION_KEY}>
${this.UNKNOWN_AUTOMATION_TEXT}
</ha-md-select-option>`
: nothing}
${this._automations.map(
(automation, idx) => html`
<ha-md-select-option .value=${`${automation.device_id}_${idx}`}>
${this._localizeDeviceAutomation(
this.hass,
this._entityReg,
automation
)}
</ha-md-select-option>
`
)}
</ha-md-select>
`;
return html`<ha-generic-picker
.hass=${this.hass}
.label=${this.label}
.value=${value}
.disabled=${!this._automations || this._automations.length === 0}
.getItems=${this._getItems(value, this._automations)}
@value-changed=${this._automationChanged}
.valueRenderer=${this._valueRenderer}
.unknownItemText=${this.hass.localize(
"ui.panel.config.devices.automation.actions.unknown_action"
)}
hide-clear-icon
>
</ha-generic-picker>`;
}
protected updated(changedProps) {
@@ -140,6 +120,57 @@ export abstract class HaDeviceAutomationPicker<
}
}
private _getItems = memoizeOne(
(value: string, automations: T[] | undefined) => {
if (!automations) {
return () => undefined;
}
const automationListItems = automations.map((automation, idx) => {
const primary = this._localizeDeviceAutomation(
this.hass,
this._entityReg,
automation
);
return {
id: `${automation.device_id}_${idx}`,
primary,
};
});
automationListItems.sort((a, b) =>
caseInsensitiveStringCompare(
a.primary,
b.primary,
this.hass.locale.language
)
);
if (value === NO_AUTOMATION_KEY) {
automationListItems.unshift({
id: NO_AUTOMATION_KEY,
primary: this.NO_AUTOMATION_TEXT,
});
}
return () => automationListItems;
}
);
private _valueRenderer: PickerValueRenderer = (value: string) => {
const automation = this._automations?.find(
(a, idx) => value === `${a.device_id}_${idx}`
);
const text = automation
? this._localizeDeviceAutomation(this.hass, this._entityReg, automation)
: value === NO_AUTOMATION_KEY
? this.NO_AUTOMATION_TEXT
: value;
return html`<span slot="headline">${text}</span>`;
};
private async _updateDeviceInfo() {
this._automations = this.deviceId
? (await this._fetchDeviceAutomations(this.hass, this.deviceId)).sort(
@@ -161,13 +192,14 @@ export abstract class HaDeviceAutomationPicker<
this._renderEmpty = false;
}
private _automationChanged(ev) {
const value = ev.target.value;
if (!value || [UNKNOWN_AUTOMATION_KEY, NO_AUTOMATION_KEY].includes(value)) {
private _automationChanged(ev: CustomEvent<{ value: string }>) {
ev.stopPropagation();
const value = ev.detail.value;
if (!value || NO_AUTOMATION_KEY === value) {
return;
}
const [deviceId, idx] = value.split("_");
const automation = this._automations[idx];
const automation = this._automations![idx];
if (automation.device_id !== deviceId) {
return;
}

View File

@@ -1,4 +1,4 @@
import { mdiLabel, mdiPlus } from "@mdi/js";
import { mdiPlus } from "@mdi/js";
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import type { TemplateResult } from "lit";
import { LitElement, html } from "lit";
@@ -25,11 +25,9 @@ import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import "./ha-generic-picker";
import type { HaGenericPicker } from "./ha-generic-picker";
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
import type { PickerValueRenderer } from "./ha-picker-field";
import "./ha-svg-icon";
const ADD_NEW_ID = "___ADD_NEW___";
const NO_LABELS = "___NO_LABELS___";
@customElement("ha-label-picker")
export class HaLabelPicker extends SubscribeMixin(LitElement) {
@@ -108,52 +106,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
];
}
private _labelMap = memoizeOne(
(
labels: LabelRegistryEntry[] | undefined
): Map<string, LabelRegistryEntry> => {
if (!labels) {
return new Map();
}
return new Map(labels.map((label) => [label.label_id, label]));
}
);
private _computeValueRenderer = memoizeOne(
(labels: LabelRegistryEntry[] | undefined): PickerValueRenderer =>
(value) => {
const label = this._labelMap(labels).get(value);
if (!label) {
return html`
<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>
<span slot="headline">${value}</span>
`;
}
return html`
${label.icon
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
: html`<ha-svg-icon slot="start" .path=${mdiLabel}></ha-svg-icon>`}
<span slot="headline">${label.name}</span>
`;
}
);
private _getLabelsMemoized = memoizeOne(getLabels);
private _getItems = () => {
if (!this._labels || this._labels.length === 0) {
return [
{
id: NO_LABELS,
primary: this.hass.localize("ui.components.label-picker.no_labels"),
icon_path: mdiLabel,
},
];
}
return this._getLabelsMemoized(
private _getItems = () =>
this._getLabelsMemoized(
this.hass.states,
this.hass.areas,
this.hass.devices,
@@ -166,7 +122,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
this.entityFilter,
this.excludeLabels
);
};
private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
if (!labels) {
@@ -219,8 +174,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
this.placeholder ??
this.hass.localize("ui.components.label-picker.label");
const valueRenderer = this._computeValueRenderer(this._labels);
return html`
<ha-generic-picker
.disabled=${this.disabled}
@@ -237,7 +190,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
.value=${this.value}
.getItems=${this._getItems}
.getAdditionalItems=${this._getAdditionalItems}
.valueRenderer=${valueRenderer}
.searchKeys=${labelComboBoxKeys}
@value-changed=${this._valueChanged}
>
@@ -251,10 +203,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
const value = ev.detail.value;
if (value === NO_LABELS) {
return;
}
if (!value) {
this._setValue(undefined);
return;

View File

@@ -58,6 +58,7 @@ import { fullEntitiesContext } from "../../../../data/context";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type {
Action,
DeviceAction,
NonConditionAction,
RepeatAction,
ServiceAction,
@@ -233,6 +234,13 @@ export default class HaAutomationActionRow extends LitElement {
private _renderRow() {
const type = getAutomationActionType(this.action);
const target =
type === "service" && "target" in this.action
? (this.action as ServiceAction).target
: type === "device_id" && (this.action as DeviceAction).device_id
? { device_id: (this.action as DeviceAction).device_id }
: undefined;
return html`
${type === "service" && "action" in this.action && this.action.action
? html`
@@ -254,9 +262,7 @@ export default class HaAutomationActionRow extends LitElement {
${capitalizeFirstLetter(
describeAction(this.hass, this._entityReg, this.action)
)}
${type === "service" && "target" in this.action
? this._renderTargets((this.action as ServiceAction).target)
: nothing}
${target ? this._renderTargets(target) : nothing}
</h3>
<slot name="icons" slot="icons"></slot>

View File

@@ -232,8 +232,6 @@ class DialogAddAutomationElement
private _configEntryLookup: Record<string, ConfigEntry> = {};
private _closing = false;
// #endregion variables
// #region lifecycle
@@ -349,8 +347,6 @@ class DialogAddAutomationElement
}
}
this._closing = true;
// if dialog is closed, but root level isn't active, clean up history state
if (mainWindow.history.state?.dialogData) {
this._open = false;
@@ -364,7 +360,6 @@ class DialogAddAutomationElement
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
this._open = true;
this._closing = false;
this._params = undefined;
this._selectedCollectionIndex = undefined;
this._selectedGroup = undefined;
@@ -1880,10 +1875,7 @@ class DialogAddAutomationElement
}
private _handleClosed() {
// if closing isn't already in progress, close the dialog
if (!this._closing) {
this.closeDialog();
}
this.closeDialog();
}
// #region interaction

View File

@@ -1504,14 +1504,7 @@ export default class HaAutomationAddFromTarget extends LitElement {
box-shadow: inset var(--ha-shadow-offset-x-lg)
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
var(--ha-shadow-spread-lg) var(--ha-color-shadow-light);
}
@media (prefers-color-scheme: dark) {
.targets-show-more {
box-shadow: inset var(--ha-shadow-offset-x-lg)
calc(var(--ha-shadow-offset-y-lg) * -1) var(--ha-shadow-blur-lg)
var(--ha-shadow-spread-lg) var(--ha-color-shadow-dark);
}
z-index: 2;
}
@media all and (max-width: 870px), all and (max-height: 500px) {

View File

@@ -76,6 +76,7 @@ import "./types/ha-automation-condition-template";
import "./types/ha-automation-condition-time";
import "./types/ha-automation-condition-trigger";
import "./types/ha-automation-condition-zone";
import type { DeviceCondition } from "../../../../data/device/device_automation";
export interface ConditionElement extends LitElement {
condition: Condition;
@@ -184,6 +185,14 @@ export default class HaAutomationConditionRow extends LitElement {
}
private _renderRow() {
const target =
"target" in (this.conditionDescriptions[this.condition.condition] || {})
? (this.condition as PlatformCondition).target
: "device_id" in this.condition &&
(this.condition as DeviceCondition).device_id
? { device_id: [(this.condition as DeviceCondition).device_id] }
: undefined;
return html`
<ha-condition-icon
slot="leading-icon"
@@ -194,10 +203,7 @@ 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}
${target ? this._renderTargets(target) : nothing}
</h3>
<slot name="icons" slot="icons"></slot>

View File

@@ -381,12 +381,12 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
},
voice_assistants: {
title: localize(
"ui.panel.config.automation.picker.headers.voice_assistants"
"ui.panel.config.voice_assistants.expose.headers.assistants"
),
type: "icon",
type: "flex",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
minWidth: "160px",
maxWidth: "160px",
template: (automation) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,

View File

@@ -56,6 +56,7 @@ import { isTrigger, subscribeTrigger } from "../../../../data/automation";
import { describeTrigger } from "../../../../data/automation_i18n";
import { validateConfig } from "../../../../data/config";
import { fullEntitiesContext } from "../../../../data/context";
import type { DeviceTrigger } from "../../../../data/device/device_automation";
import type { EntityRegistryEntry } from "../../../../data/entity/entity_registry";
import type { TriggerDescriptions } from "../../../../data/trigger";
import { isTriggerList } from "../../../../data/trigger";
@@ -196,6 +197,15 @@ export default class HaAutomationTriggerRow extends LitElement {
const yamlMode = this._yamlMode || !supported;
const target =
type === "platform" &&
"target" in
this.triggerDescriptions[(this.trigger as PlatformTrigger).trigger]
? (this.trigger as PlatformTrigger).target
: type === "device" && (this.trigger as DeviceTrigger).device_id
? { device_id: (this.trigger as DeviceTrigger).device_id }
: undefined;
return html`
${type === "list"
? html`<ha-svg-icon
@@ -210,11 +220,7 @@ export default class HaAutomationTriggerRow extends LitElement {
></ha-trigger-icon>`}
<h3 slot="header">
${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}
${target ? this._renderTargets(target) : nothing}
</h3>
<slot name="icons" slot="icons"></slot>

View File

@@ -20,7 +20,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../../types";
import { showCategoryRegistryDetailDialog } from "./show-dialog-category-registry-detail";
const ADD_NEW_ID = "___ADD_NEW___";
const NO_CATEGORIES_ID = "___NO_CATEGORIES___";
@customElement("ha-category-picker")
export class HaCategoryPicker extends SubscribeMixin(LitElement) {
@@ -108,18 +107,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
return undefined;
}
if (!categories || categories.length === 0) {
return [
{
id: NO_CATEGORIES_ID,
primary: this.hass.localize(
"ui.components.category-picker.no_categories"
),
icon_path: mdiTag,
},
];
}
const items = categories.map<PickerComboBoxItem>((category) => ({
id: category.category_id,
primary: category.name,
@@ -216,10 +203,6 @@ export class HaCategoryPicker extends SubscribeMixin(LitElement) {
const value = ev.detail.value;
if (value === NO_CATEGORIES_ID) {
return;
}
if (!value) {
this._setValue(undefined);
return;

View File

@@ -498,12 +498,12 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) {
},
voice_assistants: {
title: localize(
"ui.panel.config.entities.picker.headers.voice_assistants"
"ui.panel.config.voice_assistants.expose.headers.assistants"
),
type: "icon",
type: "flex",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
minWidth: "160px",
maxWidth: "160px",
template: (entry) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entities,

View File

@@ -487,12 +487,12 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) {
},
voice_assistants: {
title: localize(
"ui.panel.config.helpers.picker.headers.voice_assistants"
"ui.panel.config.voice_assistants.expose.headers.assistants"
),
type: "icon",
type: "flex",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
minWidth: "160px",
maxWidth: "160px",
template: (helper) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,

View File

@@ -415,12 +415,12 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) {
},
voice_assistants: {
title: localize(
"ui.panel.config.scene.picker.headers.voice_assistants"
"ui.panel.config.voice_assistants.expose.headers.assistants"
),
type: "icon",
type: "flex",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
minWidth: "160px",
maxWidth: "160px",
template: (scene) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,

View File

@@ -403,12 +403,12 @@ class HaScriptPicker extends SubscribeMixin(LitElement) {
},
voice_assistants: {
title: localize(
"ui.panel.config.script.picker.headers.voice_assistants"
"ui.panel.config.voice_assistants.expose.headers.assistants"
),
type: "icon",
type: "flex",
defaultHidden: true,
minWidth: "100px",
maxWidth: "100px",
minWidth: "160px",
maxWidth: "160px",
template: (script) => {
const exposedToVoiceAssistantIds = getEntityVoiceAssistantsIds(
this._entityReg,

View File

@@ -15,27 +15,27 @@ export const semanticColorStyles = css`
--ha-color-text-disabled: var(--ha-color-neutral-60);
--ha-color-text-link: var(--ha-color-primary-40);
/* border primary */
--ha-color-border-primary-quiet: var(--ha-color-primary-80);
--ha-color-border-primary-quiet: var(--ha-color-primary-90);
--ha-color-border-primary-normal: var(--ha-color-primary-70);
--ha-color-border-primary-loud: var(--ha-color-primary-40);
/* border neutral */
--ha-color-border-neutral-quiet: var(--ha-color-neutral-80);
--ha-color-border-neutral-quiet: var(--ha-color-neutral-90);
--ha-color-border-neutral-normal: var(--ha-color-neutral-60);
--ha-color-border-neutral-loud: var(--ha-color-neutral-40);
/* border danger */
--ha-color-border-danger-quiet: var(--ha-color-red-80);
--ha-color-border-danger-quiet: var(--ha-color-red-90);
--ha-color-border-danger-normal: var(--ha-color-red-70);
--ha-color-border-danger-loud: var(--ha-color-red-40);
/* border warning */
--ha-color-border-warning-quiet: var(--ha-color-orange-80);
--ha-color-border-warning-quiet: var(--ha-color-orange-90);
--ha-color-border-warning-normal: var(--ha-color-orange-70);
--ha-color-border-warning-loud: var(--ha-color-orange-40);
/* border success */
--ha-color-border-success-quiet: var(--ha-color-green-80);
--ha-color-border-success-quiet: var(--ha-color-green-90);
--ha-color-border-success-normal: var(--ha-color-green-70);
--ha-color-border-success-loud: var(--ha-color-green-40);

View File

@@ -3341,8 +3341,7 @@
"type": "Type",
"editable": "Editable",
"category": "Category",
"area": "Area",
"voice_assistants": "Voice assistants"
"area": "Area"
},
"create_helper": "Create helper",
"no_helpers": "Looks like you don't have any helpers yet!",