Merge pull request #8795 from home-assistant/dev

This commit is contained in:
Paulus Schoutsen 2021-04-01 15:33:43 -07:00 committed by GitHub
commit c810e541ea
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
44 changed files with 1279 additions and 339 deletions

View File

@ -2,8 +2,7 @@ import { DemoTrace } from "./types";
export const basicTrace: DemoTrace = {
trace: {
last_action: "action/2",
last_condition: "condition/0",
last_step: "action/2",
run_id: "0",
state: "stopped",
timestamp: {
@ -14,6 +13,12 @@ export const basicTrace: DemoTrace = {
domain: "automation",
item_id: "1615419646544",
trace: {
"trigger/0": [
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
"condition/0": [
{
path: "condition/0",
@ -284,45 +289,7 @@ export const basicTrace: DemoTrace = {
parent_id: "664d6d261450a9ecea6738e97269a149",
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-24T19:03:59.141440+00:00",
last_updated: "2021-03-24T19:03:59.141440+00:00",
context: {
id: "5d0918eb379214d07554bdab6a08bcff",
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-25T04:36:51.220696+00:00",
last_updated: "2021-03-25T04:36:51.220696+00:00",
context: {
id: "664d6d261450a9ecea6738e97269a149",
parent_id: null,
user_id: "d1b4e89da01445fa8bc98e39fac477ca",
},
},
for: null,
attribute: null,
description: "state of input_boolean.toggle_1",
},
},
script_execution: "finished",
},
logbookEntries: [
{

View File

@ -0,0 +1,44 @@
import { LogbookEntry } from "../../../../src/data/logbook";
import { AutomationTraceExtended } from "../../../../src/data/trace";
import { DemoTrace } from "./types";
export const mockDemoTrace = (
tracePartial: Partial<AutomationTraceExtended>,
logbookEntries?: LogbookEntry[]
): DemoTrace => ({
trace: {
last_step: "",
run_id: "0",
state: "stopped",
timestamp: {
start: "2021-03-25T04:36:51.223693+00:00",
finish: "2021-03-25T04:36:51.266132+00:00",
},
trigger: "mocked trigger",
domain: "automation",
item_id: "1615419646544",
trace: {
"trigger/0": [
{
path: "trigger/0",
changed_variables: {
trigger: {
description: "mocked trigger",
},
},
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
},
config: {
trigger: [],
action: [],
},
context: {
id: "abcd",
},
script_execution: "finished",
...tracePartial,
},
logbookEntries: logbookEntries || [],
});

View File

@ -2,8 +2,7 @@ import { DemoTrace } from "./types";
export const motionLightTrace: DemoTrace = {
trace: {
last_action: "action/3",
last_condition: null,
last_step: "action/3",
run_id: "1",
state: "stopped",
timestamp: {
@ -14,6 +13,12 @@ export const motionLightTrace: DemoTrace = {
domain: "automation",
item_id: "1614732497392",
trace: {
"trigger/0": [
{
path: "trigger/0",
timestamp: "2021-03-25T04:36:51.223693+00:00",
},
],
"action/0": [
{
path: "action/0",
@ -171,45 +176,7 @@ export const motionLightTrace: DemoTrace = {
parent_id: "e22ddfd5f11dc4aad9a52fc10dab613b",
user_id: null,
},
variables: {
trigger: {
platform: "state",
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
from_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "off",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera-off",
},
last_changed: "2021-03-14T06:06:29.235325+00:00",
last_updated: "2021-03-14T06:06:29.235325+00:00",
context: {
id: "ad4864c5ce957c38a07b50378eeb245d",
parent_id: null,
user_id: null,
},
},
to_state: {
entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
state: "on",
attributes: {
friendly_name: "Pauluss MacBook Pro Camera In Use",
icon: "mdi:camera",
},
last_changed: "2021-03-14T06:07:01.762009+00:00",
last_updated: "2021-03-14T06:07:01.762009+00:00",
context: {
id: "e22ddfd5f11dc4aad9a52fc10dab613b",
parent_id: null,
user_id: null,
},
},
for: null,
attribute: null,
description: "state of binary_sensor.pauluss_macbook_pro_camera_in_use",
},
},
script_execution: "finished",
},
logbookEntries: [
{

View File

@ -0,0 +1,102 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeAction } from "../../../src/data/script_i18n";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
const actions = [
{ wait_template: "{{ true }}", alias: "Something with an alias" },
{ delay: "0:05" },
{ wait_template: "{{ true }}" },
{
condition: "template",
value_template: "{{ true }}",
},
{ event: "happy_event" },
{
device_id: "abcdefgh",
domain: "plex",
entity_id: "media_player.kitchen",
},
{ scene: "scene.kitchen_morning" },
{
wait_for_trigger: [
{
platform: "state",
entity_id: "input_boolean.toggle_1",
},
],
},
{
variables: {
hello: "world",
},
},
{
service: "input_boolean.toggle",
target: {
entity_id: "input_boolean.toggle_4",
},
},
];
@customElement("demo-automation-describe-action")
export class DemoAutomationDescribeAction extends LitElement {
@property({ attribute: false }) hass!: HomeAssistant;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<ha-card header="Actions">
${actions.map(
(conf) => html`
<div class="action">
<span>${describeAction(this.hass, conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.action {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-action": DemoAutomationDescribeAction;
}
}

View File

@ -0,0 +1,65 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeCondition } from "../../../src/data/automation_i18n";
const conditions = [
{ condition: "and" },
{ condition: "not" },
{ condition: "or" },
{ condition: "state" },
{ condition: "numeric_state" },
{ condition: "sun", after: "sunset" },
{ condition: "sun", after: "sunrise" },
{ condition: "zone" },
{ condition: "time" },
{ condition: "template" },
];
@customElement("demo-automation-describe-condition")
export class DemoAutomationDescribeCondition extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Conditions">
${conditions.map(
(conf) => html`
<div class="condition">
<span>${describeCondition(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.condition {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-condition": DemoAutomationDescribeCondition;
}
}

View File

@ -0,0 +1,68 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
css,
LitElement,
TemplateResult,
} from "lit-element";
import "../../../src/components/ha-card";
import { describeTrigger } from "../../../src/data/automation_i18n";
const triggers = [
{ platform: "state" },
{ platform: "mqtt" },
{ platform: "geo_location" },
{ platform: "homeassistant" },
{ platform: "numeric_state" },
{ platform: "sun" },
{ platform: "time_pattern" },
{ platform: "webhook" },
{ platform: "zone" },
{ platform: "tag" },
{ platform: "time" },
{ platform: "template" },
{ platform: "event" },
];
@customElement("demo-automation-describe-trigger")
export class DemoAutomationDescribeTrigger extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Triggers">
${triggers.map(
(conf) => html`
<div class="trigger">
<span>${describeTrigger(conf as any)}</span>
<pre>${safeDump(conf)}</pre>
</div>
`
)}
</ha-card>
`;
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px auto;
}
.trigger {
padding: 16px;
display: flex;
align-items: center;
justify-content: space-between;
}
span {
margin-right: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-describe-trigger": DemoAutomationDescribeTrigger;
}
}

View File

@ -0,0 +1,87 @@
import {
customElement,
html,
css,
LitElement,
TemplateResult,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
import { mockDemoTrace } from "../data/traces/mock-demo-trace";
import { DemoTrace } from "../data/traces/types";
const traces: DemoTrace[] = [
mockDemoTrace({ state: "running" }),
mockDemoTrace({ state: "debugged" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_condition" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_single" }),
mockDemoTrace({ state: "stopped", script_execution: "failed_max_runs" }),
mockDemoTrace({ state: "stopped", script_execution: "finished" }),
mockDemoTrace({ state: "stopped", script_execution: "aborted" }),
mockDemoTrace({
state: "stopped",
script_execution: "error",
error: 'Variable "beer" cannot be None',
}),
mockDemoTrace({ state: "stopped", script_execution: "cancelled" }),
];
@customElement("demo-automation-trace-timeline")
export class DemoAutomationTraceTimeline extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace) => html`
<ha-card .header=${trace.trace.config.alias}>
<div class="card-content">
<hat-trace-timeline
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
</ha-card>
`
)}
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
const hass = provideHass(this);
hass.updateTranslations(null, "en");
}
static get styles() {
return css`
ha-card {
max-width: 600px;
margin: 24px;
}
.card-content {
display: flex;
}
button {
position: absolute;
top: 0;
right: 0;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"demo-automation-trace-timeline": DemoAutomationTraceTimeline;
}
}

View File

@ -4,9 +4,11 @@ import {
css,
LitElement,
TemplateResult,
internalProperty,
property,
} from "lit-element";
import "../../../src/components/ha-card";
import "../../../src/components/trace/hat-script-graph";
import "../../../src/components/trace/hat-trace-timeline";
import { provideHass } from "../../../src/fake_data/provide_hass";
import { HomeAssistant } from "../../../src/types";
@ -20,20 +22,38 @@ const traces: DemoTrace[] = [basicTrace, motionLightTrace];
export class DemoAutomationTrace extends LitElement {
@property({ attribute: false }) hass?: HomeAssistant;
@internalProperty() private _selected = {};
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
${traces.map(
(trace) => html`
<ha-card .heading=${trace.trace.config.alias}>
(trace, idx) => html`
<ha-card .header=${trace.trace.config.alias}>
<div class="card-content">
<hat-script-graph
.trace=${trace.trace}
.selected=${this._selected[idx]}
@graph-node-selected=${(ev) => {
this._selected = { ...this._selected, [idx]: ev.detail.path };
}}
></hat-script-graph>
<hat-trace-timeline
allowPick
.hass=${this.hass}
.trace=${trace.trace}
.logbookEntries=${trace.logbookEntries}
.selectedPath=${this._selected[idx]}
@value-changed=${(ev) => {
this._selected = {
...this._selected,
[idx]: ev.detail.value,
};
}}
></hat-trace-timeline>
<button @click=${() => console.log(trace)}>Log trace</button>
</div>
</ha-card>
`
@ -53,6 +73,20 @@ export class DemoAutomationTrace extends LitElement {
max-width: 600px;
margin: 24px;
}
.card-content {
display: flex;
}
.card-content > * {
margin-right: 16px;
}
.card-content > *:last-child {
margin-right: 0;
}
button {
position: absolute;
top: 0;
right: 0;
}
`;
}
}

View File

@ -10,10 +10,10 @@ function patch(version) {
function today() {
const now = new Date();
return `${now.getFullYear()}${String(now.getMonth() + 1).padStart(
return `${now.getUTCFullYear()}${String(now.getUTCMonth() + 1).padStart(
2,
"0"
)}${String(now.getDate()).padStart(2, "0")}.0`;
)}${String(now.getUTCDate()).padStart(2, "0")}.0`;
}
function auto(version) {

View File

@ -2,7 +2,7 @@ from setuptools import setup, find_packages
setup(
name="home-assistant-frontend",
version="20210331.0",
version="20210402.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@ -1,6 +1,8 @@
export const ensureArray = (value?: any) => {
if (!value || Array.isArray(value)) {
export function ensureArray(value: undefined): undefined;
export function ensureArray<T>(value: T | T[]): T[];
export function ensureArray(value) {
if (value === undefined || Array.isArray(value)) {
return value;
}
return [value];
};
}

View File

@ -1,4 +1,5 @@
const isTemplateRegex = new RegExp("{%|{{|{#");
const isTemplateRegex = new RegExp("{%|{{");
export const isTemplate = (value: string): boolean =>
isTemplateRegex.test(value);
@ -11,7 +12,7 @@ export const hasTemplate = (value: unknown): boolean => {
}
if (typeof value === "object") {
const values = Array.isArray(value) ? value : Object.values(value!);
return values.some((val) => hasTemplate(val));
return values.some((val) => val && hasTemplate(val));
}
return false;
};

View File

@ -63,7 +63,7 @@ export interface DataTableSortColumnData {
}
export interface DataTableColumnData extends DataTableSortColumnData {
title: string;
title: TemplateResult | string;
type?: "numeric" | "icon" | "icon-button";
template?: <T>(data: any, row: T) => TemplateResult | string;
width?: string;
@ -74,7 +74,7 @@ export interface DataTableColumnData extends DataTableSortColumnData {
}
type ClonedDataTableColumnData = Omit<DataTableColumnData, "title"> & {
title?: string;
title?: TemplateResult | string;
};
export interface DataTableRowData {

View File

@ -125,35 +125,41 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
return html``;
}
return html`<div class="mdc-chip-set items">
${ensureArray(this.value?.area_id)?.map((area_id) => {
const area = this._areas![area_id];
return this._renderChip(
"area_id",
area_id,
area?.name || area_id,
undefined,
mdiSofa
);
})}
${ensureArray(this.value?.device_id)?.map((device_id) => {
const device = this._devices![device_id];
return this._renderChip(
"device_id",
device_id,
device ? computeDeviceName(device, this.hass) : device_id,
undefined,
mdiDevices
);
})}
${ensureArray(this.value?.entity_id)?.map((entity_id) => {
const entity = this.hass.states[entity_id];
return this._renderChip(
"entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity ? stateIcon(entity) : undefined
);
})}
${this.value?.area_id
? ensureArray(this.value.area_id).map((area_id) => {
const area = this._areas![area_id];
return this._renderChip(
"area_id",
area_id,
area?.name || area_id,
undefined,
mdiSofa
);
})
: ""}
${this.value?.device_id
? ensureArray(this.value.device_id).map((device_id) => {
const device = this._devices![device_id];
return this._renderChip(
"device_id",
device_id,
device ? computeDeviceName(device, this.hass) : device_id,
undefined,
mdiDevices
);
})
: ""}
${this.value?.entity_id
? ensureArray(this.value.entity_id).map((entity_id) => {
const entity = this.hass.states[entity_id];
return this._renderChip(
"entity_id",
entity_id,
entity ? computeStateName(entity) : entity_id,
entity ? stateIcon(entity) : undefined
);
})
: ""}
</div>
${this._renderPicker()}
<div class="mdc-chip-set">

View File

@ -48,6 +48,7 @@ import {
WaitAction,
WaitForTriggerAction,
} from "../../data/script";
import { ensureArray } from "../../common/ensure-array";
declare global {
interface HASSDomEvents {
@ -93,7 +94,7 @@ class HatScriptGraph extends LitElement {
const path = `condition/${i}`;
const trace = this.trace.trace[path] as ConditionTraceStep[] | undefined;
const track_path =
trace === undefined ? 0 : trace![0].result.result ? 1 : 2;
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
if (trace) {
this.trackedNodes[path] = { config, path };
}
@ -139,7 +140,7 @@ class HatScriptGraph extends LitElement {
private render_choose_node(config: ChooseAction, path: string) {
const trace = this.trace.trace[path] as ChooseActionTraceStep[] | undefined;
const trace_path = trace
const trace_path = trace?.[0].result
? trace[0].result.choice === "default"
? [config.choose.length]
: [trace[0].result.choice]
@ -173,7 +174,7 @@ class HatScriptGraph extends LitElement {
.iconPath=${mdiCheckBoxOutline}
nofocus
class=${classMap({
track: trace !== undefined && trace[0].result.choice === i,
track: trace !== undefined && trace[0].result?.choice === i,
})}
></hat-graph-node>
${branch.sequence.map((action, j) =>
@ -188,7 +189,7 @@ class HatScriptGraph extends LitElement {
nofocus
class=${classMap({
track:
trace !== undefined && trace[0].result.choice === "default",
trace !== undefined && trace[0].result?.choice === "default",
})}
></hat-graph-node>
${config.default?.map((action, i) =>
@ -200,8 +201,9 @@ class HatScriptGraph extends LitElement {
}
private render_condition_node(node: Condition, path: string) {
const trace: any = this.trace.trace[path];
const track_path = trace === undefined ? 0 : trace[0].result.result ? 1 : 2;
const trace = (this.trace.trace[path] as ConditionTraceStep[]) || undefined;
const track_path =
trace?.[0].result === undefined ? 0 : trace[0].result.result ? 1 : 2;
return html`
<hat-graph
branching
@ -218,7 +220,7 @@ class HatScriptGraph extends LitElement {
<hat-graph-node
slot="head"
class=${classMap({
track: trace,
track: Boolean(trace),
})}
.iconPath=${mdiAbTesting}
nofocus
@ -411,16 +413,14 @@ class HatScriptGraph extends LitElement {
const manual_triggered = this.trace && "trigger" in this.trace.trace;
let track_path = manual_triggered ? undefined : [0];
const trigger_nodes = (Array.isArray(this.trace.config.trigger)
? this.trace.config.trigger
: [this.trace.config.trigger]
).map((trigger, i) => {
if (this.trace && `trigger/${i}` in this.trace.trace) {
track_path = [i];
const trigger_nodes = ensureArray(this.trace.config.trigger).map(
(trigger, i) => {
if (this.trace && `trigger/${i}` in this.trace.trace) {
track_path = [i];
}
return this.render_trigger(trigger, i);
}
return this.render_trigger(trigger, i);
});
);
return html`
<hat-graph class="parent">
<div></div>
@ -434,16 +434,13 @@ class HatScriptGraph extends LitElement {
${trigger_nodes}
</hat-graph>
<hat-graph id="condition">
${(!this.trace.config.condition ||
Array.isArray(this.trace.config.condition)
? this.trace.config.condition
: [this.trace.config.condition]
)?.map((condition, i) => this.render_condition(condition, i))}
${ensureArray(this.trace.config.condition)?.map((condition, i) =>
this.render_condition(condition!, i)
)}
</hat-graph>
${(Array.isArray(this.trace.config.action)
? this.trace.config.action
: [this.trace.config.action]
).map((action, i) => this.render_node(action, `action/${i}`))}
${ensureArray(this.trace.config.action).map((action, i) =>
this.render_node(action, `action/${i}`)
)}
</hat-graph>
<div class="actions">
<mwc-icon-button

View File

@ -20,9 +20,11 @@ import { HomeAssistant } from "../../types";
import "./ha-timeline";
import type { HaTimeline } from "./ha-timeline";
import {
mdiAlertCircle,
mdiCircle,
mdiCircleOutline,
mdiPauseCircleOutline,
mdiProgressClock,
mdiProgressWrench,
mdiRecordCircleOutline,
} from "@mdi/js";
import { LogbookEntry } from "../../data/logbook";
@ -33,6 +35,8 @@ import {
} from "../../data/script";
import relativeTime from "../../common/datetime/relative_time";
import { fireEvent } from "../../common/dom/fire_event";
import { describeAction } from "../../data/script_i18n";
import { ifDefined } from "lit-html/directives/if-defined";
const LOGBOOK_ENTRIES_BEFORE_FOLD = 2;
@ -262,7 +266,7 @@ class ActionRenderer {
return this._handleChoose(index);
}
this._renderEntry(path, data.alias || actionType);
this._renderEntry(path, describeAction(this.hass, data, actionType));
return index + 1;
}
@ -272,7 +276,7 @@ class ActionRenderer {
`Triggered ${
triggerStep.path === "trigger"
? "manually"
: `by the ${triggerStep.changed_variables.trigger.description}`
: `by the ${this.trace.trigger}`
} at
${formatDateTimeWithSeconds(
new Date(triggerStep.timestamp),
@ -302,7 +306,7 @@ class ActionRenderer {
const startLevel = choosePath.split("/").length - 1;
const chooseTrace = this._getItem(index)[0] as ChooseActionTraceStep;
const defaultExecuted = chooseTrace.result.choice === "default";
const defaultExecuted = chooseTrace.result?.choice === "default";
const chooseConfig = this._getDataFromPath(
this.keys[index]
) as ChooseAction;
@ -312,11 +316,14 @@ class ActionRenderer {
this._renderEntry(choosePath, `${name}: Default action executed`);
} else {
const choiceConfig = this._getDataFromPath(
`${this.keys[index]}/choose/${chooseTrace.result.choice}`
) as ChooseActionChoice;
const choiceName =
choiceConfig.alias || `Choice ${chooseTrace.result.choice}`;
this._renderEntry(choosePath, `${name}: ${choiceName} executed`);
`${this.keys[index]}/choose/${chooseTrace.result?.choice}`
) as ChooseActionChoice | undefined;
const choiceName = choiceConfig
? `${
choiceConfig.alias || `Choice ${chooseTrace.result?.choice}`
} executed`
: `Error: ${chooseTrace.error}`;
this._renderEntry(choosePath, `${name}: ${choiceName}`);
}
let i;
@ -331,7 +338,10 @@ class ActionRenderer {
}
// We're going to skip all conditions
if (parts[startLevel + 3] === "sequence") {
if (
(defaultExecuted && parts[startLevel + 1] === "default") ||
(!defaultExecuted && parts[startLevel + 3] === "sequence")
) {
break;
}
}
@ -414,29 +424,92 @@ export class HaAutomationTracer extends LitElement {
logbookRenderer.flush();
// Render footer
const renderFinishedAt = () =>
formatDateTimeWithSeconds(
new Date(this.trace!.timestamp.finish!),
this.hass.locale
);
const renderRuntime = () => `(runtime:
${(
(new Date(this.trace!.timestamp.finish!).getTime() -
new Date(this.trace!.timestamp.start).getTime()) /
1000
).toFixed(2)}
seconds)`;
let entry: {
description: TemplateResult | string;
icon: string;
className?: string;
};
if (this.trace.state === "running") {
entry = {
description: "Still running",
icon: mdiProgressClock,
};
} else if (this.trace.state === "debugged") {
entry = {
description: "Debugged",
icon: mdiProgressWrench,
};
} else if (this.trace.script_execution === "finished") {
entry = {
description: `Finished at ${renderFinishedAt()} ${renderRuntime()}`,
icon: mdiCircle,
};
} else if (this.trace.script_execution === "aborted") {
entry = {
description: `Aborted at ${renderFinishedAt()} ${renderRuntime()}`,
icon: mdiAlertCircle,
};
} else if (this.trace.script_execution === "cancelled") {
entry = {
description: `Cancelled at ${renderFinishedAt()} ${renderRuntime()}`,
icon: mdiAlertCircle,
};
} else {
let reason: string;
let isError = false;
let extra: TemplateResult | undefined;
switch (this.trace.script_execution) {
case "failed_condition":
reason = "a condition failed";
break;
case "failed_single":
reason = "only a single execution is allowed";
break;
case "failed_max_runs":
reason = "maximum number of parallel runs reached";
break;
case "error":
reason = "an error was encountered";
isError = true;
extra = html`<br /><br />${this.trace.error!}`;
break;
default:
reason = `of unknown reason "${this.trace.script_execution}"`;
isError = true;
}
entry = {
description: html`Stopped because ${reason} at ${renderFinishedAt()}
${renderRuntime()}${extra || ""}`,
icon: mdiAlertCircle,
className: isError ? "error" : undefined,
};
}
// null means it was stopped by a condition
if (this.trace.last_action !== null) {
if (entry) {
entries.push(html`
<ha-timeline
lastItem
.icon=${this.trace.timestamp.finish
? mdiCircle
: mdiPauseCircleOutline}
.icon=${entry.icon}
class=${ifDefined(entry.className)}
>
${this.trace.timestamp.finish
? html`Finished at
${formatDateTimeWithSeconds(
new Date(this.trace.timestamp.finish),
this.hass.locale
)}
(runtime:
${(
(new Date(this.trace.timestamp.finish!).getTime() -
new Date(this.trace.timestamp.start).getTime()) /
1000
).toFixed(2)}
seconds)`
: "Still running"}
${entry.description}
</ha-timeline>
`);
}
@ -468,17 +541,20 @@ export class HaAutomationTracer extends LitElement {
this.shadowRoot!.querySelectorAll<HaTimeline>(
"ha-timeline[data-path]"
).forEach((el) => {
el.style.setProperty(
"--timeline-ball-color",
this.selectedPath === el.dataset.path ? "var(--primary-color)" : null
);
if (!this.allowPick || el.dataset.upgraded) {
el.toggleAttribute("selected", this.selectedPath === el.dataset.path);
if (!this.allowPick || el.tabIndex === 0) {
return;
}
el.dataset.upgraded = "1";
el.addEventListener("click", () => {
el.tabIndex = 0;
const selectEl = () => {
this.selectedPath = el.dataset.path;
fireEvent(this, "value-changed", { value: el.dataset.path });
};
el.addEventListener("click", selectEl);
el.addEventListener("keydown", (ev: KeyboardEvent) => {
if (ev.key === "Enter" || ev.key === " ") {
selectEl();
}
});
el.addEventListener("mouseover", () => {
el.raised = true;
@ -499,6 +575,17 @@ export class HaAutomationTracer extends LitElement {
ha-timeline[data-path] {
cursor: pointer;
}
ha-timeline[selected] {
--timeline-ball-color: var(--primary-color);
}
ha-timeline:focus {
outline: none;
--timeline-ball-color: var(--accent-color);
}
.error {
--timeline-ball-color: var(--error-color);
color: var(--error-color);
}
`,
];
}

View File

@ -238,6 +238,9 @@ export const deleteAutomation = (hass: HomeAssistant, id: string) =>
let inititialAutomationEditorData: Partial<AutomationConfig> | undefined;
export const getAutomationConfig = (hass: HomeAssistant, id: string) =>
hass.callApi<AutomationConfig>("GET", `config/automation/config/${id}`);
export const showAutomationEditor = (
el: HTMLElement,
data?: Partial<AutomationConfig>

View File

@ -0,0 +1,15 @@
import { Trigger, Condition } from "./automation";
export const describeTrigger = (trigger: Trigger) => {
return `${trigger.platform} trigger`;
};
export const describeCondition = (condition: Condition) => {
if (condition.alias) {
return condition.alias;
}
if (condition.condition === "template") {
return "Test a template";
}
return `${condition.condition} condition`;
};

View File

@ -45,7 +45,7 @@ export interface DataEntryFlowStepCreateEntry {
flow_id: string;
handler: string;
title: string;
result: ConfigEntry;
result?: ConfigEntry;
description: string;
description_placeholders: Record<string, string>;
}

View File

@ -37,7 +37,8 @@ export interface EventAction {
export interface ServiceAction {
alias?: string;
service: string;
service?: string;
service_template?: string;
entity_id?: string;
target?: HassServiceTarget;
data?: Record<string, any>;
@ -76,7 +77,7 @@ export interface WaitAction {
export interface WaitForTriggerAction {
alias?: string;
wait_for_trigger: Trigger[];
wait_for_trigger: Trigger | Trigger[];
timeout?: number;
continue_on_timeout?: boolean;
}
@ -115,6 +116,16 @@ export interface ChooseAction {
default?: Action[];
}
export interface VariablesAction {
alias?: string;
variables: Record<string, unknown>;
}
interface UnknownAction {
alias?: string;
[key: string]: unknown;
}
export type Action =
| EventAction
| DeviceAction
@ -125,7 +136,26 @@ export type Action =
| WaitAction
| WaitForTriggerAction
| RepeatAction
| ChooseAction;
| ChooseAction
| VariablesAction
| UnknownAction;
export interface ActionTypes {
delay: DelayAction;
wait_template: WaitAction;
check_condition: Condition;
fire_event: EventAction;
device_action: DeviceAction;
activate_scene: SceneAction;
repeat: RepeatAction;
choose: ChooseAction;
wait_for_trigger: WaitForTriggerAction;
variables: VariablesAction;
service: ServiceAction;
unknown: UnknownAction;
}
export type ActionType = keyof ActionTypes;
export const triggerScript = (
hass: HomeAssistant,
@ -166,7 +196,7 @@ export const getScriptEditorInitData = () => {
return data;
};
export const getActionType = (action: Action) => {
export const getActionType = (action: Action): ActionType => {
// Check based on config_validation.py#determine_script_action
if ("delay" in action) {
return "delay";

142
src/data/script_i18n.ts Normal file
View File

@ -0,0 +1,142 @@
import secondsToDuration from "../common/datetime/seconds_to_duration";
import { ensureArray } from "../common/ensure-array";
import { computeStateName } from "../common/entity/compute_state_name";
import { isTemplate } from "../common/string/has-template";
import { HomeAssistant } from "../types";
import { Condition } from "./automation";
import { describeCondition, describeTrigger } from "./automation_i18n";
import {
ActionType,
getActionType,
DelayAction,
SceneAction,
WaitForTriggerAction,
ActionTypes,
VariablesAction,
EventAction,
} from "./script";
export const describeAction = <T extends ActionType>(
hass: HomeAssistant,
action: ActionTypes[T],
actionType?: T
): string => {
if (action.alias) {
return action.alias;
}
if (!actionType) {
actionType = getActionType(action) as T;
}
if (actionType === "service") {
const config = action as ActionTypes["service"];
let base: string | undefined;
if (
config.service_template ||
(config.service && isTemplate(config.service))
) {
base = "Call a service based on a template";
} else if (config.service) {
base = `Call service ${config.service}`;
} else {
return actionType;
}
if (config.target) {
const targets: string[] = [];
for (const [key, label] of Object.entries({
area_id: "areas",
device_id: "devices",
entity_id: "entities",
})) {
if (!(key in config.target)) {
continue;
}
const keyConf: string[] = Array.isArray(config.target[key])
? config.target[key]
: [config.target[key]];
const values: string[] = [];
let renderValues = true;
for (const targetThing of keyConf) {
if (isTemplate(targetThing)) {
targets.push(`templated ${label}`);
renderValues = false;
break;
} else {
values.push(targetThing);
}
}
if (renderValues) {
targets.push(`${label} ${values.join(", ")}`);
}
}
if (targets.length > 0) {
base += ` on ${targets.join(", ")}`;
}
}
return base;
}
if (actionType === "delay") {
const config = action as DelayAction;
let duration: string;
if (typeof config.delay === "number") {
duration = `for ${secondsToDuration(config.delay)!}`;
} else if (typeof config.delay === "string") {
duration = isTemplate(config.delay)
? "based on a template"
: `for ${config.delay}`;
} else {
duration = `for ${JSON.stringify(config.delay)}`;
}
return `Delay ${duration}`;
}
if (actionType === "activate_scene") {
const config = action as SceneAction;
const sceneStateObj = hass.states[config.scene];
return `Activate scene ${
sceneStateObj ? computeStateName(sceneStateObj) : config.scene
}`;
}
if (actionType === "wait_for_trigger") {
const config = action as WaitForTriggerAction;
return `Wait for ${ensureArray(config.wait_for_trigger)
.map((trigger) => describeTrigger(trigger))
.join(", ")}`;
}
if (actionType === "variables") {
const config = action as VariablesAction;
return `Define variables ${Object.keys(config.variables).join(", ")}`;
}
if (actionType === "fire_event") {
const config = action as EventAction;
if (isTemplate(config.event)) {
return "Fire event based on a template";
}
return `Fire event ${config.event}`;
}
if (actionType === "wait_template") {
return "Wait for a template to render true";
}
if (actionType === "check_condition") {
return `Test ${describeCondition(action as Condition)}`;
}
return actionType;
};

View File

@ -1,10 +1,14 @@
import { strStartsWith } from "../common/string/starts-with";
import { HomeAssistant, Context } from "../types";
import { AutomationConfig } from "./automation";
import {
BlueprintAutomationConfig,
ManualAutomationConfig,
} from "./automation";
interface BaseTraceStep {
path: string;
timestamp: string;
error?: string;
changed_variables?: Record<string, unknown>;
}
@ -19,11 +23,11 @@ export interface TriggerTraceStep extends BaseTraceStep {
}
export interface ConditionTraceStep extends BaseTraceStep {
result: { result: boolean };
result?: { result: boolean };
}
export interface CallServiceActionTraceStep extends BaseTraceStep {
result: {
result?: {
limit: number;
running_script: boolean;
params: Record<string, unknown>;
@ -36,11 +40,11 @@ export interface CallServiceActionTraceStep extends BaseTraceStep {
}
export interface ChooseActionTraceStep extends BaseTraceStep {
result: { choice: number | "default" };
result?: { choice: number | "default" };
}
export interface ChooseChoiceActionTraceStep extends BaseTraceStep {
result: { result: boolean };
result?: { result: boolean };
}
export type ActionTraceStep =
@ -53,22 +57,41 @@ export type ActionTraceStep =
export interface AutomationTrace {
domain: string;
item_id: string;
last_action: string | null;
last_condition: string | null;
last_step: string | null;
run_id: string;
state: "running" | "stopped" | "debugged";
timestamp: {
start: string;
finish: string | null;
};
trigger: unknown;
script_execution:
| // The script was not executed because the automation's condition failed
"failed_condition"
// The script was not executed because the run mode is single
| "failed_single"
// The script was not executed because max parallel runs would be exceeded
| "failed_max_runs"
// All script steps finished:
| "finished"
// Script execution stopped by the script itself because a condition fails, wait_for_trigger timeouts etc:
| "aborted"
// Details about failing condition, timeout etc. is in the last element of the trace
// Script execution stops because of an unexpected exception:
| "error"
// The exception is in the trace itself or in the last element of the trace
// Script execution stopped by async_stop called on the script run because home assistant is shutting down, script mode is SCRIPT_MODE_RESTART etc:
| "cancelled"
| string;
// Automation only, should become it's own type when we support script in frontend
trigger: string;
}
export interface AutomationTraceExtended extends AutomationTrace {
trace: Record<string, ActionTraceStep[]>;
context: Context;
variables: Record<string, unknown>;
config: AutomationConfig;
config: ManualAutomationConfig;
blueprint_inputs?: BlueprintAutomationConfig;
error?: string;
}
interface TraceTypes {
@ -119,7 +142,7 @@ export const loadTraceContexts = (
});
export const getDataFromPath = (
config: AutomationConfig,
config: ManualAutomationConfig,
path: string
): any => {
const parts = path.split("/").reverse();

View File

@ -314,7 +314,7 @@ class DataEntryFlowDialog extends LitElement {
this._step &&
this._step.type === "create_entry"
) {
if (this._params!.flowConfig.loadDevicesAndAreas) {
if (this._step.result && this._params!.flowConfig.loadDevicesAndAreas) {
this._fetchDevices(this._step.result.entry_id);
this._fetchAreas();
} else {

View File

@ -43,7 +43,7 @@ class StepFlowCreateEntry extends LitElement {
<h2>Success!</h2>
<div class="content">
${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)}
${this.step.result.state === "not_loaded"
${this.step.result?.state === "not_loaded"
? html`<span class="error"
>${localize(
"ui.panel.config.integrations.config_flow.not_loaded"

View File

@ -36,6 +36,7 @@ import {
AutomationConfig,
AutomationEntity,
deleteAutomation,
getAutomationConfig,
getAutomationEditorInitData,
showAutomationEditor,
triggerAutomationActions,
@ -303,39 +304,7 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
oldAutomationId !== this.automationId
) {
this._setEntityId();
this.hass
.callApi<AutomationConfig>(
"GET",
`config/automation/config/${this.automationId}`
)
.then(
(config) => {
// Normalize data: ensure trigger, action and condition are lists
// Happens when people copy paste their automations into the config
for (const key of ["trigger", "condition", "action"]) {
const value = config[key];
if (value && !Array.isArray(value)) {
config[key] = [value];
}
}
this._dirty = false;
this._config = config;
},
(resp) => {
showAlertDialog(this, {
text:
resp.status_code === 404
? this.hass.localize(
"ui.panel.config.automation.editor.load_error_not_editable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.load_error_unknown",
"err_no",
resp.status_code
),
}).then(() => history.back());
}
);
this._loadConfig();
}
if (changedProps.has("automationId") && !this.automationId && this.hass) {
@ -378,6 +347,36 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) {
this._entityId = automation?.entity_id;
}
private async _loadConfig() {
try {
const config = await getAutomationConfig(this.hass, this.automationId);
// Normalize data: ensure trigger, action and condition are lists
// Happens when people copy paste their automations into the config
for (const key of ["trigger", "condition", "action"]) {
const value = config[key];
if (value && !Array.isArray(value)) {
config[key] = [value];
}
}
this._dirty = false;
this._config = config;
} catch (err) {
showAlertDialog(this, {
text:
err.status_code === 404
? this.hass.localize(
"ui.panel.config.automation.editor.load_error_not_editable"
)
: this.hass.localize(
"ui.panel.config.automation.editor.load_error_unknown",
"err_no",
err.status_code
),
}).then(() => history.back());
}
}
private _valueChanged(ev: CustomEvent<{ value: AutomationConfig }>) {
ev.stopPropagation();
this._config = ev.detail.value;

View File

@ -1,5 +1,12 @@
import "@material/mwc-icon-button";
import { mdiHelpCircle, mdiPlus } from "@mdi/js";
import {
mdiHelpCircle,
mdiHistory,
mdiInformationOutline,
mdiPencil,
mdiPencilOff,
mdiPlus,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import {
CSSResult,
@ -70,6 +77,7 @@ class HaAutomationPicker extends LitElement {
return {
...automation,
name: computeStateName(automation),
last_triggered: automation.attributes.last_triggered || undefined,
};
});
}
@ -97,23 +105,41 @@ class HaAutomationPicker extends LitElement {
filterable: true,
direction: "asc",
grows: true,
template: (name, automation: any) => html`
${name}
<div class="secondary">
${this.hass.localize("ui.card.automation.last_triggered")}:
${automation.attributes.last_triggered
? formatDateTime(
new Date(automation.attributes.last_triggered),
this.hass.locale
)
: this.hass.localize("ui.components.relative_time.never")}
</div>
`,
template: narrow
? (name, automation: any) =>
html`
${name}
<div class="secondary">
${this.hass.localize("ui.card.automation.last_triggered")}:
${automation.attributes.last_triggered
? formatDateTime(
new Date(automation.attributes.last_triggered),
this.hass.locale
)
: this.hass.localize("ui.components.relative_time.never")}
</div>
`
: undefined,
},
};
if (!narrow) {
columns.last_triggered = {
sortable: true,
width: "20%",
title: this.hass.localize("ui.card.automation.last_triggered"),
template: (last_triggered) => html`
${last_triggered
? formatDateTime(new Date(last_triggered), this.hass.locale)
: this.hass.localize("ui.components.relative_time.never")}
`,
};
columns.trigger = {
title: "",
title: html`
<mwc-button style="visibility: hidden">
${this.hass.localize("ui.card.automation.trigger")}
</mwc-button>
`,
width: "20%",
template: (_info, automation: any) => html`
<mwc-button
.automation=${automation}
@ -129,14 +155,15 @@ class HaAutomationPicker extends LitElement {
title: "",
type: "icon-button",
template: (_info, automation) => html`
<ha-icon-button
<mwc-icon-button
.automation=${automation}
@click=${this._showInfo}
icon="hass:information-outline"
title="${this.hass.localize(
.label="${this.hass.localize(
"ui.panel.config.automation.picker.show_info_automation"
)}"
></ha-icon-button>
>
<ha-svg-icon .path=${mdiInformationOutline}></ha-svg-icon>
</mwc-icon-button>
`,
};
columns.trace = {
@ -150,13 +177,14 @@ class HaAutomationPicker extends LitElement {
: undefined
)}
>
<ha-icon-button
icon="hass:graph-outline"
.disabled=${!automation.attributes.id}
title="${this.hass.localize(
<mwc-icon-button
.label=${this.hass.localize(
"ui.panel.config.automation.picker.dev_automation"
)}"
></ha-icon-button>
)}
.disabled=${!automation.attributes.id}
>
<ha-svg-icon .path=${mdiHistory}></ha-svg-icon>
</mwc-icon-button>
</a>
${!automation.attributes.id
? html`
@ -180,25 +208,26 @@ class HaAutomationPicker extends LitElement {
: undefined
)}
>
<ha-icon-button
.icon=${automation.attributes.id
? "hass:pencil"
: "hass:pencil-off"}
<mwc-icon-button
.disabled=${!automation.attributes.id}
title="${this.hass.localize(
.label="${this.hass.localize(
"ui.panel.config.automation.picker.edit_automation"
)}"
></ha-icon-button>
><ha-svg-icon .path=${
automation.attributes.id ? mdiPencil : mdiPencilOff
}></ha-svg-icon>
</a>
${!automation.attributes.id
? html`
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.automation.picker.only_editable"
)}
</paper-tooltip>
`
: ""}
${
!automation.attributes.id
? html`
<paper-tooltip animation-delay="0" position="left">
${this.hass.localize(
"ui.panel.config.automation.picker.only_editable"
)}
</paper-tooltip>
`
: ""
}
`,
};
return columns;

View File

@ -0,0 +1,34 @@
import { safeDump } from "js-yaml";
import {
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import "../../../../components/ha-icon-button";
import "../../../../components/ha-code-editor";
import { HomeAssistant } from "../../../../types";
import { AutomationTraceExtended } from "../../../../data/trace";
@customElement("ha-automation-trace-blueprint-config")
export class HaAutomationTraceBlueprintConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public trace!: AutomationTraceExtended;
protected render(): TemplateResult {
return html`
<ha-code-editor
.value=${safeDump(this.trace.blueprint_inputs || "").trimRight()}
readOnly
></ha-code-editor>
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-automation-trace-blueprint-config": HaAutomationTraceBlueprintConfig;
}
}

View File

@ -57,13 +57,13 @@ export class HaAutomationTracePathDetails extends LitElement {
["logbook", "Related logbook entries"],
].map(
([view, label]) => html`
<div
<button
.view=${view}
class=${classMap({ active: this._view === view })}
@click=${this._showTab}
>
${label}
</div>
</button>
`
)}
</div>

View File

@ -39,6 +39,7 @@ import {
mdiRefresh,
mdiDownload,
} from "@mdi/js";
import "./ha-automation-trace-blueprint-config";
@customElement("ha-automation-trace")
export class HaAutomationTrace extends LitElement {
@ -66,8 +67,12 @@ export class HaAutomationTrace extends LitElement {
@internalProperty() private _logbookEntries?: LogbookEntry[];
@internalProperty() private _view: "details" | "config" | "timeline" =
"details";
@internalProperty() private _view:
| "details"
| "config"
| "timeline"
| "logbook"
| "blueprint" = "details";
protected render(): TemplateResult {
const stateObj = this._entityId
@ -117,7 +122,7 @@ export class HaAutomationTrace extends LitElement {
class="linkButton"
href="/config/automation/edit/${this.automationId}"
>
<mwc-icon-button label="Edit Automation">
<mwc-icon-button label="Edit Automation" tabindex="-1">
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>
</a>
@ -181,18 +186,34 @@ export class HaAutomationTrace extends LitElement {
${[
["details", "Step Details"],
["timeline", "Trace Timeline"],
["logbook", "Related logbook entries"],
["config", "Automation Config"],
].map(
([view, label]) => html`
<div
<button
tabindex="0"
.view=${view}
class=${classMap({ active: this._view === view })}
@click=${this._showTab}
>
${label}
</div>
</button>
`
)}
${this._trace.blueprint_inputs
? html`
<button
tabindex="0"
.view=${"blueprint"}
class=${classMap({
active: this._view === "blueprint",
})}
@click=${this._showTab}
>
Blueprint Config
</div>
`
: ""}
</div>
${this._selected === undefined ||
this._logbookEntries === undefined ||
@ -216,6 +237,20 @@ export class HaAutomationTrace extends LitElement {
.trace=${this._trace}
></ha-automation-trace-config>
`
: this._view === "logbook"
? html`
<ha-logbook
.hass=${this.hass}
.entries=${this._logbookEntries}
></ha-logbook>
`
: this._view === "blueprint"
? html`
<ha-automation-trace-blueprint-config
.hass=${this.hass}
.trace=${this._trace}
></ha-automation-trace-blueprint-config>
`
: html`
<ha-automation-trace-timeline
.hass=${this.hass}

View File

@ -18,8 +18,10 @@ export const traceTabStyles = css`
cursor: pointer;
position: relative;
bottom: -1px;
border: none;
border-bottom: 2px solid transparent;
user-select: none;
background: none;
}
.tabs > *.active {

View File

@ -66,14 +66,15 @@ class HaScriptPicker extends LitElement {
...script,
name: computeStateName(script),
icon: stateIcon(script),
last_triggered: script.attributes.last_triggered || undefined,
};
});
}
);
private _columns = memoizeOne(
(_language): DataTableColumnContainer => {
return {
(narrow, _locale): DataTableColumnContainer => {
const columns: DataTableColumnContainer = {
activate: {
title: "",
type: "icon-button",
@ -103,50 +104,65 @@ class HaScriptPicker extends LitElement {
filterable: true,
direction: "asc",
grows: true,
template: (name, script: any) => html`
${name}
<div class="secondary">
${this.hass.localize("ui.card.automation.last_triggered")}:
${script.attributes.last_triggered
? formatDateTime(
new Date(script.attributes.last_triggered),
this.hass.locale
)
: this.hass.localize("ui.components.relative_time.never")}
</div>
`,
},
info: {
title: "",
type: "icon-button",
template: (_info, script) => html`
<mwc-icon-button
.script=${script}
@click=${this._showInfo}
title="${this.hass.localize(
"ui.panel.config.script.picker.show_info"
)}"
>
<ha-svg-icon .path=${mdiInformationOutline}></ha-svg-icon>
</mwc-icon-button>
`,
},
edit: {
title: "",
type: "icon-button",
template: (_info, script: any) => html`
<a href="/config/script/edit/${script.entity_id}">
<mwc-icon-button
title="${this.hass.localize(
"ui.panel.config.script.picker.edit_script"
)}"
>
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>
</a>
`,
template: narrow
? (name, script: any) => html`
${name}
<div class="secondary">
${this.hass.localize("ui.card.automation.last_triggered")}:
${script.attributes.last_triggered
? formatDateTime(
new Date(script.attributes.last_triggered),
this.hass.locale
)
: this.hass.localize("ui.components.relative_time.never")}
</div>
`
: undefined,
},
};
if (!narrow) {
columns.last_triggered = {
sortable: true,
width: "20%",
title: this.hass.localize("ui.card.automation.last_triggered"),
template: (last_triggered) => html`
${last_triggered
? formatDateTime(new Date(last_triggered), this.hass.locale)
: this.hass.localize("ui.components.relative_time.never")}
`,
};
}
columns.info = {
title: "",
type: "icon-button",
template: (_info, script) => html`
<mwc-icon-button
.script=${script}
@click=${this._showInfo}
title="${this.hass.localize(
"ui.panel.config.script.picker.show_info"
)}"
>
<ha-svg-icon .path=${mdiInformationOutline}></ha-svg-icon>
</mwc-icon-button>
`,
};
columns.edit = {
title: "",
type: "icon-button",
template: (_info, script: any) => html`
<a href="/config/script/edit/${script.entity_id}">
<mwc-icon-button
title="${this.hass.localize(
"ui.panel.config.script.picker.edit_script"
)}"
>
<ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
</mwc-icon-button>
</a>
`,
};
return columns;
}
);
@ -158,7 +174,7 @@ class HaScriptPicker extends LitElement {
back-path="/config"
.route=${this.route}
.tabs=${configSections.automation}
.columns=${this._columns(this.hass.language)}
.columns=${this._columns(this.narrow, this.hass.locale)}
.data=${this._scripts(this.scripts, this._filteredScripts)}
.activeFilters=${this._activeFilters}
id="entity_id"

View File

@ -286,7 +286,7 @@ class HaPanelDevService extends LitElement {
}
private _checkUiSupported() {
if (hasTemplate(this._serviceData)) {
if (this._serviceData && hasTemplate(this._serviceData)) {
this._yamlMode = true;
this._uiAvailable = false;
} else {

View File

@ -259,7 +259,7 @@ class HaLogbook extends LitElement {
haStyle,
haStyleScrollbar,
css`
:host {
:host([virtualize]) {
display: block;
height: 100%;
}

View File

@ -1188,6 +1188,9 @@
"zha_device_card": {
"device_name_placeholder": "Změnit název zařízení"
}
},
"zha_reconfigure_device": {
"heading": "Znovunastavení zařízení"
}
},
"duration": {
@ -1691,6 +1694,7 @@
"info": "Díky integraci Google Assistant pro Home Assistant Cloud budete moci ovládat všechna zařízení v Home Assistant pomocí jakéhokoli zařízení podporujícího Google Assistant.",
"info_state_reporting": "Pokud povolíte hlášení stavu, Home Assistant bude posílat veškeré změny stavů všech exponovaných entit do Google. Toto vám umožní sledovat vždy aktuální stavy entit v aplikaci Google.",
"manage_entities": "Správa entit",
"not_configured_text": "Než budete moci používat Google Assistant, musíte v aplikaci Google Home aktivovat dovednost Home Assistant Cloud pro Google Assistant.",
"not_configured_title": "Google Asistent není aktivován",
"security_devices": "Zabezpečovací zařízení",
"sync_entities": "Synchronizovat entity s Google",
@ -1859,6 +1863,7 @@
"header": "Analytika",
"instance_id": "ID instance: {huuid}",
"introduction": "Sdílejte analytiku ze své instance. Tato data budou veřejně dostupná na {link}",
"learn_more": "Zjistěte více, jak budou vaše údaje zpracovány.",
"needs_base": "Aby byla tato možnost k dispozici, musíte povolit základní analytiku",
"preference": {
"base": {
@ -2070,6 +2075,9 @@
"filtering_by": "Filtrování podle",
"show": "Zobrazit"
},
"hassio": {
"button": "Nastavit"
},
"header": "Nastavení Home Assistant",
"helpers": {
"caption": "Pomocníci",
@ -2236,10 +2244,17 @@
"clear": "Zrušit",
"description": "Zobrazení logů Home Assistant",
"details": "Detaily protokolu ({level})",
"level": {
"critical": "KRITICKÉ",
"debug": "LADĚNÍ",
"error": "CHYBA",
"info": "INFO",
"warning": "VAROVÁNÍ"
},
"load_full_log": "Načíst úplný protokol Home Assistanta",
"loading_log": "Načítání protokolu chyb...",
"multiple_messages": "zpráva se poprvé objevila v {time} a zobrazuje se {counter} krát",
"no_errors": "Nebyly hlášeny žádné chyby.",
"no_errors": "Nebyly hlášeny žádné chyby",
"no_issues": "Nejsou žádné nové problémy!",
"refresh": "Obnovit"
},

View File

@ -1188,6 +1188,9 @@
"zha_device_card": {
"device_name_placeholder": "Change device name"
}
},
"zha_reconfigure_device": {
"heading": "Reconfiguring device"
}
},
"duration": {
@ -1859,7 +1862,7 @@
"documentation": "Before you enable this make sure you visit the analytics documentation page {link} to understand what you are sending and how it's stored.",
"header": "Analytics",
"instance_id": "Instance ID: {huuid}",
"introduction": "Share analytics from your instance. This data will be publiclly available at {link}",
"introduction": "Share analytics from your instance. This data will be publicly available at {link}",
"learn_more": "Learn more about how your data will be processed.",
"needs_base": "You need to enable base analytics for this option to be available",
"preference": {
@ -3746,7 +3749,7 @@
"page-onboarding": {
"analytics": {
"finish": "Next",
"intro": "Share analytics from your instance. This data will be publiclly available at {link}"
"intro": "Share analytics from your instance. This data will be publicly available at {link}"
},
"core-config": {
"button_detect": "Detect",

View File

@ -1187,6 +1187,9 @@
"zha_device_card": {
"device_name_placeholder": "Cambiar el nombre del dispositivo"
}
},
"zha_reconfigure_device": {
"heading": "Reconfigurando el dispositivo"
}
},
"duration": {
@ -1859,6 +1862,7 @@
"header": "Analítica",
"instance_id": "ID de instancia: {huuid}",
"introduction": "Comparte análisis de tu instancia. Estos datos estarán disponibles públicamente en {link}",
"learn_more": "Aprende más sobre cómo se procesarán tus datos.",
"needs_base": "Debes habilitar el análisis base para que esta opción esté disponible",
"preference": {
"base": {
@ -2070,6 +2074,9 @@
"filtering_by": "Filtrando por",
"show": "Mostrar"
},
"hassio": {
"button": "Configurar"
},
"header": "Configurar Home Assistant",
"helpers": {
"caption": "Ayudantes",
@ -2236,10 +2243,17 @@
"clear": "Limpiar",
"description": "Ve los registros de Home Assistant",
"details": "Detalles de registro ({level})",
"level": {
"critical": "CRÍTICO",
"debug": "DEPURACIÓN",
"error": "ERROR",
"info": "INFO",
"warning": "ADVERTENCIA"
},
"load_full_log": "Cargar registro completo de Home Assistant",
"loading_log": "Cargando registro de errores...",
"multiple_messages": "el mensaje se produjo por primera vez a las {time} y aparece {counter} veces",
"no_errors": "No se han reportado errores.",
"no_errors": "No se han reportado errores",
"no_issues": "¡No hay nuevos problemas!",
"refresh": "Actualizar"
},

View File

@ -1188,6 +1188,9 @@
"zha_device_card": {
"device_name_placeholder": "Muuda seadme nime"
}
},
"zha_reconfigure_device": {
"heading": "Seadme sätete muutmine"
}
},
"duration": {
@ -2072,6 +2075,9 @@
"filtering_by": "Filtreeri",
"show": "Kuva"
},
"hassio": {
"button": "Seadista"
},
"header": "Home Assistant'i seadistamine",
"helpers": {
"caption": "Abimehed",

View File

@ -440,6 +440,9 @@
"area-picker": {
"show_areas": "Amosar áreas"
},
"data-table": {
"clear": "Elim"
},
"device-picker": {
"show_devices": "Amosar dispositivos"
},
@ -457,6 +460,12 @@
"is_opening": "estase abrindo"
},
"show_trace": "Amosar rastro"
},
"related-filter-menu": {
"filter_by_area": "Filtrar por área",
"filter_by_device": "Filtrar por dispositivo",
"filtered_by_area": "área: {area_name}",
"filtered_by_device": "dispositivo: {device_name}"
}
},
"dialogs": {
@ -531,6 +540,7 @@
}
}
},
"show_trace": "Mostrar rastro",
"triggers": {
"type": {
"mqtt": {
@ -555,6 +565,12 @@
}
},
"cloud": {
"account": {
"google": {
"not_configured_text": "Antes de usar o Asistente de Google, debes activar a skill Home Assistant Cloud para Google Assistant na aplicación Google Home.",
"not_configured_title": "Google Assistant non está activado"
}
},
"forgot_password": {
"instructions": "Introduce o teu enderezo de correo electrónico e enviarémosche unha ligazón para restablecer o teu contrasinal."
},
@ -562,6 +578,41 @@
"email_address": "Enderezo electrónico"
}
},
"core": {
"section": {
"core": {
"analytics": {
"documentation": "Antes de habilitalo, asegúrese de visitar a páxina de documentación de análise {link} para comprender o que está a enviar e como se almacena.",
"header": "Analítica",
"instance_id": "ID de instancia: {huuid}",
"introduction": "Comparte analítica desde a túa instancia. Estes datos estarán dispoñibles publicamente en {link}",
"learn_more": "Máis información sobre como se procesarán os teus datos.",
"needs_base": "Debe habilitar a analítica base para que esta opción estea dispoñible",
"preference": {
"base": {
"description": "Isto inclúe o ID de instancia, a versión e o tipo de instalación",
"title": "Analítica básica"
},
"diagnostics": {
"description": "Comparte informes de fallos e información de diagnóstico",
"title": "Diagnóstico"
},
"statistics": {
"description": "Isto inclúe un reconto de elementos na súa instalación, para ver unha lista completa consulta a documentación",
"title": "Estatísticas de uso"
},
"usage_supervisor": {
"title": "Integracións e complementos usados"
},
"usage": {
"description": "Isto inclúe os nomes das túas integracións",
"title": "Integracións usadas"
}
}
}
}
}
},
"devices": {
"enabled_description": "Os dispositivos desactivados non se amosarán e as entidades que pertencen ao dispositivo desactivaranse e non se engadirán ao Asistente doméstico.",
"picker": {
@ -585,7 +636,15 @@
"show": "Amosar"
},
"integrations": {
"config_entry": {
"logs": "rexistros",
"not_loaded": "Non cargado, comproba o {logs_link}"
},
"config_flow": {
"not_loaded": "Non se puido cargar a integración. Tenta reiniciar Home Assistant."
},
"disable": {
"show": "Amosar",
"show_disabled": "Amosar as integracións desactivadas"
},
"ignore": {
@ -593,6 +652,15 @@
"show_ignored": "Amosar integracións ignoradas"
}
},
"logs": {
"level": {
"critical": "CRÍTICO",
"debug": "DEPURAR",
"error": "ERRO",
"info": "INFORMACIÓN",
"warning": "AVISO"
}
},
"lovelace": {
"dashboards": {
"detail": {
@ -853,9 +921,14 @@
"not_supported": "Esta redirección non é compatible coa túa instancia de Home Assistant. Comprobe a {link} para coñecer as redireccións compatibles e a versión na que se introduciron."
},
"page-onboarding": {
"analytics": {
"finish": "Seguinte"
},
"core-config": {
"intro_location": "Gustaríanos saber onde vives. Esta información axudará a amosar información e configurar automatismos baseados no sol. Estes datos nunca se comparten fóra da túa rede."
},
"finish": "Rematar",
"next": "Seguinte",
"restore": {
"show_log": "Amosar rexistro completo"
}
@ -863,6 +936,19 @@
"profile": {
"long_lived_access_tokens": {
"prompt_copy_token": "Copia o teu token de acceso. Non aparecerá de novo."
},
"number_format": {
"description": "Escolle como se formatean os números.",
"dropdown_label": "Formato de número",
"formats": {
"comma_decimal": "1,234,567.89",
"decimal_comma": "1.234.567,89",
"language": "Automático (usar a configuración do idioma)",
"none": "Ningunha",
"space_comma": "1 234 567,89",
"system": "Usa a configuración rexional do sistema"
},
"header": "Formato de número"
}
}
}

View File

@ -1860,6 +1860,7 @@
"header": "분석",
"instance_id": "인스턴스 ID: {huuid}",
"introduction": "인스턴스의 분석 내용을 공유합니다. 이 데이터는 {link}에서 공개적으로 사용할 수 있습니다",
"learn_more": "통계자료가 어떻게 처리되는지 알아보기.",
"needs_base": "이 옵션을 사용하려면 기본 분석을 활성화해야 합니다",
"preference": {
"base": {
@ -2237,6 +2238,13 @@
"clear": "지우기",
"description": "Home Assistant 로그 내역을 봅니다",
"details": "로그 상세정보 ({level})",
"level": {
"critical": "치명적오류",
"debug": "디버그",
"error": "오류",
"info": "정보",
"warning": "경고"
},
"load_full_log": "Home Assistant 로그 전부 불러오기",
"loading_log": "오류 로그를 읽는 중...",
"multiple_messages": "{time}에 처음 발생했으며, {counter}번 발생했습니다.",

View File

@ -1860,6 +1860,7 @@
"header": "Analytics",
"instance_id": "Forekomst-ID: {huuid}",
"introduction": "Del analyse fra forekomsten din. Disse dataene vil være offentlig tilgjengelige på {link}",
"learn_more": "Lær mer om hvordan dataene dine blir behandlet.",
"needs_base": "Du må aktivere basisanalyse for at dette alternativet skal være tilgjengelig",
"preference": {
"base": {
@ -2071,6 +2072,9 @@
"filtering_by": "Filtrering etter",
"show": "Vis"
},
"hassio": {
"button": "Konfigurer"
},
"header": "Konfigurer Home Assistant",
"helpers": {
"caption": "Hjelpere",
@ -2237,10 +2241,17 @@
"clear": "Tøm",
"description": "Vis Home Assistant loggene",
"details": "Loggdetaljer ({level})",
"level": {
"critical": "KRITISK",
"debug": "DEBUG",
"error": "FEIL",
"info": "INFO",
"warning": "ADVARSEL"
},
"load_full_log": "Last inn fullstendig Home Assistant logg",
"loading_log": "Laster inn feillogg ...",
"multiple_messages": "meldingen oppstod først ved {time} og vist {counter} ganger",
"no_errors": "Ingen feil er rapportert.",
"no_errors": "Ingen feil er rapportert",
"no_issues": "Det er ingen nye problemer!",
"refresh": "Oppdater"
},

View File

@ -1188,6 +1188,9 @@
"zha_device_card": {
"device_name_placeholder": "Wijzig apparaatnaam"
}
},
"zha_reconfigure_device": {
"heading": "Apparaat opnieuw configureren"
}
},
"duration": {
@ -1656,8 +1659,8 @@
},
"introduction": "Met de Blueprinteditor kunt je Blueprints maken en bewerken.",
"learn_more": "Meer informatie over Blueprints",
"share_blueprint": "Deel blauwdruk",
"share_blueprint_no_url": "Kan blauwdruk niet delen: geen bron-URL",
"share_blueprint": "Deel Blueprint",
"share_blueprint_no_url": "Kan Blueprint niet delen: geen bron-URL",
"use_blueprint": "Automatisering maken"
}
},
@ -1860,6 +1863,7 @@
"header": "Analytics",
"instance_id": "Instantie-ID: {huuid}",
"introduction": "Deel analyses vanuit uw instantie. Deze gegevens zijn openbaar beschikbaar op {link}",
"learn_more": "Lees meer over hoe uw gegevens worden verwerkt.",
"needs_base": "U moet basisanalyses inschakelen om deze optie beschikbaar te maken",
"preference": {
"base": {
@ -2071,6 +2075,9 @@
"filtering_by": "Filteren op",
"show": "Toon"
},
"hassio": {
"button": "Configureer"
},
"header": "Configureer Home Assistant",
"helpers": {
"caption": "Helpers",
@ -2237,6 +2244,13 @@
"clear": "Wis",
"description": "Home Assistant logboek bekijken",
"details": "Logboekdetails ({level})",
"level": {
"critical": "KRITISCH",
"debug": "DEBUG",
"error": "FOUT",
"info": "INFO",
"warning": "WAARSCHUWING"
},
"load_full_log": "Laad volledige Home Assistant logboek",
"loading_log": "Foutenlogboek laden ...",
"multiple_messages": "bericht kwam voor het eerst om {time} en verschijnt {counter} keer",

View File

@ -1188,6 +1188,9 @@
"zha_device_card": {
"device_name_placeholder": "Название"
}
},
"zha_reconfigure_device": {
"heading": "Перенастройка устройства"
}
},
"duration": {
@ -1859,7 +1862,8 @@
"documentation": "Прежде всего, посетите страницу документации по аналитике {link}, чтобы понять, что Вы будете отправлять и как это будет храниться.",
"header": "Аналитика",
"instance_id": "Идентификатор экземпляра Home Assistant: {huuid}",
"introduction": "Поделитесь аналитикой из Вашего Home Assistant. Эти данные будут доступны для всех по адресу {ссылка}.",
"introduction": "Поделитесь аналитикой из Вашего Home Assistant. Эти данные будут доступны для всех по адресу {link}.",
"learn_more": "Узнайте больше о том, как будут обрабатываться Ваши данные.",
"needs_base": "Включите базовую аналитику, чтобы эта опция была доступна",
"preference": {
"base": {
@ -2071,6 +2075,9 @@
"filtering_by": "Отфильтровано по принадлежности к",
"show": "Показать"
},
"hassio": {
"button": "Настроить"
},
"header": "Настройка Home Assistant",
"helpers": {
"caption": "Вспомогательное",
@ -2237,10 +2244,17 @@
"clear": "Очистить",
"description": "Журналы работы сервера",
"details": "Уровень: {level}",
"level": {
"critical": "КРИТИЧЕСКАЯ НЕИСПРАВНОСТЬ",
"debug": "ОТЛАДКА",
"error": "ОШИБКА",
"info": "ИНФОРМАЦИЯ",
"warning": "ПРЕДУПРЕЖДЕНИЕ"
},
"load_full_log": "Показать весь журнал",
"loading_log": "Загрузка журнала…",
"multiple_messages": "первое сообщение получено {time} и повторялось {counter} раз",
"no_errors": "Нет сообщений об ошибках.",
"no_errors": "Нет сообщений об ошибках",
"no_issues": "Нет сообщений о проблемах.",
"refresh": "Обновить"
},
@ -3735,7 +3749,7 @@
"page-onboarding": {
"analytics": {
"finish": "Далее",
"intro": "Поделитесь аналитикой из Вашего Home Assistant. Эти данные будут доступны для всех по адресу {ссылка}."
"intro": "Поделитесь аналитикой из Вашего Home Assistant. Эти данные будут доступны для всех по адресу {link}."
},
"core-config": {
"button_detect": "Заполнить",

View File

@ -1188,6 +1188,9 @@
"zha_device_card": {
"device_name_placeholder": "變更裝置名稱"
}
},
"zha_reconfigure_device": {
"heading": "重新設定裝置"
}
},
"duration": {
@ -1860,6 +1863,7 @@
"header": "分析資料",
"instance_id": "實例 ID{huuid}",
"introduction": "分享實例分析資料,資料將可透過 {link} 連結公開取得",
"learn_more": "詳細了解資料會如何處理。",
"needs_base": "需要開啟基本分析、方能使用此選項",
"preference": {
"base": {
@ -2071,6 +2075,9 @@
"filtering_by": "篩選",
"show": "顯示"
},
"hassio": {
"button": "設定"
},
"header": "設定 Home Assistant",
"helpers": {
"caption": "助手",
@ -2237,10 +2244,17 @@
"clear": "清除",
"description": "檢視 Home Assistant 日誌",
"details": "記錄詳細資料({level}",
"level": {
"critical": "緊急",
"debug": "除錯",
"error": "錯誤",
"info": "資訊",
"warning": "警告"
},
"load_full_log": "載入完整 Home Assistant 記錄",
"loading_log": "載入錯誤記錄中...",
"multiple_messages": "訊息首次出現於 {time}、共顯示 {counter} 次",
"no_errors": "未回報任何錯誤。",
"no_errors": "未回報任何錯誤",
"no_issues": "沒有新問題!",
"refresh": "更新"
},

View File

@ -1926,7 +1926,7 @@
"@codemirror/state" "^0.18.0"
"@codemirror/view" "^0.18.0"
"@codemirror/highlight@^0.18.0":
"@codemirror/highlight@^0.18.0", "@codemirror/highlight@^0.18.1":
version "0.18.3"
resolved "https://registry.yarnpkg.com/@codemirror/highlight/-/highlight-0.18.3.tgz#50e268630f113c322a2dc97c9f68d71934fffcb0"
integrity sha512-NmRmkmWl8ht6Y6Y39ghov84AMPCqhUPIH9fmILs2NaWxZFZf4jGCTzrULnmREGsTie+26+LbKUncIU+PBu1Qng==