Compare commits

...

8 Commits

12 changed files with 1434 additions and 223 deletions

View File

@@ -154,7 +154,10 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
}
return this._getLabelsMemoized(
this.hass,
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._labels,
this.includeDomains,
this.excludeDomains,

View File

@@ -0,0 +1,28 @@
import { css, html, LitElement } from "lit";
import { customElement } from "lit/decorators";
@customElement("ha-section-title")
class HaSectionTitle extends LitElement {
protected render() {
return html`<slot></slot>`;
}
static styles = css`
:host {
display: block;
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
min-height: var(--ha-space-6);
display: flex;
align-items: center;
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-section-title": HaSectionTitle;
}
}

View File

@@ -858,7 +858,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
if (!filterType || filterType === "label") {
let labels = this._getLabelsMemoized(
this.hass,
this.hass.states,
this.hass.areas,
this.hass.devices,
this.hass.entities,
this._labelRegistry,
includeDomains,
undefined,

View File

@@ -24,11 +24,54 @@ export interface FloorComboBoxItem extends PickerComboBoxItem {
area?: AreaRegistryEntry;
}
export interface FloorNestedComboBoxItem extends PickerComboBoxItem {
floor?: FloorRegistryEntry;
areas: FloorComboBoxItem[];
}
export interface UnassignedAreasFloorComboBoxItem extends PickerComboBoxItem {
areas: FloorComboBoxItem[];
}
export interface AreaFloorValue {
id: string;
type: "floor" | "area";
}
export const getAreasNestedInFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[],
includeEmptyFloors = false
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors,
includeEmptyFloors,
true
) as (FloorNestedComboBoxItem | UnassignedAreasFloorComboBoxItem)[];
export const getAreasAndFloors = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
@@ -42,8 +85,47 @@ export const getAreasAndFloors = (
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[]
): FloorComboBoxItem[] => {
excludeFloors?: string[],
includeEmptyFloors = false
) =>
getAreasAndFloorsItems(
states,
haFloors,
haAreas,
haDevices,
haEntities,
formatId,
includeDomains,
excludeDomains,
includeDeviceClasses,
deviceFilter,
entityFilter,
excludeAreas,
excludeFloors,
includeEmptyFloors
) as FloorComboBoxItem[];
const getAreasAndFloorsItems = (
states: HomeAssistant["states"],
haFloors: HomeAssistant["floors"],
haAreas: HomeAssistant["areas"],
haDevices: HomeAssistant["devices"],
haEntities: HomeAssistant["entities"],
formatId: (value: AreaFloorValue) => string,
includeDomains?: string[],
excludeDomains?: string[],
includeDeviceClasses?: string[],
deviceFilter?: HaDevicePickerDeviceFilterFunc,
entityFilter?: HaEntityPickerEntityFilterFunc,
excludeAreas?: string[],
excludeFloors?: string[],
includeEmptyFloors = false,
nested = false
): (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] => {
const floors = Object.values(haFloors);
const areas = Object.values(haAreas);
const devices = Object.values(haDevices);
@@ -189,6 +271,14 @@ export const getAreasAndFloors = (
const compare = floorCompare(haFloors);
if (includeEmptyFloors) {
Object.values(haFloors).forEach((floor) => {
if (!floorAreaLookup[floor.floor_id]) {
floorAreaLookup[floor.floor_id] = [];
}
});
}
// @ts-ignore
const floorAreaEntries: [
FloorRegistryEntry | undefined,
@@ -200,9 +290,15 @@ export const getAreasAndFloors = (
})
.sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
const items: FloorComboBoxItem[] = [];
const items: (
| FloorComboBoxItem
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] = [];
floorAreaEntries.forEach(([floor, floorAreas]) => {
let floorItem: FloorComboBoxItem | FloorNestedComboBoxItem;
if (floor) {
const floorName = computeFloorName(floor);
@@ -213,7 +309,7 @@ export const getAreasAndFloors = (
})
.flat();
items.push({
floorItem = {
id: formatId({ id: floor.floor_id, type: "floor" }),
type: "floor",
primary: floorName,
@@ -225,25 +321,9 @@ export const getAreasAndFloors = (
...floor.aliases,
...areaSearchLabels,
],
});
};
}
items.push(
...floorAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
})
);
});
items.push(
...unassignedAreas.map((area) => {
const floorAreasItems = floorAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
@@ -253,8 +333,38 @@ export const getAreasAndFloors = (
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
})
);
});
if (floor) {
items.push(floorItem!);
}
if (nested && floor) {
(floorItem! as FloorNestedComboBoxItem).areas = floorAreasItems;
} else {
items.push(...floorAreasItems);
}
});
const unassignedAreaItems = unassignedAreas.map((area) => {
const areaName = computeAreaName(area) || area.area_id;
return {
id: formatId({ id: area.area_id, type: "area" }),
type: "area" as const,
primary: areaName,
area: area,
icon: area.icon || undefined,
search_labels: [area.area_id, areaName, ...area.aliases],
};
});
if (nested && unassignedAreaItems.length) {
items.push({
areas: unassignedAreaItems,
} as UnassignedAreasFloorComboBoxItem);
} else {
items.push(...unassignedAreaItems);
}
return items;
};

View File

@@ -60,11 +60,12 @@ export const deleteAreaRegistryEntry = (hass: HomeAssistant, areaId: string) =>
});
export const getAreaEntityLookup = (
entities: EntityRegistryEntry[]
entities: EntityRegistryEntry[],
filterHidden = false
): AreaEntityLookup => {
const areaEntityLookup: AreaEntityLookup = {};
for (const entity of entities) {
if (!entity.area_id) {
if (!entity.area_id || (filterHidden && entity.hidden_by)) {
continue;
}
if (!(entity.area_id in areaEntityLookup)) {

View File

@@ -107,11 +107,12 @@ export const sortDeviceRegistryByName = (
);
export const getDeviceEntityLookup = (
entities: EntityRegistryEntry[]
entities: EntityRegistryEntry[],
filterHidden = false
): DeviceEntityLookup => {
const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) {
if (!entity.device_id || (filterHidden && entity.hidden_by)) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {

View File

@@ -101,7 +101,10 @@ export const deleteLabelRegistryEntry = (
});
export const getLabels = (
hass: HomeAssistant,
hassStates: HomeAssistant["states"],
hassAreas: HomeAssistant["areas"],
hassDevices: HomeAssistant["devices"],
hassEntities: HomeAssistant["entities"],
labels?: LabelRegistryEntry[],
includeDomains?: string[],
excludeDomains?: string[],
@@ -115,8 +118,8 @@ export const getLabels = (
return [];
}
const devices = Object.values(hass.devices);
const entities = Object.values(hass.entities);
const devices = Object.values(hassDevices);
const entities = Object.values(hassEntities);
let deviceEntityLookup: DeviceEntityDisplayLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
@@ -170,7 +173,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hass.states[entity.entity_id];
const stateObj = hassStates[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -181,7 +184,7 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hass.states[entity.entity_id];
const stateObj = hassStates[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
@@ -200,7 +203,7 @@ export const getLabels = (
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = hass.states[entity.entity_id];
const stateObj = hassStates[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -208,7 +211,7 @@ export const getLabels = (
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = hass.states[entity.entity_id];
const stateObj = hassStates[entity.entity_id];
if (!stateObj) {
return false;
}
@@ -245,7 +248,7 @@ export const getLabels = (
if (areaIds) {
areaIds.forEach((areaId) => {
const area = hass.areas[areaId];
const area = hassAreas[areaId];
area.labels.forEach((label) => usedLabels.add(label));
});
}

View File

@@ -1,3 +1,4 @@
import { consume } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiClose,
@@ -5,6 +6,7 @@ import {
mdiPlus,
} from "@mdi/js";
import Fuse from "fuse.js";
import type { SingleHassServiceTarget } from "home-assistant-js-websocket";
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
import { LitElement, css, html, nothing } from "lit";
import {
@@ -20,12 +22,17 @@ import { repeat } from "lit/directives/repeat";
import memoizeOne from "memoize-one";
import { tinykeys } from "tinykeys";
import { fireEvent } from "../../../common/dom/fire_event";
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";
import { stringCompare } from "../../../common/string/compare";
import type {
LocalizeFunc,
LocalizeKeys,
} from "../../../common/translations/localize";
import { computeRTL } from "../../../common/util/compute_rtl";
import { debounce } from "../../../common/util/debounce";
import { deepEqual } from "../../../common/util/deep-equal";
import "../../../components/ha-bottom-sheet";
@@ -39,6 +46,7 @@ import "../../../components/ha-md-divider";
import "../../../components/ha-md-list";
import type { HaMdList } from "../../../components/ha-md-list";
import "../../../components/ha-md-list-item";
import "../../../components/ha-section-title";
import "../../../components/ha-service-icon";
import "../../../components/ha-wa-dialog";
import "../../../components/search-input";
@@ -50,6 +58,10 @@ import {
getService,
isService,
} from "../../../data/action";
import {
getAreaDeviceLookup,
getAreaEntityLookup,
} from "../../../data/area_registry";
import type {
AutomationElementGroup,
AutomationElementGroupCollection,
@@ -59,6 +71,10 @@ import {
CONDITION_COLLECTIONS,
CONDITION_ICONS,
} from "../../../data/condition";
import { fullEntitiesContext } from "../../../data/context";
import { getDeviceEntityLookup } from "../../../data/device_registry";
import type { EntityRegistryEntry } from "../../../data/entity_registry";
import { getFloorAreaLookup } from "../../../data/floor_registry";
import { getServiceIcons } from "../../../data/icons";
import type { IntegrationManifest } from "../../../data/integration";
import {
@@ -72,6 +88,8 @@ import { HaFuse } from "../../../resources/fuse";
import type { HomeAssistant } from "../../../types";
import { isMac } from "../../../util/is_mac";
import { showToast } from "../../../util/toast";
import "./add-automation-element/ha-automation-add-from-target";
import type HaAutomationAddFromTarget from "./add-automation-element/ha-automation-add-from-target";
import type { AddAutomationElementDialogParams } from "./show-add-automation-element-dialog";
import { PASTE_VALUE } from "./show-add-automation-element-dialog";
@@ -126,7 +144,9 @@ class DialogAddAutomationElement
@state() private _selectedGroup?: string;
@state() private _tab: "groups" | "blocks" = "groups";
@state() private _selectedTarget?: SingleHassServiceTarget;
@state() private _tab: "targets" | "groups" | "blocks" = "targets";
@state() private _filter = "";
@@ -142,12 +162,22 @@ class DialogAddAutomationElement
@state() private _narrow = false;
@state()
@consume({ context: fullEntitiesContext, subscribe: true })
private fullEntities!: EntityRegistryEntry[];
@query(".items ha-md-list ha-md-list-item")
private _itemsListFirstElement?: HaMdList;
@query("ha-automation-add-from-target")
private _targetPickerElement?: HaAutomationAddFromTarget;
@query(".items")
private _itemsListElement?: HTMLDivElement;
@query(".content")
private _contentElement?: HTMLDivElement;
private _fullScreen = false;
private _removeKeyboardShortcuts?: () => void;
@@ -184,7 +214,8 @@ class DialogAddAutomationElement
this._bottomSheetMode = false;
this._params = undefined;
this._selectedGroup = undefined;
this._tab = "groups";
this._tab = "targets";
this._selectedTarget = undefined;
this._selectedCollectionIndex = undefined;
this._filter = "";
this._manifests = undefined;
@@ -631,27 +662,28 @@ class DialogAddAutomationElement
private _renderContent() {
const automationElementType = this._params!.type;
const items = this._filter
? this._getFilteredItems(
automationElementType,
this._filter,
this.hass.localize,
this.hass.services,
this._manifests
)
: this._tab === "blocks"
? this._getBlockItems(automationElementType, this.hass.localize)
: this._selectedGroup
? this._getGroupItems(
automationElementType,
this._selectedGroup,
this._selectedCollectionIndex ?? 0,
this._domains,
this.hass.localize,
this.hass.services,
this._manifests
)
: undefined;
const items =
this._filter || this._selectedTarget
? this._getFilteredItems(
automationElementType,
this._filter || "Ti",
this.hass.localize,
this.hass.services,
this._manifests
)
: this._tab === "blocks"
? this._getBlockItems(automationElementType, this.hass.localize)
: this._selectedGroup
? this._getGroupItems(
automationElementType,
this._selectedGroup,
this._selectedCollectionIndex ?? 0,
this._domains,
this.hass.localize,
this.hass.services,
this._manifests
)
: undefined;
const filteredBlockItems =
this._filter && automationElementType !== "trigger"
@@ -671,35 +703,25 @@ class DialogAddAutomationElement
this._manifests
);
const groupName = isService(this._selectedGroup)
? domainToName(
this.hass.localize,
getService(this._selectedGroup!),
this._manifests?.[getService(this._selectedGroup!)]
)
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
) ||
this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
);
const typeTitle = this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.add`
);
const tabButtons = [
{
label: this.hass.localize(`ui.panel.config.automation.editor.targets`),
value: "targets",
},
{
label: this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.name`
),
value: "groups",
},
{
];
if (this._params?.type !== "trigger") {
tabButtons.push({
label: this.hass.localize(`ui.panel.config.automation.editor.blocks`),
value: "blocks",
},
];
});
}
const hideCollections =
this._filter ||
@@ -708,28 +730,8 @@ class DialogAddAutomationElement
return html`
<div slot="header">
<ha-dialog-header subtitle-position="above">
<span slot="title"
>${this._narrow && this._selectedGroup
? groupName
: typeTitle}</span
>
${this._narrow && this._selectedGroup
? html`<span slot="subtitle">${typeTitle}</span>`
: nothing}
${this._narrow && this._selectedGroup
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
></ha-icon-button-prev>`
: html`<ha-icon-button
.path=${mdiClose}
@click=${this._close}
slot="navigationIcon"
></ha-icon-button>`}
</ha-dialog-header>
${!this._narrow || !this._selectedGroup
${this._renderHeader()}
${!this._narrow || (!this._selectedGroup && !this._selectedTarget)
? html`
<search-input
?autofocus=${!this._narrow}
@@ -742,9 +744,8 @@ class DialogAddAutomationElement
></search-input>
`
: nothing}
${this._params?.type !== "trigger" &&
!this._filter &&
(!this._narrow || !this._selectedGroup)
${!this._filter &&
(!this._narrow || (!this._selectedGroup && !this._selectedTarget))
? html`<ha-button-toggle-group
variant="neutral"
active-variant="brand"
@@ -756,110 +757,138 @@ class DialogAddAutomationElement
></ha-button-toggle-group>`
: nothing}
</div>
<div class="content">
<ha-md-list
class=${classMap({
groups: true,
hidden: hideCollections,
})}
>
${this._params!.clipboardItem && !this._filter
? html`<ha-md-list-item
interactive
type="button"
class="paste"
.value=${PASTE_VALUE}
@click=${this._selected}
>
<div class="shortcut-label">
<div class="label">
<div>
${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.paste`
)}
</div>
<div class="supporting-text">
${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
)}
</div>
</div>
${!this._narrow
? html`<span class="shortcut">
<span
>${isMac
<div
class=${classMap({
content: true,
column: this._narrow && this._selectedTarget,
})}
>
${this._tab === "targets"
? html`<ha-automation-add-from-target
.hass=${this.hass}
.value=${this._selectedTarget}
@value-changed=${this._handleTargetSelected}
.narrow=${this._narrow}
class=${this._getAddFromTargetHidden()}
></ha-automation-add-from-target>`
: html`
<ha-md-list
class=${classMap({
groups: true,
hidden: hideCollections,
})}
>
${this._params!.clipboardItem && !this._filter
? html`<ha-md-list-item
interactive
type="button"
class="paste"
.value=${PASTE_VALUE}
@click=${this._selected}
>
<div class="shortcut-label">
<div class="label">
<div>
${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.paste`
)}
</div>
<div class="supporting-text">
${this.hass.localize(
// @ts-ignore
`ui.panel.config.automation.editor.${automationElementType}s.type.${this._params.clipboardItem}.label`
)}
</div>
</div>
${!this._narrow
? html`<span class="shortcut">
<span
>${isMac
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>V</span>
</span>`
: nothing}
</div>
<ha-svg-icon
slot="start"
.path=${mdiContentPaste}
></ha-svg-icon
><ha-svg-icon
class="plus"
slot="end"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
<ha-md-divider
role="separator"
tabindex="-1"
></ha-md-divider>`
: nothing}
${collections.map(
(collection, index) => html`
${collection.titleKey
? html`<ha-section-title>
${this.hass.localize(collection.titleKey)}
</ha-section-title>`
: nothing}
${repeat(
collection.groups,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
.value=${item.key}
.index=${index}
@click=${this._groupSelected}
class=${item.key === this._selectedGroup
? "selected"
: ""}
>
<div slot="headline">${item.name}</div>
${item.icon
? html`<span slot="start">${item.icon}</span>`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${mdiAppleKeyboardCommand}
.path=${item.iconPath}
></ha-svg-icon>`
: this.hass.localize(
"ui.panel.config.automation.editor.ctrl"
)}</span
>
<span>+</span>
<span>V</span>
</span>`
: nothing}
</div>
<ha-svg-icon
slot="start"
.path=${mdiContentPaste}
></ha-svg-icon
><ha-svg-icon
class="plus"
slot="end"
.path=${mdiPlus}
></ha-svg-icon>
</ha-md-list-item>
<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
: nothing}
${collections.map(
(collection, index) => html`
${collection.titleKey
? html`<div class="collection-title">
${this.hass.localize(collection.titleKey)}
</div>`
: nothing}
${repeat(
collection.groups,
(item) => item.key,
(item) => html`
<ha-md-list-item
interactive
type="button"
.value=${item.key}
.index=${index}
@click=${this._groupSelected}
class=${item.key === this._selectedGroup ? "selected" : ""}
>
<div slot="headline">${item.name}</div>
${item.icon
? html`<span slot="start">${item.icon}</span>`
: item.iconPath
? html`<ha-svg-icon
slot="start"
.path=${item.iconPath}
></ha-svg-icon>`
: nothing}
</ha-md-list-item>
`
)}
`
)}
</ha-md-list>
: nothing}
</ha-md-list-item>
`
)}
`
)}
</ha-md-list>
`}
<div
class=${classMap({
items: true,
blank:
!this._selectedGroup && !this._filter && this._tab === "groups",
(this._tab === "groups" &&
!this._selectedGroup &&
!this._filter) ||
(this._tab === "targets" &&
!this._selectedTarget &&
!this._filter),
"empty-search":
!items?.length && !filteredBlockItems?.length && this._filter,
hidden:
this._narrow &&
!this._selectedGroup &&
(!this._selectedTarget ||
(this._selectedTarget &&
!Object.values(this._selectedTarget)[0])) &&
!this._filter &&
this._tab === "groups",
this._tab !== "blocks",
})}
@scroll=${this._onItemsScroll}
>
@@ -873,23 +902,27 @@ class DialogAddAutomationElement
? this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.select`
)
: !items?.length &&
this._filter &&
(!filteredBlockItems || !filteredBlockItems.length)
? html`<span
>${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.empty_search`,
{
term: html`<b>${this._filter}</b>`,
}
)}</span
>`
: this._renderItemList(
this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.name`
),
items
)}
: this._tab === "targets" && !this._selectedTarget && !this._filter
? this.hass.localize(
`ui.panel.config.automation.editor.select_target`
)
: !items?.length &&
this._filter &&
(!filteredBlockItems || !filteredBlockItems.length)
? html`<span
>${this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.empty_search`,
{
term: html`<b>${this._filter}</b>`,
}
)}</span
>`
: this._renderItemList(
this.hass.localize(
`ui.panel.config.automation.editor.${automationElementType}s.name`
),
items
)}
</div>
</div>
`;
@@ -985,6 +1018,10 @@ class DialogAddAutomationElement
}
private _back() {
if (this._selectedTarget) {
this._targetPickerElement?.navigateBack();
return;
}
this._selectedGroup = undefined;
}
@@ -1008,6 +1045,21 @@ class DialogAddAutomationElement
this.closeDialog();
}
private _handleTargetSelected = (
ev: CustomEvent<{ value: SingleHassServiceTarget }>
) => {
this._selectedTarget = ev.detail.value;
// TODO fix on mobile
requestAnimationFrame(() => {
if (this._narrow) {
this._contentElement?.scrollTo(0, 0);
} else {
this._itemsListElement?.scrollTo(0, 0);
}
});
};
private _debounceFilterChanged = debounce(
(ev) => this._filterChanged(ev),
200
@@ -1104,6 +1156,183 @@ class DialogAddAutomationElement
this.closeDialog();
};
private _renderHeader() {
return html`
<ha-dialog-header subtitle-position="above">
<span slot="title">${this._getDialogTitle()}</span>
${this._renderDialogSubtitle()}
${this._narrow && (this._selectedGroup || this._selectedTarget)
? html`<ha-icon-button-prev
slot="navigationIcon"
@click=${this._back}
></ha-icon-button-prev>`
: html`<ha-icon-button
.path=${mdiClose}
@click=${this._close}
slot="navigationIcon"
></ha-icon-button>`}
</ha-dialog-header>
`;
}
private _getDialogTitle() {
if (this._narrow && this._selectedGroup) {
return isService(this._selectedGroup)
? domainToName(
this.hass.localize,
getService(this._selectedGroup!),
this._manifests?.[getService(this._selectedGroup!)]
)
: this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.groups.${this._selectedGroup}.label` as LocalizeKeys
) ||
this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.type.${this._selectedGroup}.label` as LocalizeKeys
);
}
if (this._narrow && this._selectedTarget) {
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
this._selectedTarget
);
if (targetId === undefined && targetType === "floor") {
return this.hass.localize("ui.components.area-picker.unassigned_areas");
}
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;
}
}
}
return this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.add`
);
}
private _renderDialogSubtitle() {
if (!this._narrow) {
return nothing;
}
if (this._selectedGroup) {
return html`<span slot="subtitle"
>${this.hass.localize(
`ui.panel.config.automation.editor.${this._params!.type}s.add`
)}</span
>`;
}
if (this._selectedTarget) {
let subtitle: string | undefined;
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
this._selectedTarget
);
if (targetId) {
if (targetType === "area" && this.hass.areas[targetId].floor_id) {
const floorId = this.hass.areas[targetId].floor_id;
subtitle = computeFloorName(this.hass.floors[floorId]) || floorId;
}
if (targetType === "device" && this.hass.devices[targetId].area_id) {
const areaId = this.hass.devices[targetId].area_id;
subtitle = computeAreaName(this.hass.areas[areaId]) || areaId;
}
if (targetType === "entity" && this.hass.states[targetId]) {
const stateObj = this.hass.states[targetId];
const [entityName, deviceName, areaName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.hass.entities,
this.hass.devices,
this.hass.areas,
this.hass.floors
);
subtitle = [areaName, entityName ? deviceName : undefined]
.filter(Boolean)
.join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
}
}
if (subtitle) {
return html`<span slot="subtitle">${subtitle}</span>`;
}
}
return nothing;
}
private _getFloorAreaLookupMemoized = memoizeOne(
(areas: HomeAssistant["areas"]) => getFloorAreaLookup(Object.values(areas))
);
private _getAreaDeviceLookupMemoized = memoizeOne(
(devices: HomeAssistant["devices"]) =>
getAreaDeviceLookup(Object.values(devices))
);
private _getAreaEntityLookupMemoized = memoizeOne(getAreaEntityLookup);
private _getDeviceEntityLookupMemoized = memoizeOne(getDeviceEntityLookup);
private _extractTypeAndIdFromTarget = memoizeOne(
(target: SingleHassServiceTarget): [string, string | undefined] => {
const [targetTypeId, targetId] = Object.entries(target)[0];
const targetType = targetTypeId.replace("_id", "");
return [targetType, targetId];
}
);
private _getAddFromTargetHidden() {
if (this._narrow && this._selectedTarget) {
const [targetType, targetId] = this._extractTypeAndIdFromTarget(
this._selectedTarget
);
if (
targetId &&
((targetType === "floor" &&
this._getFloorAreaLookupMemoized(this.hass.areas)[targetId]
?.length === 0) ||
(targetType === "area" &&
this._getAreaDeviceLookupMemoized(this.hass.devices)[targetId]
?.length === 0 &&
this._getAreaEntityLookupMemoized(this.fullEntities, true)[targetId]
?.length === 0) ||
(targetType === "device" &&
this._getDeviceEntityLookupMemoized(this.fullEntities, true)[
targetId
]?.length === 0))
) {
return "hidden";
}
}
return "";
}
static get styles(): CSSResultGroup {
return [
css`
@@ -1157,10 +1386,15 @@ class DialogAddAutomationElement
display: flex;
}
.content.column {
flex-direction: column;
}
ha-md-list {
padding: 0;
}
ha-automation-add-from-target,
.groups {
overflow: auto;
flex: 3;
@@ -1168,6 +1402,13 @@ class DialogAddAutomationElement
border: 1px solid var(--ha-color-border-neutral-quiet);
margin: var(--ha-space-3);
margin-inline-end: var(--ha-space-0);
}
ha-automation-add-from-target.hidden {
display: none;
}
.groups {
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-1);
@@ -1175,7 +1416,8 @@ class DialogAddAutomationElement
--md-list-item-supporting-text-font: var(--ha-font-size-s);
--md-list-item-one-line-container-height: var(--ha-space-10);
}
ha-bottom-sheet .groups {
ha-bottom-sheet .groups,
ha-bottom-sheet ha-automation-add-from-target {
margin: var(--ha-space-3);
}
.groups .selected {
@@ -1187,16 +1429,9 @@ class DialogAddAutomationElement
color: var(--ha-color-on-primary-normal);
}
.collection-title {
background-color: var(--ha-color-fill-neutral-quiet-resting);
padding: var(--ha-space-1) var(--ha-space-2);
font-weight: var(--ha-font-weight-bold);
color: var(--secondary-text-color);
ha-section-title {
top: 0;
position: sticky;
min-height: var(--ha-space-6);
display: flex;
align-items: center;
z-index: 1;
}
@@ -1207,6 +1442,11 @@ class DialogAddAutomationElement
flex: 7;
}
.content.column ha-automation-add-from-target,
.content.column .items {
flex: none;
}
ha-wa-dialog .items {
margin-top: var(--ha-space-3);
}

View File

@@ -0,0 +1,810 @@
import "@home-assistant/webawesome/dist/components/tree-item/tree-item";
import "@home-assistant/webawesome/dist/components/tree/tree";
import type { WaSelectionChangeEvent } from "@home-assistant/webawesome/dist/events/selection-change";
import { consume } from "@lit/context";
import { mdiSelectionMarker, mdiTextureBox } from "@mdi/js";
import type { SingleHassServiceTarget } from "home-assistant-js-websocket";
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
import { customElement, property, state } from "lit/decorators";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../common/dom/fire_event";
import { computeDeviceName } from "../../../../common/entity/compute_device_name";
import { computeEntityNameList } from "../../../../common/entity/compute_entity_name_display";
import "../../../../components/entity/state-badge";
import "../../../../components/ha-floor-icon";
import "../../../../components/ha-icon";
import "../../../../components/ha-icon-next";
import "../../../../components/ha-md-list";
import "../../../../components/ha-md-list-item";
import "../../../../components/ha-section-title";
import "../../../../components/ha-svg-icon";
import {
getAreasNestedInFloors,
type AreaFloorValue,
type FloorComboBoxItem,
type FloorNestedComboBoxItem,
type UnassignedAreasFloorComboBoxItem,
} from "../../../../data/area_floor";
import {
getConfigEntries,
type ConfigEntry,
} from "../../../../data/config_entries";
import {
areasContext,
devicesContext,
entitiesContext,
floorsContext,
labelsContext,
localizeContext,
statesContext,
} from "../../../../data/context";
import {
getLabels,
type LabelRegistryEntry,
} from "../../../../data/label_registry";
import { extractFromTarget } from "../../../../data/target";
import type { HomeAssistant } from "../../../../types";
import { brandsUrl } from "../../../../util/brands-url";
const SEPARATOR = "________";
interface DeviceEntries {
open: boolean;
entities: string[];
}
interface AreaEntries {
open: boolean;
devices: Record<string, DeviceEntries>;
entities: string[];
}
@customElement("ha-automation-add-from-target")
export default class HaAutomationAddFromTarget extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public value?: SingleHassServiceTarget;
@property({ type: Boolean }) public narrow = false;
// #region context
@state()
@consume({ context: localizeContext, subscribe: true })
private localize!: HomeAssistant["localize"];
@state()
@consume({ context: statesContext, subscribe: true })
private states!: HomeAssistant["states"];
@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: entitiesContext, subscribe: true })
private entities!: HomeAssistant["entities"];
@state()
@consume({ context: labelsContext, subscribe: true })
private _labelRegistry!: LabelRegistryEntry[];
// #endregion context
@state()
private _floorAreas: (
| FloorNestedComboBoxItem
| UnassignedAreasFloorComboBoxItem
)[] = [];
@state()
private _areaEntries: Record<string, AreaEntries> = {};
private _getLabelsMemoized = memoizeOne(getLabels);
private _configEntryLookup: Record<string, ConfigEntry> = {};
public willUpdate(changedProps: PropertyValues) {
super.willUpdate(changedProps);
if (!this.hasUpdated) {
this._loadConfigEntries();
this._getTreeData();
}
}
protected render() {
return html`
${!this.narrow || !this.value ? this._renderFloors() : nothing}
${this.narrow && this.value ? this._renderNarrow(this.value) : nothing}
${!this.narrow || !this.value ? this._renderLabels() : nothing}
`;
}
private _renderNarrow(value: SingleHassServiceTarget) {
const [valueTypeId, valueId] = Object.entries(value)[0];
const valueType = valueTypeId.replace("_id", "");
if (!valueType || valueType === "label") {
return nothing;
}
if (valueType === "floor") {
return this._renderAreas(
this._floorAreas.find(
(floor) =>
(valueId && floor.id === `${valueType}${SEPARATOR}${valueId}`) ||
(!valueId && !floor.id)
)?.areas
);
}
if (valueType === "area") {
const { devices, entities } =
this._areaEntries[`area${SEPARATOR}${valueId}`];
const numberOfDevices = Object.keys(devices).length;
return html`
${numberOfDevices ? this._renderDevices(devices) : nothing}
${entities.length ? this._renderEntities(entities) : nothing}
`;
}
if (valueType === "device" && this.devices[valueId]) {
const deviceArea = this.devices[valueId].area_id!;
return this._renderEntities(
this._areaEntries[`area${SEPARATOR}${deviceArea}`]?.devices[valueId]
?.entities
);
}
return nothing;
}
private _renderFloors() {
return html`<ha-section-title
>${this.localize(
"ui.panel.config.automation.editor.home"
)}</ha-section-title
>
${!this._floorAreas.length ||
(!this._floorAreas[0].id && !this._floorAreas[0].areas.length)
? html`<ha-md-list>
<ha-md-list-item type="text">
<div slot="headline">
${this.localize("ui.components.area-picker.no_areas")}
</div>
</ha-md-list-item>
</ha-md-list>`
: this.narrow
? html`<ha-md-list>
${this._floorAreas.map((floor, index) =>
index === 0 && !floor.id
? this._renderAreas(floor.areas)
: html`<ha-md-list-item
interactive
type="button"
.target=${floor.id || `floor${SEPARATOR}`}
@click=${this._selectItem}
>
${floor.id && (floor as FloorNestedComboBoxItem).floor
? html`<ha-floor-icon
slot="start"
.floor=${(floor as FloorNestedComboBoxItem).floor}
></ha-floor-icon>`
: html`<ha-svg-icon
slot="start"
.path=${mdiSelectionMarker}
></ha-svg-icon>`}
<div slot="headline">
${!floor.id
? this.localize(
"ui.components.area-picker.unassigned_areas"
)
: floor.primary}
</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`
)}
</ha-md-list>`
: html`<wa-tree @wa-selection-change=${this._handleSelectionChange}>
${this._floorAreas.map((floor, index) =>
index === 0 && !floor.id
? this._renderAreas(floor.areas)
: html`<wa-tree-item
.disabledSelection=${!floor.id}
.target=${floor.id}
.selected=${!!floor.id &&
this._getSelectedTargetId(this.value) === floor.id}
>
${floor.id && (floor as FloorNestedComboBoxItem).floor
? html`<ha-floor-icon
.floor=${(floor as FloorNestedComboBoxItem).floor}
></ha-floor-icon>`
: html`<ha-svg-icon
.path=${mdiSelectionMarker}
></ha-svg-icon>`}
${!floor.id
? this.localize(
"ui.components.area-picker.unassigned_areas"
)
: floor.primary}
${this._renderAreas(floor.areas)}
</wa-tree-item>`
)}
</wa-tree>`} `;
}
private _renderLabels() {
const labels = this._getLabelsMemoized(
this.states,
this.areas,
this.devices,
this.entities,
this._labelRegistry,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
`label${SEPARATOR}`
);
if (!labels.length) {
return nothing;
}
return html`<ha-section-title
>${this.localize("ui.components.label-picker.labels")}</ha-section-title
>
<ha-md-list>
${labels.map(
(label) =>
html`<ha-md-list-item
interactive
type="button"
.target=${label.id}
@click=${this._selectItem}
class=${this._getSelectedTargetId(this.value) === label.id
? "selected"
: ""}
>${label.icon
? html`<ha-icon slot="start" .icon=${label.icon}></ha-icon>`
: label.icon_path
? html`<ha-svg-icon
slot="start"
.path=${label.icon_path}
></ha-svg-icon>`
: nothing}
<div slot="headline">${label.primary}</div>
${this.narrow
? html`<ha-icon-next slot="end"></ha-icon-next> `
: nothing}
</ha-md-list-item>`
)}
</ha-md-list>`;
}
private _renderAreas(areas: FloorComboBoxItem[] = []) {
if (!areas.length) {
return nothing;
}
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
"ui.components.target-picker.type.areas"
)}</ha-section-title
>
<ha-md-list>
${areas.map(({ id, primary, icon, icon_path }) => {
if (!this._areaEntries[id]) {
return nothing;
}
return html`<ha-md-list-item
interactive
type="button"
.target=${id}
@click=${this._selectItem}
>
${icon
? html`<ha-icon slot="start" .icon=${icon}></ha-icon>`
: html`<ha-svg-icon
slot="start"
.path=${icon_path || mdiTextureBox}
></ha-svg-icon>`}
<div slot="headline">${primary}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`;
})}
</ha-md-list>`;
}
return areas.map(({ id, primary, icon, icon_path }) => {
if (!this._areaEntries[id]) {
return nothing;
}
const { open, devices, entities } = this._areaEntries[id];
const numberOfDevices = Object.keys(devices).length;
const numberOfItems = numberOfDevices + entities.length;
return html`<wa-tree-item
.target=${id}
.selected=${this._getSelectedTargetId(this.value) === id}
.lazy=${!open && !!numberOfItems}
@wa-lazy-load=${this._expandItem}
@wa-collapse=${this._collapseItem}
>
${icon
? html`<ha-icon .icon=${icon}></ha-icon>`
: html`<ha-svg-icon
.path=${icon_path || mdiTextureBox}
></ha-svg-icon>`}
${primary}
${open
? html`
${numberOfDevices ? this._renderDevices(devices) : nothing}
${entities.length ? this._renderEntities(entities) : nothing}
`
: nothing}
</wa-tree-item>`;
});
}
private _renderDevices(devices: Record<string, DeviceEntries>) {
const renderedDevices = Object.keys(devices).map((deviceId) => {
if (!this.devices[deviceId]) {
return nothing;
}
const device = this.devices[deviceId];
const configEntry = device.primary_config_entry
? this._configEntryLookup?.[device.primary_config_entry]
: undefined;
const domain = configEntry?.domain;
const deviceName = computeDeviceName(device) || deviceId;
if (this.narrow) {
return html`<ha-md-list-item
interactive
type="button"
.target=${`device${SEPARATOR}${deviceId}`}
@click=${this._selectItem}
>
${domain
? html`
<img
slot="start"
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
/>
`
: nothing}
<div slot="headline">${deviceName}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`;
}
const { open, entities } = devices[deviceId];
return html`<wa-tree-item
.target=${`device${SEPARATOR}${deviceId}`}
.selected=${this._getSelectedTargetId(this.value) ===
`device${SEPARATOR}${deviceId}`}
.lazy=${!open && !!entities.length}
@wa-lazy-load=${this._expandItem}
@wa-collapse=${this._collapseItem}
.title=${deviceName}
>
${domain
? html`
<img
alt=""
crossorigin="anonymous"
referrerpolicy="no-referrer"
src=${brandsUrl({
domain,
type: "icon",
darkOptimized: this.hass.themes?.darkMode,
})}
/>
`
: nothing}
<span class="item-label">${deviceName}</span>
${open ? this._renderEntities(entities) : nothing}
</wa-tree-item>`;
});
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
"ui.components.target-picker.type.devices"
)}</ha-section-title
>
<ha-md-list> ${renderedDevices} </ha-md-list>`;
}
return renderedDevices;
}
private _renderEntities(entities: string[] = []) {
if (!entities.length) {
return nothing;
}
const renderedEntites = entities.map((entityId) => {
const stateObj = this.hass.states[entityId];
const [entityName, deviceName] = computeEntityNameList(
stateObj,
[{ type: "entity" }, { type: "device" }, { type: "area" }],
this.entities,
this.devices,
this.areas,
this.floors
);
const label = entityName || deviceName || entityId;
if (this.narrow) {
return html`<ha-md-list-item
interactive
type="button"
.target=${`entity${SEPARATOR}${entityId}`}
@click=${this._selectItem}
>
<state-badge
slot="start"
.stateObj=${stateObj}
.hass=${this.hass}
></state-badge>
<div slot="headline" class="item-label">${label}</div>
<ha-icon-next slot="end"></ha-icon-next>
</ha-md-list-item>`;
}
return html`<wa-tree-item
.target=${`entity${SEPARATOR}${entityId}`}
.selected=${this._getSelectedTargetId(this.value) ===
`entity${SEPARATOR}${entityId}`}
.title=${label}
>
<state-badge .stateObj=${stateObj} .hass=${this.hass}></state-badge>
<span class="item-label">${label}</span>
</wa-tree-item>`;
});
if (this.narrow) {
return html`<ha-section-title
>${this.localize(
"ui.components.target-picker.type.entities"
)}</ha-section-title
>
<ha-md-list>${renderedEntites}</ha-md-list>`;
}
return renderedEntites;
}
private _getSelectedTargetId = memoizeOne(
(value: SingleHassServiceTarget | undefined) =>
value && Object.keys(value).length
? `${Object.keys(value)[0].replace("_id", "")}${SEPARATOR}${Object.values(value)[0]}`
: undefined
);
private _getTreeData() {
this._floorAreas = getAreasNestedInFloors(
this.states,
this.floors,
this.areas,
this.devices,
this.entities,
this._formatId,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
undefined,
true
);
for (const floor of this._floorAreas) {
for (const area of floor.areas) {
this._loadArea(area);
}
}
}
private _formatId = memoizeOne((value: AreaFloorValue): string =>
[value.type, value.id].join(SEPARATOR)
);
private _handleSelectionChange(ev: WaSelectionChangeEvent) {
const treeItem = ev.detail.selection[0] as unknown as
| { target?: string }
| undefined;
if (treeItem?.target) {
this._valueChanged(treeItem.target);
}
}
private _selectItem(ev: CustomEvent) {
const target = (ev.currentTarget as any).target;
if (target) {
this._valueChanged(target);
}
}
private _valueChanged(itemId: string) {
const [type, id] = itemId.split(SEPARATOR, 2);
fireEvent(this, "value-changed", {
value: { [`${type}_id`]: id || undefined },
});
}
private async _loadArea(area: FloorComboBoxItem) {
try {
const [, id] = area.id.split(SEPARATOR, 2);
const targetEntries = await extractFromTarget(this.hass, {
area_id: id,
});
const devices: Record<string, DeviceEntries> = {};
targetEntries.referenced_devices.forEach((device_id) => {
devices[device_id] = {
open: false,
entities: [],
};
});
const entities: string[] = [];
targetEntries.referenced_entities.forEach((entity_id) => {
const entity = this.hass.entities[entity_id];
if (entity.device_id && devices[entity.device_id]) {
devices[entity.device_id].entities.push(entity_id);
} else {
entities.push(entity_id);
}
});
this._areaEntries = {
...this._areaEntries,
[area.id]: {
open: false,
devices,
entities,
},
};
} catch (e) {
// eslint-disable-next-line no-console
console.error("Failed to extract target", e);
}
}
private _expandItem(ev) {
const targetId = ev.target.target;
const [type, id] = targetId.split(SEPARATOR, 2);
if (type === "area") {
this._areaEntries = {
...this._areaEntries,
[targetId]: {
...this._areaEntries[targetId],
open: true,
},
};
} else if (type === "device") {
const areaEntry = Object.values(this._areaEntries).find((area) =>
Object.keys(area.devices).includes(id)
);
if (areaEntry) {
areaEntry.devices[id].open = true;
this._areaEntries = {
...this._areaEntries,
};
}
}
}
private _collapseItem(ev) {
const targetId = ev.target.target;
const [type, id] = targetId.split(SEPARATOR, 2);
if (type === "area") {
this._areaEntries = {
...this._areaEntries,
[targetId]: {
...this._areaEntries[targetId],
open: false,
},
};
} else if (type === "device") {
const areaEntry = Object.values(this._areaEntries).find((area) =>
Object.keys(area.devices).includes(id)
);
if (areaEntry) {
areaEntry.devices[id].open = false;
this._areaEntries = {
...this._areaEntries,
};
}
}
}
private async _loadConfigEntries() {
const configEntries = await getConfigEntries(this.hass);
this._configEntryLookup = Object.fromEntries(
configEntries.map((entry) => [entry.entry_id, entry])
);
}
public navigateBack() {
if (!this.value) {
return;
}
const valueType = Object.keys(this.value)[0].replace("_id", "");
const valueId = this.value[`${valueType}_id`];
if (valueType === "floor" || valueType === "label") {
fireEvent(this, "value-changed", { value: undefined });
}
if (valueType === "area") {
fireEvent(this, "value-changed", {
value: { floor_id: this.areas[valueId].floor_id },
});
return;
}
if (valueType === "device") {
fireEvent(this, "value-changed", {
value: { area_id: this.devices[valueId].area_id },
});
}
if (valueType === "entity") {
for (const [areaId, areaEntry] of Object.entries(this._areaEntries)) {
const entityDeviceId = this.entities[valueId].device_id;
if (entityDeviceId && areaEntry.devices[entityDeviceId]) {
// Device is also in area -> go back to device
break;
}
if (areaEntry.entities.includes(valueId)) {
fireEvent(this, "value-changed", {
value: { area_id: areaId.split(SEPARATOR, 2)[1] },
});
return;
}
}
fireEvent(this, "value-changed", {
value: { device_id: this.entities[valueId].device_id },
});
}
}
static styles = css`
:host {
--wa-color-neutral-fill-quiet: var(--ha-color-fill-primary-normal-active);
}
ha-section-title {
top: 0;
position: sticky;
z-index: 1;
}
wa-tree-item::part(item) {
height: var(--ha-space-10);
padding: var(--ha-space-1) var(--ha-space-3);
cursor: pointer;
border-inline-start: 0;
}
wa-tree-item::part(label) {
gap: var(--ha-space-3);
font-family: var(--ha-font-family-heading);
font-weight: var(--ha-font-weight-medium);
overflow: hidden;
}
.item-label {
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
ha-svg-icon,
ha-icon,
ha-floor-icon {
padding: var(--ha-space-1);
color: var(--ha-color-on-neutral-quiet);
}
wa-tree-item::part(item):hover {
background-color: var(--ha-color-fill-neutral-quiet-hover);
}
img {
width: 24px;
height: 24px;
padding: var(--ha-space-1);
}
state-badge {
min-width: 32px;
max-width: 32px;
min-height: 32px;
max-height: 32px;
}
wa-tree-item[selected],
wa-tree-item[selected] > ha-svg-icon,
wa-tree-item[selected] > ha-icon,
wa-tree-item[selected] > ha-floor-icon {
color: var(--ha-color-on-primary-normal);
}
wa-tree-item[selected]::part(item):hover {
background-color: var(--ha-color-fill-primary-normal-hover);
}
wa-tree-item::part(base).tree-item-selected .item {
background-color: yellow;
}
ha-md-list {
padding: 0;
--md-list-item-leading-space: var(--ha-space-3);
--md-list-item-trailing-space: var(--md-list-item-leading-space);
--md-list-item-bottom-space: var(--ha-space-1);
--md-list-item-top-space: var(--md-list-item-bottom-space);
--md-list-item-supporting-text-font: var(--ha-font-size-s);
--md-list-item-one-line-container-height: var(--ha-space-10);
}
ha-md-list-item.selected {
background-color: var(--ha-color-fill-primary-normal-active);
--md-list-item-label-text-color: var(--ha-color-on-primary-normal);
--icon-primary-color: var(--ha-color-on-primary-normal);
}
ha-md-list-item.selected ha-icon,
ha-md-list-item.selected ha-svg-icon {
color: var(--ha-color-on-primary-normal);
}
`;
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-add-from-target": HaAutomationAddFromTarget;
}
}

View File

@@ -52,6 +52,8 @@ export const waColorStyles = css`
--wa-color-danger-on-normal: var(--ha-color-on-danger-normal);
--wa-color-danger-on-quiet: var(--ha-color-on-danger-quiet);
--wa-color-text-quiet: var(--ha-color-text-secondary);
--wa-color-text-normal: var(--ha-color-text-primary);
--wa-color-surface-default: var(--card-background-color);
--wa-color-surface-raised: var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff));
@@ -62,5 +64,7 @@ export const waColorStyles = css`
--wa-focus-ring-color: var(--ha-color-neutral-60);
--wa-shadow-l: 4px 8px 12px 0 rgba(0, 0, 0, 0.3);
--wa-color-text-normal: var(--ha-color-text-primary);
}
`;

View File

@@ -9,12 +9,16 @@ export const waMainStyles = css`
--wa-focus-ring-offset: 2px;
--wa-focus-ring: var(--wa-focus-ring-style) var(--wa-focus-ring-width) var(--wa-focus-ring-color);
--wa-space-xs: var(--ha-space-2);
--wa-space-m: var(--ha-space-4);
--wa-space-l: var(--ha-space-6);
--wa-space-xl: var(--ha-space-8);
--wa-form-control-padding-block: 0.75em;
--wa-form-control-value-line-height: var(--ha-line-height-condensed);
--wa-font-weight-action: var(--ha-font-weight-medium);
--wa-transition-normal: 150ms;
--wa-transition-fast: 75ms;
--wa-transition-easing: ease;
@@ -28,6 +32,7 @@ export const waMainStyles = css`
--wa-line-height-condensed: var(--ha-line-height-condensed);
--wa-font-size-m: var(--ha-font-size-m);
--wa-shadow-s: var(--ha-box-shadow-s);
--wa-shadow-m: var(--ha-box-shadow-m);
--wa-shadow-l: var(--ha-box-shadow-l);

View File

@@ -3989,6 +3989,9 @@
"item_pasted": "{item} pasted",
"ctrl": "Ctrl",
"del": "Del",
"targets": "Targets",
"select_target": "Select a target",
"home": "Home",
"blocks": "Blocks",
"triggers": {
"name": "Triggers",