Trace foundation (#8608)

This commit is contained in:
Paulus Schoutsen 2021-03-12 20:13:06 -08:00 committed by GitHub
parent dc3ee7c779
commit 7bd4eeb0df
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 807 additions and 47 deletions

View 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",
},
],
};

View 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[];
}

View 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;
}
}

View File

@ -81,4 +81,8 @@ class DemoMoreInfoLight extends LitElement {
}
}
customElements.define("demo-more-info-light", DemoMoreInfoLight);
declare global {
interface HTMLElementTagNameMap {
"demo-more-info-light": DemoMoreInfoLight;
}
}

View File

@ -111,29 +111,9 @@ class HaGallery extends PolymerElement {
</template>
</ha-card>
<ha-card header="More Info Demos">
<div class="card-content intro">
<p>
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]]">
<ha-card header="Other Demos">
<div class="card-content intro"></div>
<template is="dom-repeat" items="[[_restDemos]]">
<a href="#[[item]]">
<paper-item>
<paper-item-body>{{ item }}</paper-item-body>
@ -178,13 +158,9 @@ class HaGallery extends PolymerElement {
type: Array,
computed: "_computeLovelace(_demos)",
},
_moreInfoDemos: {
_restDemos: {
type: Array,
computed: "_computeMoreInfos(_demos)",
},
_utilDemos: {
type: Array,
computed: "_computeUtil(_demos)",
computed: "_computeRest(_demos)",
},
};
}
@ -237,12 +213,8 @@ class HaGallery extends PolymerElement {
return demos.filter((demo) => demo.includes("hui"));
}
_computeMoreInfos(demos) {
return demos.filter((demo) => demo.includes("more-info"));
}
_computeUtil(demos) {
return demos.filter((demo) => demo.includes("util"));
_computeRest(demos) {
return demos.filter((demo) => !demo.includes("hui"));
}
}

View 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;
}
}

View 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;
}
}

View File

@ -149,11 +149,13 @@ export type Trigger =
export interface LogicalCondition {
condition: "and" | "not" | "or";
alias?: string;
conditions: Condition[];
}
export interface StateCondition {
condition: "state";
alias?: string;
entity_id: string;
attribute?: string;
state: string | number;
@ -162,6 +164,7 @@ export interface StateCondition {
export interface NumericStateCondition {
condition: "numeric_state";
alias?: string;
entity_id: string;
attribute?: string;
above?: number;
@ -171,6 +174,7 @@ export interface NumericStateCondition {
export interface SunCondition {
condition: "sun";
alias?: string;
after_offset: number;
before_offset: number;
after: "sunrise" | "sunset";
@ -179,12 +183,14 @@ export interface SunCondition {
export interface ZoneCondition {
condition: "zone";
alias?: string;
entity_id: string;
zone: string;
}
export interface TimeCondition {
condition: "time";
alias?: string;
after?: string;
before?: string;
weekday?: string | string[];
@ -192,6 +198,7 @@ export interface TimeCondition {
export interface TemplateCondition {
condition: "template";
alias?: string;
value_template: string;
}

View 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])];
};

View File

@ -3,6 +3,7 @@ import { HaFormSchema } from "../components/ha-form/ha-form";
import { HomeAssistant } from "../types";
export interface DeviceAutomation {
alias?: string;
device_id: string;
domain: string;
entity_id: string;

View File

@ -14,7 +14,8 @@ export interface LogbookEntry {
message?: string;
entity_id?: string;
icon?: string;
domain: string;
source?: string;
domain?: string;
context_user_id?: string;
context_event_type?: string;
context_domain?: string;
@ -29,6 +30,20 @@ const DATA_CACHE: {
[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 (
hass: HomeAssistant,
startDate: string,
@ -100,15 +115,30 @@ export const getLogbookDataCache = async (
const getLogbookDataFromServer = async (
hass: HomeAssistant,
startDate: string,
endDate: string,
endDate?: string,
entityId?: string,
entity_matches_only?: boolean
entitymatchesOnly?: boolean,
contextId?: string
) => {
const url = `logbook/${startDate}?end_time=${endDate}${
entityId ? `&entity=${entityId}` : ""
}${entity_matches_only ? `&entity_matches_only` : ""}`;
const params = new URLSearchParams();
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) => {

View File

@ -29,12 +29,14 @@ export interface ScriptConfig {
}
export interface EventAction {
alias?: string;
event: string;
event_data?: Record<string, any>;
event_data_template?: Record<string, any>;
}
export interface ServiceAction {
alias?: string;
service: string;
entity_id?: string;
target?: HassServiceTarget;
@ -42,6 +44,7 @@ export interface ServiceAction {
}
export interface DeviceAction {
alias?: string;
device_id: string;
domain: string;
entity_id: string;
@ -55,26 +58,31 @@ export interface DelayActionParts {
days?: number;
}
export interface DelayAction {
alias?: string;
delay: number | Partial<DelayActionParts> | string;
}
export interface SceneAction {
alias?: string;
scene: string;
}
export interface WaitAction {
alias?: string;
wait_template: string;
timeout?: number;
continue_on_timeout?: boolean;
}
export interface WaitForTriggerAction {
alias?: string;
wait_for_trigger: Trigger[];
timeout?: number;
continue_on_timeout?: boolean;
}
export interface RepeatAction {
alias?: string;
repeat: CountRepeat | WhileRepeat | UntilRepeat;
}
@ -95,6 +103,7 @@ export interface UntilRepeat extends BaseRepeat {
}
export interface ChooseAction {
alias?: string;
choose: [{ conditions: Condition[]; sequence: Action[] }];
default?: Action[];
}

View File

@ -9,8 +9,8 @@ import {
RouterOptions,
} from "../../../layouts/hass-router-page";
import { HomeAssistant } from "../../../types";
import "./ha-automation-editor";
import "./ha-automation-picker";
import "./ha-automation-editor";
const equal = (a: AutomationEntity[], b: AutomationEntity[]): boolean => {
if (a.length !== b.length) {
@ -48,6 +48,10 @@ class HaConfigAutomation extends HassRouterPage {
edit: {
tag: "ha-automation-editor",
},
trace: {
tag: "ha-automation-trace",
load: () => import("./trace/ha-automation-trace"),
},
},
};
@ -81,7 +85,7 @@ class HaConfigAutomation extends HassRouterPage {
if (
(!changedProps || changedProps.has("route")) &&
this._currentPage === "edit"
this._currentPage !== "dashboard"
) {
const automationId = this.routeTail.path.substr(1);
pageEl.automationId = automationId === "new" ? null : automationId;

View 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;
}
}

View File

@ -117,7 +117,10 @@ class HaLogbook extends LitElement {
: undefined;
const item_username =
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`
<div class="entry-container">

View File

@ -168,7 +168,7 @@ export interface Resources {
export interface Context {
id: string;
parent_id?: string;
user_id?: string;
user_id?: string | null;
}
export interface ServiceCallResponse {