Compare commits

...
Author SHA1 Message Date
Maarten Lakerveld 53f0b5df77 Resolve if-action conditions in the trace path details
Core traces the conditions of an if action under if/condition/<n>, but the
config keeps them directly in the if list. The path details walked the
selected config by the trace path, so that segment stopped the walk at the
whole list, which the condition describer rejected with "Cannot read
properties of undefined (reading 'includes')". Skip the segment when the
current level is already a list.

Fixes #54871
2026-10-14 13:11:34 +02:00
3 changed files with 86 additions and 16 deletions

No files matched your search

+9 -15
View File
@@ -28,7 +28,11 @@ import type {
TraceExtended,
} from "../../data/trace";
import type { TargetSelector } from "../../data/selector";
import { getDataFromPath, isTriggerPath } from "../../data/trace";
import {
getDataFromPath,
getNestedConfigFromPath,
isTriggerPath,
} from "../../data/trace";
import { getTraceTriggers } from "../../data/trace-tree";
import type { TriggerDescriptions } from "../../data/trigger";
import { getDeviceTarget } from "../../panels/config/automation/target/get_device_target";
@@ -220,20 +224,10 @@ export class HaTracePathDetails extends LitElement {
const data: ActionTraceStep[] = paths[curPath];
// Extract details from this.selected.config child properties used to add 'alias' (to headline), describeCondition and 'entity_id' (to result)
const nestPath = curPath
.substring(this.selected.path.length + 1)
.split("/");
let currentDetail: unknown = this.selected.config;
for (const part of nestPath) {
if (typeof currentDetail !== "object" || currentDetail === null) {
break;
}
const child = (currentDetail as Record<string, unknown>)[part];
if (child === undefined || typeof child === "string") {
break;
}
currentDetail = child;
}
const currentDetail = getNestedConfigFromPath(
this.selected.config,
curPath.substring(this.selected.path.length + 1).split("/")
);
parts.push(
data.map((trace, idx) => {
+24
View File
@@ -266,6 +266,30 @@ export const getDataFromPath = (
return result;
};
export const getNestedConfigFromPath = (
config: unknown,
nestPath: string[]
): unknown => {
let result = config;
for (const part of nestPath) {
if (typeof result !== "object" || result === null) {
break;
}
if (part === "entity_id") {
break;
}
if (part === "condition" && Array.isArray(result)) {
continue;
}
const child = (result as Record<string, unknown>)[part];
if (child === undefined) {
break;
}
result = child;
}
return result;
};
// It is 'trigger' if manually triggered by the user via UI
export const isTriggerPath = (path: string): boolean =>
path === "trigger" || strStartsWith(path, "trigger/");
+53 -1
View File
@@ -1,5 +1,5 @@
import { describe, expect, it } from "vitest";
import { getTraceUrl } from "../../src/data/trace";
import { getNestedConfigFromPath, getTraceUrl } from "../../src/data/trace";
// Trace links from the logbook and from trace steps share this URL. Automation
// ids are free-form, and the automation router decodes the path segment, so
@@ -28,3 +28,55 @@ describe("getTraceUrl", () => {
expect(parsed.searchParams.get("run_id")).toBe("run_1");
});
});
// Core traces the conditions of an `if` action under `if/condition/<n>` while
// the config keeps them directly in the `if` list, so the path details must
// skip that segment or they hand the whole list to the condition describer.
describe("getNestedConfigFromPath", () => {
const stateCondition = {
condition: "state",
entity_id: "light.a",
state: "off",
};
const ifAction = {
if: [{ condition: "or", conditions: [stateCondition] }],
then: [],
};
it("resolves the conditions of an if action", () => {
expect(getNestedConfigFromPath(ifAction, ["if", "condition", "0"])).toBe(
ifAction.if[0]
);
});
it("keeps the condition for its traced entity ids", () => {
const path = ["if", "condition", "0", "conditions", "0", "entity_id", "0"];
expect(getNestedConfigFromPath(ifAction, path)).toBe(stateCondition);
const listCondition = { ...stateCondition, entity_id: ["light.a"] };
const listAction = { if: [listCondition], then: [] };
expect(
getNestedConfigFromPath(listAction, [
"if",
"condition",
"0",
"entity_id",
"0",
])
).toBe(listCondition);
});
it("resolves a template string condition", () => {
const templateAction = {
if: ["{{ is_state('light.a', 'off') }}"],
then: [],
};
expect(
getNestedConfigFromPath(templateAction, ["if", "condition", "0"])
).toBe(templateAction.if[0]);
});
it("keeps the selected config when the path does not exist", () => {
expect(getNestedConfigFromPath(ifAction, ["else", "0"])).toBe(ifAction);
});
});