mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-20 15:56:35 +00:00
Trace foundation (#8608)
This commit is contained in:
parent
dc3ee7c779
commit
7bd4eeb0df
182
gallery/src/data/traces/basic_trace.ts
Normal file
182
gallery/src/data/traces/basic_trace.ts
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
import { DemoTrace } from "./types";
|
||||||
|
|
||||||
|
export const basicTrace: DemoTrace = {
|
||||||
|
trace: {
|
||||||
|
last_action: "action/0",
|
||||||
|
last_condition: "condition/0",
|
||||||
|
run_id: "0",
|
||||||
|
state: "stopped",
|
||||||
|
timestamp: {
|
||||||
|
start: "2021-03-12T21:38:48.050464+00:00",
|
||||||
|
finish: "2021-03-12T21:38:48.068458+00:00",
|
||||||
|
},
|
||||||
|
trigger: "state of input_boolean.toggle_1",
|
||||||
|
unique_id: "1615419646544",
|
||||||
|
action_trace: {
|
||||||
|
"action/0": [
|
||||||
|
{
|
||||||
|
timestamp: "2021-03-12T21:38:48.054395+00:00",
|
||||||
|
changed_variables: {
|
||||||
|
trigger: {
|
||||||
|
platform: "state",
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
from_state: {
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
state: "on",
|
||||||
|
attributes: { editable: true, friendly_name: "Toggle 1" },
|
||||||
|
last_changed: "2021-03-12T21:38:03.262985+00:00",
|
||||||
|
last_updated: "2021-03-12T21:38:03.262985+00:00",
|
||||||
|
context: {
|
||||||
|
id: "4ad34793b237d7cb5e541e8a331e7bf9",
|
||||||
|
parent_id: null,
|
||||||
|
user_id: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_state: {
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
state: "off",
|
||||||
|
attributes: { editable: true, friendly_name: "Toggle 1" },
|
||||||
|
last_changed: "2021-03-12T21:38:48.049816+00:00",
|
||||||
|
last_updated: "2021-03-12T21:38:48.049816+00:00",
|
||||||
|
context: {
|
||||||
|
id: "2d83ca81663c85df51fae2a1f940d0e7",
|
||||||
|
parent_id: null,
|
||||||
|
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
for: null,
|
||||||
|
attribute: null,
|
||||||
|
description: "state of input_boolean.toggle_1",
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
id: "febeaa3d50152bae8017d783ed3c0751",
|
||||||
|
parent_id: "2d83ca81663c85df51fae2a1f940d0e7",
|
||||||
|
user_id: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
condition_trace: {
|
||||||
|
"condition/0": [
|
||||||
|
{
|
||||||
|
timestamp: "2021-03-12T21:38:48.050783+00:00",
|
||||||
|
changed_variables: {
|
||||||
|
trigger: {
|
||||||
|
platform: "state",
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
from_state: {
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
state: "on",
|
||||||
|
attributes: { editable: true, friendly_name: "Toggle 1" },
|
||||||
|
last_changed: "2021-03-12T21:38:03.262985+00:00",
|
||||||
|
last_updated: "2021-03-12T21:38:03.262985+00:00",
|
||||||
|
context: {
|
||||||
|
id: "4ad34793b237d7cb5e541e8a331e7bf9",
|
||||||
|
parent_id: null,
|
||||||
|
user_id: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_state: {
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
state: "off",
|
||||||
|
attributes: { editable: true, friendly_name: "Toggle 1" },
|
||||||
|
last_changed: "2021-03-12T21:38:48.049816+00:00",
|
||||||
|
last_updated: "2021-03-12T21:38:48.049816+00:00",
|
||||||
|
context: {
|
||||||
|
id: "2d83ca81663c85df51fae2a1f940d0e7",
|
||||||
|
parent_id: null,
|
||||||
|
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
for: null,
|
||||||
|
attribute: null,
|
||||||
|
description: "state of input_boolean.toggle_1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
result: { result: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
config: {
|
||||||
|
id: "1615419646544",
|
||||||
|
alias: "Basic Trace Example",
|
||||||
|
description: "",
|
||||||
|
trigger: [{ platform: "state", entity_id: "input_boolean.toggle_1" }],
|
||||||
|
condition: [
|
||||||
|
{
|
||||||
|
condition: "template",
|
||||||
|
alias: "Test if Paulus is home",
|
||||||
|
value_template: "{{ true }}",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
action: [
|
||||||
|
{
|
||||||
|
service: "input_boolean.toggle",
|
||||||
|
alias: "Enable party mode",
|
||||||
|
target: { entity_id: "input_boolean.toggle_2" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
mode: "single",
|
||||||
|
},
|
||||||
|
context: {
|
||||||
|
id: "febeaa3d50152bae8017d783ed3c0751",
|
||||||
|
parent_id: "2d83ca81663c85df51fae2a1f940d0e7",
|
||||||
|
user_id: null,
|
||||||
|
},
|
||||||
|
variables: {
|
||||||
|
trigger: {
|
||||||
|
platform: "state",
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
from_state: {
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
state: "on",
|
||||||
|
attributes: { editable: true, friendly_name: "Toggle 1" },
|
||||||
|
last_changed: "2021-03-12T21:38:03.262985+00:00",
|
||||||
|
last_updated: "2021-03-12T21:38:03.262985+00:00",
|
||||||
|
context: {
|
||||||
|
id: "4ad34793b237d7cb5e541e8a331e7bf9",
|
||||||
|
parent_id: null,
|
||||||
|
user_id: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
to_state: {
|
||||||
|
entity_id: "input_boolean.toggle_1",
|
||||||
|
state: "off",
|
||||||
|
attributes: { editable: true, friendly_name: "Toggle 1" },
|
||||||
|
last_changed: "2021-03-12T21:38:48.049816+00:00",
|
||||||
|
last_updated: "2021-03-12T21:38:48.049816+00:00",
|
||||||
|
context: {
|
||||||
|
id: "2d83ca81663c85df51fae2a1f940d0e7",
|
||||||
|
parent_id: null,
|
||||||
|
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
for: null,
|
||||||
|
attribute: null,
|
||||||
|
description: "state of input_boolean.toggle_1",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
logbookEntries: [
|
||||||
|
{
|
||||||
|
name: "Ensure Party mode",
|
||||||
|
message: "has been triggered by state of input_boolean.toggle_1",
|
||||||
|
source: "state of input_boolean.toggle_1",
|
||||||
|
entity_id: "automation.toggle_toggles",
|
||||||
|
when: "2021-03-12T21:38:48.051460+00:00",
|
||||||
|
domain: "automation",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
when: "2021-03-12T21:38:48.064184+00:00",
|
||||||
|
name: "Toggle 2",
|
||||||
|
state: "off",
|
||||||
|
entity_id: "input_boolean.toggle_2",
|
||||||
|
context_entity_id: "automation.toggle_toggles",
|
||||||
|
context_entity_id_name: "Ensure Party mode",
|
||||||
|
context_event_type: "automation_triggered",
|
||||||
|
context_domain: "automation",
|
||||||
|
context_name: "Ensure Party mode",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
7
gallery/src/data/traces/types.ts
Normal file
7
gallery/src/data/traces/types.ts
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
import { AutomationTraceExtended } from "../../../../src/data/automation_debug";
|
||||||
|
import { LogbookEntry } from "../../../../src/data/logbook";
|
||||||
|
|
||||||
|
export interface DemoTrace {
|
||||||
|
trace: AutomationTraceExtended;
|
||||||
|
logbookEntries: LogbookEntry[];
|
||||||
|
}
|
62
gallery/src/demos/demo-automation-trace.ts
Normal file
62
gallery/src/demos/demo-automation-trace.ts
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
import {
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
css,
|
||||||
|
LitElement,
|
||||||
|
TemplateResult,
|
||||||
|
property,
|
||||||
|
} from "lit-element";
|
||||||
|
import "../../../src/components/ha-card";
|
||||||
|
import "../../../src/components/trace/hat-trace";
|
||||||
|
import { provideHass } from "../../../src/fake_data/provide_hass";
|
||||||
|
import { HomeAssistant } from "../../../src/types";
|
||||||
|
import { basicTrace } from "../data/traces/basic_trace";
|
||||||
|
import { DemoTrace } from "../data/traces/types";
|
||||||
|
|
||||||
|
const traces: DemoTrace[] = [basicTrace];
|
||||||
|
|
||||||
|
@customElement("demo-automation-trace")
|
||||||
|
export class DemoAutomationTrace extends LitElement {
|
||||||
|
@property({ attribute: false }) hass?: HomeAssistant;
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.hass) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
return html`
|
||||||
|
${traces.map(
|
||||||
|
(trace) => html`
|
||||||
|
<ha-card .heading=${trace.trace.config.alias}>
|
||||||
|
<div class="card-content">
|
||||||
|
<hat-trace
|
||||||
|
.hass=${this.hass}
|
||||||
|
.trace=${trace.trace}
|
||||||
|
.logbookEntries=${trace.logbookEntries}
|
||||||
|
></hat-trace>
|
||||||
|
</div>
|
||||||
|
</ha-card>
|
||||||
|
`
|
||||||
|
)}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected firstUpdated(changedProps) {
|
||||||
|
super.firstUpdated(changedProps);
|
||||||
|
provideHass(this);
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 600px;
|
||||||
|
margin: 24px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-automation-trace": DemoAutomationTrace;
|
||||||
|
}
|
||||||
|
}
|
@ -81,4 +81,8 @@ class DemoMoreInfoLight extends LitElement {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
customElements.define("demo-more-info-light", DemoMoreInfoLight);
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"demo-more-info-light": DemoMoreInfoLight;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -111,29 +111,9 @@ class HaGallery extends PolymerElement {
|
|||||||
</template>
|
</template>
|
||||||
</ha-card>
|
</ha-card>
|
||||||
|
|
||||||
<ha-card header="More Info Demos">
|
<ha-card header="Other Demos">
|
||||||
<div class="card-content intro">
|
<div class="card-content intro"></div>
|
||||||
<p>
|
<template is="dom-repeat" items="[[_restDemos]]">
|
||||||
More info screens show up when an entity is clicked.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<template is="dom-repeat" items="[[_moreInfoDemos]]">
|
|
||||||
<a href="#[[item]]">
|
|
||||||
<paper-item>
|
|
||||||
<paper-item-body>{{ item }}</paper-item-body>
|
|
||||||
<ha-icon icon="hass:chevron-right"></ha-icon>
|
|
||||||
</paper-item>
|
|
||||||
</a>
|
|
||||||
</template>
|
|
||||||
</ha-card>
|
|
||||||
|
|
||||||
<ha-card header="Util Demos">
|
|
||||||
<div class="card-content intro">
|
|
||||||
<p>
|
|
||||||
Test pages for our utility functions.
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
<template is="dom-repeat" items="[[_utilDemos]]">
|
|
||||||
<a href="#[[item]]">
|
<a href="#[[item]]">
|
||||||
<paper-item>
|
<paper-item>
|
||||||
<paper-item-body>{{ item }}</paper-item-body>
|
<paper-item-body>{{ item }}</paper-item-body>
|
||||||
@ -178,13 +158,9 @@ class HaGallery extends PolymerElement {
|
|||||||
type: Array,
|
type: Array,
|
||||||
computed: "_computeLovelace(_demos)",
|
computed: "_computeLovelace(_demos)",
|
||||||
},
|
},
|
||||||
_moreInfoDemos: {
|
_restDemos: {
|
||||||
type: Array,
|
type: Array,
|
||||||
computed: "_computeMoreInfos(_demos)",
|
computed: "_computeRest(_demos)",
|
||||||
},
|
|
||||||
_utilDemos: {
|
|
||||||
type: Array,
|
|
||||||
computed: "_computeUtil(_demos)",
|
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@ -237,12 +213,8 @@ class HaGallery extends PolymerElement {
|
|||||||
return demos.filter((demo) => demo.includes("hui"));
|
return demos.filter((demo) => demo.includes("hui"));
|
||||||
}
|
}
|
||||||
|
|
||||||
_computeMoreInfos(demos) {
|
_computeRest(demos) {
|
||||||
return demos.filter((demo) => demo.includes("more-info"));
|
return demos.filter((demo) => !demo.includes("hui"));
|
||||||
}
|
|
||||||
|
|
||||||
_computeUtil(demos) {
|
|
||||||
return demos.filter((demo) => demo.includes("util"));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
65
src/components/trace/ha-timeline.ts
Normal file
65
src/components/trace/ha-timeline.ts
Normal file
@ -0,0 +1,65 @@
|
|||||||
|
import { mdiCircleOutline } from "@mdi/js";
|
||||||
|
import { LitElement, customElement, html, css, property } from "lit-element";
|
||||||
|
import "../ha-svg-icon";
|
||||||
|
|
||||||
|
@customElement("ha-timeline")
|
||||||
|
class HaTimeline extends LitElement {
|
||||||
|
@property({ type: Boolean }) public lastItem = false;
|
||||||
|
|
||||||
|
@property({ type: String }) public icon?: string;
|
||||||
|
|
||||||
|
protected render() {
|
||||||
|
return html`
|
||||||
|
<div class="timeline-start">
|
||||||
|
<ha-svg-icon .path=${this.icon || mdiCircleOutline}></ha-svg-icon>
|
||||||
|
${this.lastItem ? "" : html`<div class="line"></div>`}
|
||||||
|
</div>
|
||||||
|
<div class="content"><slot></slot></div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles() {
|
||||||
|
return css`
|
||||||
|
:host {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
:host(:not([lastItem])) {
|
||||||
|
min-height: 50px;
|
||||||
|
}
|
||||||
|
.timeline-start {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
||||||
|
ha-svg-icon {
|
||||||
|
color: var(
|
||||||
|
--timeline-ball-color,
|
||||||
|
var(--timeline-color, var(--secondary-text-color))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
.line {
|
||||||
|
flex: 1;
|
||||||
|
width: 2px;
|
||||||
|
background-color: var(
|
||||||
|
--timeline-line-color,
|
||||||
|
var(--timeline-color, var(--secondary-text-color))
|
||||||
|
);
|
||||||
|
margin: 4px 0;
|
||||||
|
}
|
||||||
|
.content {
|
||||||
|
margin-top: 2px;
|
||||||
|
}
|
||||||
|
:host(:not([lastItem])) .content {
|
||||||
|
padding-bottom: 16px;
|
||||||
|
}
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-timeline": HaTimeline;
|
||||||
|
}
|
||||||
|
}
|
185
src/components/trace/hat-trace.ts
Normal file
185
src/components/trace/hat-trace.ts
Normal file
@ -0,0 +1,185 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
|
||||||
|
import {
|
||||||
|
ActionTrace,
|
||||||
|
AutomationTraceExtended,
|
||||||
|
getConfigFromPath,
|
||||||
|
} from "../../data/automation_debug";
|
||||||
|
import { HomeAssistant } from "../../types";
|
||||||
|
import "./ha-timeline";
|
||||||
|
import {
|
||||||
|
mdiCheckCircleOutline,
|
||||||
|
mdiCircle,
|
||||||
|
mdiCircleOutline,
|
||||||
|
mdiPauseCircleOutline,
|
||||||
|
mdiRecordCircleOutline,
|
||||||
|
mdiStopCircleOutline,
|
||||||
|
} from "@mdi/js";
|
||||||
|
import { LogbookEntry } from "../../data/logbook";
|
||||||
|
|
||||||
|
const pathToName = (path: string) => path.split("/").join(" ");
|
||||||
|
|
||||||
|
@customElement("hat-trace")
|
||||||
|
export class HaAutomationTracer extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property({ attribute: false }) private trace?: AutomationTraceExtended;
|
||||||
|
|
||||||
|
@property({ attribute: false }) private logbookEntries?: LogbookEntry[];
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
if (!this.trace) {
|
||||||
|
return html``;
|
||||||
|
}
|
||||||
|
const entries = [
|
||||||
|
html`
|
||||||
|
<ha-timeline .icon=${mdiCircle}>
|
||||||
|
Triggered by the ${this.trace.variables.trigger.description} at
|
||||||
|
${formatDateTimeWithSeconds(
|
||||||
|
new Date(this.trace.timestamp.start),
|
||||||
|
this.hass.language
|
||||||
|
)}
|
||||||
|
</ha-timeline>
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
|
||||||
|
if (this.trace.condition_trace) {
|
||||||
|
for (const [path, value] of Object.entries(this.trace.condition_trace)) {
|
||||||
|
entries.push(html`
|
||||||
|
<ha-timeline
|
||||||
|
?lastItem=${!value[0].result.result}
|
||||||
|
class="condition"
|
||||||
|
.icon=${value[0].result.result
|
||||||
|
? mdiCheckCircleOutline
|
||||||
|
: mdiStopCircleOutline}
|
||||||
|
>
|
||||||
|
${getConfigFromPath(this.trace!.config, path).alias ||
|
||||||
|
pathToName(path)}
|
||||||
|
${value[0].result.result ? "passed" : "failed"}
|
||||||
|
</ha-timeline>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (this.trace.action_trace && this.logbookEntries) {
|
||||||
|
const actionTraces = Object.entries(this.trace.action_trace);
|
||||||
|
|
||||||
|
let logbookIndex = 0;
|
||||||
|
let actionTraceIndex = 0;
|
||||||
|
|
||||||
|
while (
|
||||||
|
logbookIndex < this.logbookEntries.length &&
|
||||||
|
actionTraceIndex < actionTraces.length
|
||||||
|
) {
|
||||||
|
// Find next item.
|
||||||
|
|
||||||
|
// Skip the "automation got triggered item"
|
||||||
|
if (
|
||||||
|
logbookIndex === 0 &&
|
||||||
|
this.logbookEntries[0].domain === "automation"
|
||||||
|
) {
|
||||||
|
logbookIndex++;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find next item time-wise.
|
||||||
|
const logbookItem = this.logbookEntries[logbookIndex];
|
||||||
|
const actionTrace = actionTraces[actionTraceIndex];
|
||||||
|
|
||||||
|
if (
|
||||||
|
new Date(logbookItem.when) > new Date(actionTrace[1][0].timestamp)
|
||||||
|
) {
|
||||||
|
actionTraceIndex++;
|
||||||
|
entries.push(this._renderActionTrace(...actionTrace));
|
||||||
|
} else {
|
||||||
|
logbookIndex++;
|
||||||
|
entries.push(this._renderLogbookEntry(logbookItem));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Append all leftover items
|
||||||
|
while (logbookIndex < this.logbookEntries.length) {
|
||||||
|
entries.push(
|
||||||
|
this._renderLogbookEntry(this.logbookEntries[logbookIndex])
|
||||||
|
);
|
||||||
|
logbookIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (actionTraceIndex < actionTraces.length) {
|
||||||
|
entries.push(
|
||||||
|
this._renderActionTrace(...actionTraces[actionTraceIndex])
|
||||||
|
);
|
||||||
|
actionTraceIndex++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// null means it was stopped by a condition
|
||||||
|
if (this.trace.last_action !== null) {
|
||||||
|
entries.push(html`
|
||||||
|
<ha-timeline
|
||||||
|
lastItem
|
||||||
|
.icon=${this.trace.timestamp.finish
|
||||||
|
? mdiCircle
|
||||||
|
: mdiPauseCircleOutline}
|
||||||
|
>
|
||||||
|
${this.trace.timestamp.finish
|
||||||
|
? html`Finished at
|
||||||
|
${formatDateTimeWithSeconds(
|
||||||
|
new Date(this.trace.timestamp.finish),
|
||||||
|
this.hass.language
|
||||||
|
)}
|
||||||
|
(runtime:
|
||||||
|
${(
|
||||||
|
(Number(new Date(this.trace.timestamp.finish!)) -
|
||||||
|
Number(new Date(this.trace.timestamp.start))) /
|
||||||
|
1000
|
||||||
|
).toFixed(2)}
|
||||||
|
seconds)`
|
||||||
|
: "Still running"}
|
||||||
|
</ha-timeline>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
return html`${entries}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderLogbookEntry(entry: LogbookEntry) {
|
||||||
|
return html`
|
||||||
|
<ha-timeline .icon=${mdiCircleOutline}>
|
||||||
|
${entry.name} (${entry.entity_id}) turned ${entry.state}
|
||||||
|
</ha-timeline>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _renderActionTrace(path: string, _value: ActionTrace[]) {
|
||||||
|
return html`
|
||||||
|
<ha-timeline .icon=${mdiRecordCircleOutline}>
|
||||||
|
${getConfigFromPath(this.trace!.config, path).alias || pathToName(path)}
|
||||||
|
</ha-timeline>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
css`
|
||||||
|
ha-timeline[lastItem].condition {
|
||||||
|
--timeline-ball-color: var(--error-color);
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"hat-trace": HaAutomationTracer;
|
||||||
|
}
|
||||||
|
}
|
@ -149,11 +149,13 @@ export type Trigger =
|
|||||||
|
|
||||||
export interface LogicalCondition {
|
export interface LogicalCondition {
|
||||||
condition: "and" | "not" | "or";
|
condition: "and" | "not" | "or";
|
||||||
|
alias?: string;
|
||||||
conditions: Condition[];
|
conditions: Condition[];
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface StateCondition {
|
export interface StateCondition {
|
||||||
condition: "state";
|
condition: "state";
|
||||||
|
alias?: string;
|
||||||
entity_id: string;
|
entity_id: string;
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
state: string | number;
|
state: string | number;
|
||||||
@ -162,6 +164,7 @@ export interface StateCondition {
|
|||||||
|
|
||||||
export interface NumericStateCondition {
|
export interface NumericStateCondition {
|
||||||
condition: "numeric_state";
|
condition: "numeric_state";
|
||||||
|
alias?: string;
|
||||||
entity_id: string;
|
entity_id: string;
|
||||||
attribute?: string;
|
attribute?: string;
|
||||||
above?: number;
|
above?: number;
|
||||||
@ -171,6 +174,7 @@ export interface NumericStateCondition {
|
|||||||
|
|
||||||
export interface SunCondition {
|
export interface SunCondition {
|
||||||
condition: "sun";
|
condition: "sun";
|
||||||
|
alias?: string;
|
||||||
after_offset: number;
|
after_offset: number;
|
||||||
before_offset: number;
|
before_offset: number;
|
||||||
after: "sunrise" | "sunset";
|
after: "sunrise" | "sunset";
|
||||||
@ -179,12 +183,14 @@ export interface SunCondition {
|
|||||||
|
|
||||||
export interface ZoneCondition {
|
export interface ZoneCondition {
|
||||||
condition: "zone";
|
condition: "zone";
|
||||||
|
alias?: string;
|
||||||
entity_id: string;
|
entity_id: string;
|
||||||
zone: string;
|
zone: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface TimeCondition {
|
export interface TimeCondition {
|
||||||
condition: "time";
|
condition: "time";
|
||||||
|
alias?: string;
|
||||||
after?: string;
|
after?: string;
|
||||||
before?: string;
|
before?: string;
|
||||||
weekday?: string | string[];
|
weekday?: string | string[];
|
||||||
@ -192,6 +198,7 @@ export interface TimeCondition {
|
|||||||
|
|
||||||
export interface TemplateCondition {
|
export interface TemplateCondition {
|
||||||
condition: "template";
|
condition: "template";
|
||||||
|
alias?: string;
|
||||||
value_template: string;
|
value_template: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
68
src/data/automation_debug.ts
Normal file
68
src/data/automation_debug.ts
Normal file
@ -0,0 +1,68 @@
|
|||||||
|
import { HomeAssistant, Context } from "../types";
|
||||||
|
import { AutomationConfig, Condition } from "./automation";
|
||||||
|
import { Action } from "./script";
|
||||||
|
|
||||||
|
interface TraceVariables extends Record<string, unknown> {
|
||||||
|
trigger: {
|
||||||
|
description: string;
|
||||||
|
[key: string]: unknown;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
interface BaseTrace {
|
||||||
|
timestamp: string;
|
||||||
|
changed_variables: Record<string, unknown>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface ConditionTrace extends BaseTrace {
|
||||||
|
result: { result: boolean };
|
||||||
|
}
|
||||||
|
|
||||||
|
export type ActionTrace = BaseTrace;
|
||||||
|
|
||||||
|
export interface AutomationTrace {
|
||||||
|
last_action: string | null;
|
||||||
|
last_condition: string | null;
|
||||||
|
run_id: string;
|
||||||
|
state: "running" | "stopped" | "debugged";
|
||||||
|
timestamp: {
|
||||||
|
start: string;
|
||||||
|
finish: string | null;
|
||||||
|
};
|
||||||
|
trigger: unknown;
|
||||||
|
unique_id: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface AutomationTraceExtended extends AutomationTrace {
|
||||||
|
condition_trace: Record<string, ConditionTrace[]>;
|
||||||
|
action_trace: Record<string, ActionTrace[]>;
|
||||||
|
context: Context;
|
||||||
|
variables: TraceVariables;
|
||||||
|
config: AutomationConfig;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const loadAutomationTrace = (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
automation_id: string,
|
||||||
|
run_id: string
|
||||||
|
): Promise<AutomationTraceExtended> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "automation/trace/get",
|
||||||
|
automation_id,
|
||||||
|
run_id,
|
||||||
|
});
|
||||||
|
|
||||||
|
export const loadAutomationTraces = (
|
||||||
|
hass: HomeAssistant
|
||||||
|
): Promise<AutomationTrace[]> =>
|
||||||
|
hass.callWS({
|
||||||
|
type: "automation/trace/list",
|
||||||
|
});
|
||||||
|
|
||||||
|
export const getConfigFromPath = <T extends Condition | Action>(
|
||||||
|
config: AutomationConfig,
|
||||||
|
path: string
|
||||||
|
): T => {
|
||||||
|
const parts = path.split("/");
|
||||||
|
return config[parts[0]][Number(parts[1])];
|
||||||
|
};
|
@ -3,6 +3,7 @@ import { HaFormSchema } from "../components/ha-form/ha-form";
|
|||||||
import { HomeAssistant } from "../types";
|
import { HomeAssistant } from "../types";
|
||||||
|
|
||||||
export interface DeviceAutomation {
|
export interface DeviceAutomation {
|
||||||
|
alias?: string;
|
||||||
device_id: string;
|
device_id: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
entity_id: string;
|
entity_id: string;
|
||||||
|
@ -14,7 +14,8 @@ export interface LogbookEntry {
|
|||||||
message?: string;
|
message?: string;
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
icon?: string;
|
icon?: string;
|
||||||
domain: string;
|
source?: string;
|
||||||
|
domain?: string;
|
||||||
context_user_id?: string;
|
context_user_id?: string;
|
||||||
context_event_type?: string;
|
context_event_type?: string;
|
||||||
context_domain?: string;
|
context_domain?: string;
|
||||||
@ -29,6 +30,20 @@ const DATA_CACHE: {
|
|||||||
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
|
[cacheKey: string]: { [entityId: string]: Promise<LogbookEntry[]> };
|
||||||
} = {};
|
} = {};
|
||||||
|
|
||||||
|
export const getLogbookDataForContext = async (
|
||||||
|
hass: HomeAssistant,
|
||||||
|
startDate: string,
|
||||||
|
contextId?: string
|
||||||
|
) =>
|
||||||
|
getLogbookDataFromServer(
|
||||||
|
hass,
|
||||||
|
startDate,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
undefined,
|
||||||
|
contextId
|
||||||
|
);
|
||||||
|
|
||||||
export const getLogbookData = async (
|
export const getLogbookData = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
startDate: string,
|
startDate: string,
|
||||||
@ -100,15 +115,30 @@ export const getLogbookDataCache = async (
|
|||||||
const getLogbookDataFromServer = async (
|
const getLogbookDataFromServer = async (
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
startDate: string,
|
startDate: string,
|
||||||
endDate: string,
|
endDate?: string,
|
||||||
entityId?: string,
|
entityId?: string,
|
||||||
entity_matches_only?: boolean
|
entitymatchesOnly?: boolean,
|
||||||
|
contextId?: string
|
||||||
) => {
|
) => {
|
||||||
const url = `logbook/${startDate}?end_time=${endDate}${
|
const params = new URLSearchParams();
|
||||||
entityId ? `&entity=${entityId}` : ""
|
|
||||||
}${entity_matches_only ? `&entity_matches_only` : ""}`;
|
|
||||||
|
|
||||||
return hass.callApi<LogbookEntry[]>("GET", url);
|
if (endDate) {
|
||||||
|
params.append("end_time", endDate);
|
||||||
|
}
|
||||||
|
if (entityId) {
|
||||||
|
params.append("entity", entityId);
|
||||||
|
}
|
||||||
|
if (entitymatchesOnly) {
|
||||||
|
params.append("entity_matches_only", "");
|
||||||
|
}
|
||||||
|
if (contextId) {
|
||||||
|
params.append("context_id", contextId);
|
||||||
|
}
|
||||||
|
|
||||||
|
return hass.callApi<LogbookEntry[]>(
|
||||||
|
"GET",
|
||||||
|
`logbook/${startDate}?${params.toString()}`
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const clearLogbookCache = (startDate: string, endDate: string) => {
|
export const clearLogbookCache = (startDate: string, endDate: string) => {
|
||||||
|
@ -29,12 +29,14 @@ export interface ScriptConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface EventAction {
|
export interface EventAction {
|
||||||
|
alias?: string;
|
||||||
event: string;
|
event: string;
|
||||||
event_data?: Record<string, any>;
|
event_data?: Record<string, any>;
|
||||||
event_data_template?: Record<string, any>;
|
event_data_template?: Record<string, any>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceAction {
|
export interface ServiceAction {
|
||||||
|
alias?: string;
|
||||||
service: string;
|
service: string;
|
||||||
entity_id?: string;
|
entity_id?: string;
|
||||||
target?: HassServiceTarget;
|
target?: HassServiceTarget;
|
||||||
@ -42,6 +44,7 @@ export interface ServiceAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface DeviceAction {
|
export interface DeviceAction {
|
||||||
|
alias?: string;
|
||||||
device_id: string;
|
device_id: string;
|
||||||
domain: string;
|
domain: string;
|
||||||
entity_id: string;
|
entity_id: string;
|
||||||
@ -55,26 +58,31 @@ export interface DelayActionParts {
|
|||||||
days?: number;
|
days?: number;
|
||||||
}
|
}
|
||||||
export interface DelayAction {
|
export interface DelayAction {
|
||||||
|
alias?: string;
|
||||||
delay: number | Partial<DelayActionParts> | string;
|
delay: number | Partial<DelayActionParts> | string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface SceneAction {
|
export interface SceneAction {
|
||||||
|
alias?: string;
|
||||||
scene: string;
|
scene: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WaitAction {
|
export interface WaitAction {
|
||||||
|
alias?: string;
|
||||||
wait_template: string;
|
wait_template: string;
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
continue_on_timeout?: boolean;
|
continue_on_timeout?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface WaitForTriggerAction {
|
export interface WaitForTriggerAction {
|
||||||
|
alias?: string;
|
||||||
wait_for_trigger: Trigger[];
|
wait_for_trigger: Trigger[];
|
||||||
timeout?: number;
|
timeout?: number;
|
||||||
continue_on_timeout?: boolean;
|
continue_on_timeout?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface RepeatAction {
|
export interface RepeatAction {
|
||||||
|
alias?: string;
|
||||||
repeat: CountRepeat | WhileRepeat | UntilRepeat;
|
repeat: CountRepeat | WhileRepeat | UntilRepeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,6 +103,7 @@ export interface UntilRepeat extends BaseRepeat {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface ChooseAction {
|
export interface ChooseAction {
|
||||||
|
alias?: string;
|
||||||
choose: [{ conditions: Condition[]; sequence: Action[] }];
|
choose: [{ conditions: Condition[]; sequence: Action[] }];
|
||||||
default?: Action[];
|
default?: Action[];
|
||||||
}
|
}
|
||||||
|
@ -9,8 +9,8 @@ import {
|
|||||||
RouterOptions,
|
RouterOptions,
|
||||||
} from "../../../layouts/hass-router-page";
|
} from "../../../layouts/hass-router-page";
|
||||||
import { HomeAssistant } from "../../../types";
|
import { HomeAssistant } from "../../../types";
|
||||||
import "./ha-automation-editor";
|
|
||||||
import "./ha-automation-picker";
|
import "./ha-automation-picker";
|
||||||
|
import "./ha-automation-editor";
|
||||||
|
|
||||||
const equal = (a: AutomationEntity[], b: AutomationEntity[]): boolean => {
|
const equal = (a: AutomationEntity[], b: AutomationEntity[]): boolean => {
|
||||||
if (a.length !== b.length) {
|
if (a.length !== b.length) {
|
||||||
@ -48,6 +48,10 @@ class HaConfigAutomation extends HassRouterPage {
|
|||||||
edit: {
|
edit: {
|
||||||
tag: "ha-automation-editor",
|
tag: "ha-automation-editor",
|
||||||
},
|
},
|
||||||
|
trace: {
|
||||||
|
tag: "ha-automation-trace",
|
||||||
|
load: () => import("./trace/ha-automation-trace"),
|
||||||
|
},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -81,7 +85,7 @@ class HaConfigAutomation extends HassRouterPage {
|
|||||||
|
|
||||||
if (
|
if (
|
||||||
(!changedProps || changedProps.has("route")) &&
|
(!changedProps || changedProps.has("route")) &&
|
||||||
this._currentPage === "edit"
|
this._currentPage !== "dashboard"
|
||||||
) {
|
) {
|
||||||
const automationId = this.routeTail.path.substr(1);
|
const automationId = this.routeTail.path.substr(1);
|
||||||
pageEl.automationId = automationId === "new" ? null : automationId;
|
pageEl.automationId = automationId === "new" ? null : automationId;
|
||||||
|
161
src/panels/config/automation/trace/ha-automation-trace.ts
Normal file
161
src/panels/config/automation/trace/ha-automation-trace.ts
Normal file
@ -0,0 +1,161 @@
|
|||||||
|
import {
|
||||||
|
css,
|
||||||
|
CSSResult,
|
||||||
|
customElement,
|
||||||
|
html,
|
||||||
|
internalProperty,
|
||||||
|
LitElement,
|
||||||
|
property,
|
||||||
|
TemplateResult,
|
||||||
|
} from "lit-element";
|
||||||
|
import { AutomationEntity } from "../../../../data/automation";
|
||||||
|
import {
|
||||||
|
AutomationTraceExtended,
|
||||||
|
loadAutomationTrace,
|
||||||
|
loadAutomationTraces,
|
||||||
|
} from "../../../../data/automation_debug";
|
||||||
|
import "../../../../components/ha-card";
|
||||||
|
import "../../../../components/trace/hat-trace";
|
||||||
|
import { haStyle } from "../../../../resources/styles";
|
||||||
|
import { HomeAssistant, Route } from "../../../../types";
|
||||||
|
import { configSections } from "../../ha-panel-config";
|
||||||
|
import {
|
||||||
|
getLogbookDataForContext,
|
||||||
|
LogbookEntry,
|
||||||
|
} from "../../../../data/logbook";
|
||||||
|
|
||||||
|
@customElement("ha-automation-trace")
|
||||||
|
export class HaAutomationTrace extends LitElement {
|
||||||
|
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||||
|
|
||||||
|
@property() public automationId!: string;
|
||||||
|
|
||||||
|
@property() public automations!: AutomationEntity[];
|
||||||
|
|
||||||
|
@property() public isWide?: boolean;
|
||||||
|
|
||||||
|
@property() public narrow!: boolean;
|
||||||
|
|
||||||
|
@property() public route!: Route;
|
||||||
|
|
||||||
|
@internalProperty() private _entityId?: string;
|
||||||
|
|
||||||
|
@internalProperty() private _trace?: AutomationTraceExtended;
|
||||||
|
|
||||||
|
@internalProperty() private _logbookEntries?: LogbookEntry[];
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
const stateObj = this._entityId
|
||||||
|
? this.hass.states[this._entityId]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
return html`
|
||||||
|
<hass-tabs-subpage
|
||||||
|
.hass=${this.hass}
|
||||||
|
.narrow=${this.narrow}
|
||||||
|
.route=${this.route}
|
||||||
|
.backCallback=${() => this._backTapped()}
|
||||||
|
.tabs=${configSections.automation}
|
||||||
|
>
|
||||||
|
<ha-card
|
||||||
|
.header=${`Trace for ${
|
||||||
|
stateObj?.attributes.friendly_name || this._entityId
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<button class="load-last" @click=${this._loadTrace}>
|
||||||
|
Load last trace
|
||||||
|
</button>
|
||||||
|
${this._trace
|
||||||
|
? html`
|
||||||
|
<div class="card-content">
|
||||||
|
<hat-trace
|
||||||
|
.hass=${this.hass}
|
||||||
|
.trace=${this._trace}
|
||||||
|
.logbookEntries=${this._logbookEntries}
|
||||||
|
></hat-trace>
|
||||||
|
</div>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
</ha-card>
|
||||||
|
</hass-tabs-subpage>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected updated(changedProps) {
|
||||||
|
super.updated(changedProps);
|
||||||
|
|
||||||
|
if (changedProps.has("automationId")) {
|
||||||
|
this._entityId = undefined;
|
||||||
|
this._trace = undefined;
|
||||||
|
this._logbookEntries = undefined;
|
||||||
|
this._loadTrace();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
changedProps.has("automations") &&
|
||||||
|
this.automationId &&
|
||||||
|
!this._entityId
|
||||||
|
) {
|
||||||
|
this._setEntityId();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async _loadTrace() {
|
||||||
|
const traces = await loadAutomationTraces(this.hass);
|
||||||
|
const automationTraces = traces[this.automationId];
|
||||||
|
|
||||||
|
if (!automationTraces || automationTraces.length === 0) {
|
||||||
|
// TODO no trace found.
|
||||||
|
alert("NO traces found");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const trace = await loadAutomationTrace(
|
||||||
|
this.hass,
|
||||||
|
this.automationId,
|
||||||
|
automationTraces[automationTraces.length - 1].run_id
|
||||||
|
);
|
||||||
|
this._logbookEntries = await getLogbookDataForContext(
|
||||||
|
this.hass,
|
||||||
|
trace.timestamp.start,
|
||||||
|
trace.context.id
|
||||||
|
);
|
||||||
|
|
||||||
|
this._trace = trace;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _setEntityId() {
|
||||||
|
const automation = this.automations.find(
|
||||||
|
(entity: AutomationEntity) => entity.attributes.id === this.automationId
|
||||||
|
);
|
||||||
|
this._entityId = automation?.entity_id;
|
||||||
|
}
|
||||||
|
|
||||||
|
private _backTapped(): void {
|
||||||
|
history.back();
|
||||||
|
}
|
||||||
|
|
||||||
|
static get styles(): CSSResult[] {
|
||||||
|
return [
|
||||||
|
haStyle,
|
||||||
|
css`
|
||||||
|
ha-card {
|
||||||
|
max-width: 800px;
|
||||||
|
margin: 24px auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.load-last {
|
||||||
|
position: absolute;
|
||||||
|
top: 8px;
|
||||||
|
right: 8px;
|
||||||
|
}
|
||||||
|
`,
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
declare global {
|
||||||
|
interface HTMLElementTagNameMap {
|
||||||
|
"ha-automation-trace": HaAutomationTrace;
|
||||||
|
}
|
||||||
|
}
|
@ -117,7 +117,10 @@ class HaLogbook extends LitElement {
|
|||||||
: undefined;
|
: undefined;
|
||||||
const item_username =
|
const item_username =
|
||||||
item.context_user_id && this.userIdToName[item.context_user_id];
|
item.context_user_id && this.userIdToName[item.context_user_id];
|
||||||
const domain = item.entity_id ? computeDomain(item.entity_id) : item.domain;
|
const domain = item.entity_id
|
||||||
|
? computeDomain(item.entity_id)
|
||||||
|
: // Domain is there if there is no entity ID.
|
||||||
|
item.domain!;
|
||||||
|
|
||||||
return html`
|
return html`
|
||||||
<div class="entry-container">
|
<div class="entry-container">
|
||||||
|
@ -168,7 +168,7 @@ export interface Resources {
|
|||||||
export interface Context {
|
export interface Context {
|
||||||
id: string;
|
id: string;
|
||||||
parent_id?: string;
|
parent_id?: string;
|
||||||
user_id?: string;
|
user_id?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ServiceCallResponse {
|
export interface ServiceCallResponse {
|
||||||
|
Loading…
x
Reference in New Issue
Block a user