Skip to content
Open
Show file tree
Hide file tree
Changes from 9 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 17 additions & 4 deletions src/components/device/ha-device-picker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,14 @@ export class HaDevicePicker extends LitElement {
@property({ type: Array, attribute: "exclude-devices" })
public excludeDevices?: string[];

/**
* List of devices to be suggested at the top of the list.
* @type {Array}
* @attr suggested-devices
*/
@property({ attribute: false, type: Array })
public suggestedDevices?: string[];

@property({ attribute: false })
public deviceFilter?: HaDevicePickerDeviceFilterFunc;

Expand Down Expand Up @@ -126,7 +134,8 @@ export class HaDevicePicker extends LitElement {
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.excludeDevices
this.excludeDevices,
this.suggestedDevices
);

private _getDevices = memoizeOne(
Expand All @@ -139,7 +148,8 @@ export class HaDevicePicker extends LitElement {
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
excludeDevices: this["excludeDevices"]
excludeDevices: this["excludeDevices"],
suggestedDevices: this["suggestedDevices"]
): DevicePickerItem[] => {
const devices = Object.values(haDevices);
const entities = Object.values(haEntities);
Expand Down Expand Up @@ -253,6 +263,10 @@ export class HaDevicePicker extends LitElement {
? domainToName(this.hass.localize, domain)
: undefined;

const suggestedPrefix = suggestedDevices?.includes(device.id)
? "0|"
: "1|";
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bramkragten This is hacky but I didn't want to increase the scope of the PR nor know how you would approach an alternative. In my mind this would be replaced by a proper grouping like “Suggested” and “All devices” or something similar.


return {
id: device.id,
label: "",
Expand All @@ -265,10 +279,9 @@ export class HaDevicePicker extends LitElement {
search_labels: [deviceName, areaName, domain, domainName].filter(
Boolean
) as string[],
sorting_label: deviceName || "zzz",
sorting_label: `${suggestedPrefix}${deviceName || "zzz"}`,
};
});

return outputDevices;
}
);
Expand Down
137 changes: 137 additions & 0 deletions src/data/automation_editor_context.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,137 @@
import { createContext } from "@lit/context";
import type { HomeAssistant } from "../types";
import type { AutomationConfig } from "./automation";
import { ensureArray } from "../common/array/ensure-array";

export type AutomationSection = "triggers" | "conditions" | "actions";

export type EntityId = string;
export type DeviceId = string;
export type AreaId = string;
export type Domain = string;

export interface AutomationLocalContext {
meta: {
automationId?: string;
signature: string;
};

used: {
entities: EntityId[];
devices: DeviceId[];
areas: AreaId[];
domains: Domain[];
};

maps: {
entityArea: Record<EntityId, AreaId | null>;
entityDevice: Record<EntityId, DeviceId | null>;
deviceArea: Record<DeviceId, AreaId | null>;
deviceDomains: Record<DeviceId, Domain[]>;
};

weights?: {
used: number;
sameDomain: number;
sameArea: number;
};

hints?: {
lastEditedPath?: string[] | number[];
labels?: string[];
services?: string[];
};
}

/**
* Context key for the automation editor locality context.
*/
export const automationEditorContext = createContext<
AutomationLocalContext | undefined
>("automationEditorContext");

const addIds = (ids: unknown, set: Set<string>) => {
if (ids === undefined || ids === null) return;
const list = ensureArray(ids) as unknown[];
for (const item of list) {
if (typeof item === "string" && item) {
set.add(item);
}
}
};

const getContextSignature = (devices: Set<string>): string =>
Array.from(devices.values())
.map((d) => d.slice(0, 6))
.sort()
.join();

export const buildAutomationLocalContext = (
config: AutomationConfig | undefined,
_hass: HomeAssistant,
automationId?: string,
previous?: AutomationLocalContext
): AutomationLocalContext | undefined => {
if (!config) return undefined;

const devicesSet = new Set<string>();

const scan = (val: any) => {
if (val === null || val === undefined) return;
if (Array.isArray(val)) {
for (const item of val) scan(item);
return;
}
if (typeof val !== "object") return;

if ("device_id" in val) {
addIds((val as any).device_id, devicesSet);
}
if ("target" in val && val.target && typeof val.target === "object") {
if ("device_id" in (val.target as any)) {
addIds((val.target as any).device_id, devicesSet);
}
}

for (const key of Object.keys(val)) {
const child = (val as any)[key];
if (child && typeof child === "object") {
scan(child);
}
}
};

scan(config.triggers);
scan(config.conditions);
scan(config.actions);

// Avoid re-rendering if nothing changed
const signature = getContextSignature(devicesSet);
if (previous && previous.meta.signature === signature) {
return previous;
}

return {
meta: {
automationId,
signature,
},
used: {
entities: [],
devices: Array.from(devicesSet),
areas: [],
domains: [],
},
maps: {
entityArea: {},
entityDevice: {},
deviceArea: Object.fromEntries(
Object.entries(_hass.devices || {}).map(([id, dev]) => [
id,
dev.area_id || null,
])
),
deviceDomains: {},
},
};
};
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import "../../../../../components/device/ha-device-action-picker";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-form/ha-form";
import { fullEntitiesContext } from "../../../../../data/context";
import {
automationEditorContext,
type AutomationLocalContext,
} from "../../../../../data/automation_editor_context";
import type {
DeviceAction,
DeviceCapabilities,
Expand Down Expand Up @@ -37,6 +41,10 @@ export class HaDeviceAction extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];

@state()
@consume({ context: automationEditorContext, subscribe: true })
private _automationCtx?: AutomationLocalContext;

private _origAction?: DeviceAction;

public static get defaultConfig(): DeviceAction {
Expand Down Expand Up @@ -87,6 +95,7 @@ export class HaDeviceAction extends LitElement {
return html`
<ha-device-picker
.value=${deviceId}
.suggestedDevices=${this._automationCtx?.used?.devices}
.disabled=${this.disabled}
@value-changed=${this._devicePicked}
.hass=${this.hass}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@ import "../../../../../components/device/ha-device-condition-picker";
import "../../../../../components/device/ha-device-picker";
import "../../../../../components/ha-form/ha-form";
import { fullEntitiesContext } from "../../../../../data/context";
import {
automationEditorContext,
type AutomationLocalContext,
} from "../../../../../data/automation_editor_context";
import type {
DeviceCapabilities,
DeviceCondition,
Expand Down Expand Up @@ -37,6 +41,10 @@ export class HaDeviceCondition extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];

@state()
@consume({ context: automationEditorContext, subscribe: true })
private _automationCtx?: AutomationLocalContext;

private _origCondition?: DeviceCondition;

public static get defaultConfig(): DeviceCondition {
Expand Down Expand Up @@ -88,6 +96,7 @@ export class HaDeviceCondition extends LitElement {
return html`
<ha-device-picker
.value=${deviceId}
.suggestedDevices=${this._automationCtx?.used?.devices}
@value-changed=${this._devicePicked}
.hass=${this.hass}
.disabled=${this.disabled}
Expand Down
22 changes: 21 additions & 1 deletion src/panels/config/automation/ha-automation-editor.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { consume } from "@lit/context";
import { consume, ContextProvider } from "@lit/context";
import {
mdiAppleKeyboardCommand,
mdiCog,
Expand Down Expand Up @@ -89,6 +89,11 @@ import {
import "./blueprint-automation-editor";
import "./manual-automation-editor";
import type { HaManualAutomationEditor } from "./manual-automation-editor";
import {
automationEditorContext,
buildAutomationLocalContext,
} from "../../../data/automation_editor_context";
import type { AutomationLocalContext } from "../../../data/automation_editor_context";

declare global {
interface HTMLElementTagNameMap {
Expand Down Expand Up @@ -185,6 +190,13 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
currentConfig: () => this._config!,
});

private _automationContextProvider = new ContextProvider(this, {
context: automationEditorContext,
initialValue: undefined,
});

private _automationContextValue?: AutomationLocalContext;

protected willUpdate(changedProps) {
super.willUpdate(changedProps);

Expand Down Expand Up @@ -716,6 +728,14 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
Object.values(this._configSubscriptions).forEach((sub) =>
sub(this._config)
);
const ctx = buildAutomationLocalContext(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would like to prevent this from re-creating everything when the automation changes, as it will trigger the other memoized functions to also recalculate. Can we make sure to only update those parts that actually changed? So if no devices changed, we don't recreate used.devices.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I added a signature to the context that we can compare on change updates to not update the context object.

this._config,
this.hass,
this.automationId ?? this._entityId ?? undefined,
this._automationContextValue
);
this._automationContextProvider.setValue(ctx);
this._automationContextValue = ctx;
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,10 @@ import "../../../../../components/device/ha-device-trigger-picker";
import "../../../../../components/ha-form/ha-form";
import { computeInitialHaFormData } from "../../../../../components/ha-form/compute-initial-ha-form-data";
import { fullEntitiesContext } from "../../../../../data/context";
import {
automationEditorContext,
type AutomationLocalContext,
} from "../../../../../data/automation_editor_context";
import type {
DeviceCapabilities,
DeviceTrigger,
Expand Down Expand Up @@ -39,6 +43,10 @@ export class HaDeviceTrigger extends LitElement {
@consume({ context: fullEntitiesContext, subscribe: true })
_entityReg!: EntityRegistryEntry[];

@state()
@consume({ context: automationEditorContext, subscribe: true })
private _automationCtx?: AutomationLocalContext;

private _origTrigger?: DeviceTrigger;

public static get defaultConfig(): DeviceTrigger {
Expand Down Expand Up @@ -92,6 +100,7 @@ export class HaDeviceTrigger extends LitElement {
return html`
<ha-device-picker
.value=${deviceId}
.suggestedDevices=${this._automationCtx?.used?.devices}
@value-changed=${this._devicePicked}
.hass=${this.hass}
.disabled=${this.disabled}
Expand Down
Loading