From 9b947ef734f745c5cb51998eb3b6185f73457664 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Wed, 31 Mar 2021 18:15:06 +0200 Subject: [PATCH 01/14] Add top level logbook entries tab (#8776) --- .../automation/trace/ha-automation-trace.ts | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/panels/config/automation/trace/ha-automation-trace.ts b/src/panels/config/automation/trace/ha-automation-trace.ts index f99503e3b5..13f9f51c2e 100644 --- a/src/panels/config/automation/trace/ha-automation-trace.ts +++ b/src/panels/config/automation/trace/ha-automation-trace.ts @@ -66,8 +66,11 @@ export class HaAutomationTrace extends LitElement { @internalProperty() private _logbookEntries?: LogbookEntry[]; - @internalProperty() private _view: "details" | "config" | "timeline" = - "details"; + @internalProperty() private _view: + | "details" + | "config" + | "timeline" + | "logbook" = "details"; protected render(): TemplateResult { const stateObj = this._entityId @@ -181,6 +184,7 @@ export class HaAutomationTrace extends LitElement { ${[ ["details", "Step Details"], ["timeline", "Trace Timeline"], + ["logbook", "Related logbook entries"], ["config", "Automation Config"], ].map( ([view, label]) => html` @@ -216,6 +220,13 @@ export class HaAutomationTrace extends LitElement { .trace=${this._trace} > ` + : this._view === "logbook" + ? html` + + ` : html` Date: Wed, 31 Mar 2021 18:35:30 +0200 Subject: [PATCH 02/14] Handle errors in trace (#8775) --- src/components/trace/hat-script-graph.ts | 15 ++++++++------- src/components/trace/hat-trace-timeline.ts | 15 +++++++++------ src/data/trace.ts | 9 +++++---- src/panels/logbook/ha-logbook.ts | 2 +- 4 files changed, 23 insertions(+), 18 deletions(-) diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts index 156be802a7..d347adad77 100644 --- a/src/components/trace/hat-script-graph.ts +++ b/src/components/trace/hat-script-graph.ts @@ -93,7 +93,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 +139,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 +173,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, })} > ${branch.sequence.map((action, j) => @@ -188,7 +188,7 @@ class HatScriptGraph extends LitElement { nofocus class=${classMap({ track: - trace !== undefined && trace[0].result.choice === "default", + trace !== undefined && trace[0].result?.choice === "default", })} > ${config.default?.map((action, i) => @@ -200,8 +200,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` ; } @@ -19,11 +20,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; @@ -36,11 +37,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 = diff --git a/src/panels/logbook/ha-logbook.ts b/src/panels/logbook/ha-logbook.ts index 1fed1769b3..cb77b90a92 100644 --- a/src/panels/logbook/ha-logbook.ts +++ b/src/panels/logbook/ha-logbook.ts @@ -259,7 +259,7 @@ class HaLogbook extends LitElement { haStyle, haStyleScrollbar, css` - :host { + :host([virtualize]) { display: block; height: 100%; } From 17b1f3e4655d4200320daa3d8e343067c5c2f17d Mon Sep 17 00:00:00 2001 From: GitHub Action Date: Thu, 1 Apr 2021 00:48:39 +0000 Subject: [PATCH 03/14] Translation update --- translations/frontend/cs.json | 17 +++++- translations/frontend/en.json | 7 ++- translations/frontend/es.json | 16 +++++- translations/frontend/et.json | 6 +++ translations/frontend/gl.json | 86 ++++++++++++++++++++++++++++++ translations/frontend/ko.json | 8 +++ translations/frontend/nb.json | 13 ++++- translations/frontend/nl.json | 18 ++++++- translations/frontend/ru.json | 20 +++++-- translations/frontend/zh-Hant.json | 16 +++++- 10 files changed, 196 insertions(+), 11 deletions(-) diff --git a/translations/frontend/cs.json b/translations/frontend/cs.json index a1c2cc7549..6d7628b2a8 100644 --- a/translations/frontend/cs.json +++ b/translations/frontend/cs.json @@ -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" }, diff --git a/translations/frontend/en.json b/translations/frontend/en.json index 619f545005..ef4f9e358f 100644 --- a/translations/frontend/en.json +++ b/translations/frontend/en.json @@ -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", diff --git a/translations/frontend/es.json b/translations/frontend/es.json index 4526a4970b..68db774d39 100644 --- a/translations/frontend/es.json +++ b/translations/frontend/es.json @@ -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" }, diff --git a/translations/frontend/et.json b/translations/frontend/et.json index ec456424e6..b2496473a8 100644 --- a/translations/frontend/et.json +++ b/translations/frontend/et.json @@ -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", diff --git a/translations/frontend/gl.json b/translations/frontend/gl.json index fd87ea9f38..37e9541e15 100644 --- a/translations/frontend/gl.json +++ b/translations/frontend/gl.json @@ -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" } } } diff --git a/translations/frontend/ko.json b/translations/frontend/ko.json index eb41cd5fc8..f0196ebb02 100644 --- a/translations/frontend/ko.json +++ b/translations/frontend/ko.json @@ -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}번 발생했습니다.", diff --git a/translations/frontend/nb.json b/translations/frontend/nb.json index 765e9e2712..04ac429deb 100644 --- a/translations/frontend/nb.json +++ b/translations/frontend/nb.json @@ -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" }, diff --git a/translations/frontend/nl.json b/translations/frontend/nl.json index c817166558..3f0eedc383 100644 --- a/translations/frontend/nl.json +++ b/translations/frontend/nl.json @@ -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", diff --git a/translations/frontend/ru.json b/translations/frontend/ru.json index 5cb5729aef..c52247f1c8 100644 --- a/translations/frontend/ru.json +++ b/translations/frontend/ru.json @@ -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": "Заполнить", diff --git a/translations/frontend/zh-Hant.json b/translations/frontend/zh-Hant.json index 50fe0070fb..7356aea22a 100644 --- a/translations/frontend/zh-Hant.json +++ b/translations/frontend/zh-Hant.json @@ -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": "更新" }, From 5c1604e95989a6e8b601dc04a3a075ea67cb3ac0 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Apr 2021 01:28:37 -0700 Subject: [PATCH 04/14] Fix showing choose actions if default path chosen and other things (#8779) --- .../demos/demo-automation-describe-action.ts | 102 +++++++++++++ .../demo-automation-describe-condition.ts | 65 ++++++++ .../demos/demo-automation-describe-trigger.ts | 68 +++++++++ gallery/src/demos/demo-automation-trace.ts | 38 ++++- src/components/trace/hat-trace-timeline.ts | 8 +- src/data/automation_i18n.ts | 15 ++ src/data/script.ts | 36 ++++- src/data/script_i18n.ts | 141 ++++++++++++++++++ src/data/template.ts | 1 + 9 files changed, 467 insertions(+), 7 deletions(-) create mode 100644 gallery/src/demos/demo-automation-describe-action.ts create mode 100644 gallery/src/demos/demo-automation-describe-condition.ts create mode 100644 gallery/src/demos/demo-automation-describe-trigger.ts create mode 100644 src/data/automation_i18n.ts create mode 100644 src/data/script_i18n.ts create mode 100644 src/data/template.ts diff --git a/gallery/src/demos/demo-automation-describe-action.ts b/gallery/src/demos/demo-automation-describe-action.ts new file mode 100644 index 0000000000..642bcfb58c --- /dev/null +++ b/gallery/src/demos/demo-automation-describe-action.ts @@ -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` + + ${actions.map( + (conf) => html` +
+ ${describeAction(this.hass, conf as any)} +
${safeDump(conf)}
+
+ ` + )} +
+ `; + } + + 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; + } +} diff --git a/gallery/src/demos/demo-automation-describe-condition.ts b/gallery/src/demos/demo-automation-describe-condition.ts new file mode 100644 index 0000000000..ef57b9a1e5 --- /dev/null +++ b/gallery/src/demos/demo-automation-describe-condition.ts @@ -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` + + ${conditions.map( + (conf) => html` +
+ ${describeCondition(conf as any)} +
${safeDump(conf)}
+
+ ` + )} +
+ `; + } + + 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; + } +} diff --git a/gallery/src/demos/demo-automation-describe-trigger.ts b/gallery/src/demos/demo-automation-describe-trigger.ts new file mode 100644 index 0000000000..f870db44d7 --- /dev/null +++ b/gallery/src/demos/demo-automation-describe-trigger.ts @@ -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` + + ${triggers.map( + (conf) => html` +
+ ${describeTrigger(conf as any)} +
${safeDump(conf)}
+
+ ` + )} +
+ `; + } + + 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; + } +} diff --git a/gallery/src/demos/demo-automation-trace.ts b/gallery/src/demos/demo-automation-trace.ts index a7124ab527..8187c01630 100644 --- a/gallery/src/demos/demo-automation-trace.ts +++ b/gallery/src/demos/demo-automation-trace.ts @@ -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` - + (trace, idx) => html` +
+ { + this._selected = { ...this._selected, [idx]: ev.detail.path }; + }} + > { + this._selected = { + ...this._selected, + [idx]: ev.detail.value, + }; + }} > +
` @@ -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; + } `; } } diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index ad7ce559e2..ac1b957004 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -33,6 +33,7 @@ import { } from "../../data/script"; import relativeTime from "../../common/datetime/relative_time"; import { fireEvent } from "../../common/dom/fire_event"; +import { describeAction } from "../../data/script_i18n"; const LOGBOOK_ENTRIES_BEFORE_FOLD = 2; @@ -262,7 +263,7 @@ class ActionRenderer { return this._handleChoose(index); } - this._renderEntry(path, data.alias || actionType); + this._renderEntry(path, describeAction(this.hass, data, actionType)); return index + 1; } @@ -334,7 +335,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; } } diff --git a/src/data/automation_i18n.ts b/src/data/automation_i18n.ts new file mode 100644 index 0000000000..c4c9410a75 --- /dev/null +++ b/src/data/automation_i18n.ts @@ -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`; +}; diff --git a/src/data/script.ts b/src/data/script.ts index 5bc023b161..bad7b3650c 100644 --- a/src/data/script.ts +++ b/src/data/script.ts @@ -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; @@ -115,6 +116,16 @@ export interface ChooseAction { default?: Action[]; } +export interface VariablesAction { + alias?: string; + variables: Record; +} + +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"; diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts new file mode 100644 index 0000000000..d920987351 --- /dev/null +++ b/src/data/script_i18n.ts @@ -0,0 +1,141 @@ +import secondsToDuration from "../common/datetime/seconds_to_duration"; +import { computeStateName } from "../common/entity/compute_state_name"; +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"; +import { isDynamicTemplate } from "./template"; + +export const describeAction = ( + 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 && isDynamicTemplate(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 (isDynamicTemplate(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 = isDynamicTemplate(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 ${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 (isDynamicTemplate(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; +}; diff --git a/src/data/template.ts b/src/data/template.ts new file mode 100644 index 0000000000..452a36043a --- /dev/null +++ b/src/data/template.ts @@ -0,0 +1 @@ +export const isDynamicTemplate = (value: string) => value.includes("{{"); From 1fb3663398bb9424a412bd0b8f9c2102d8e61acc Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 1 Apr 2021 18:02:58 +0200 Subject: [PATCH 05/14] Add sortable last trigger column to automation and script overview (#8783) --- src/components/data-table/ha-data-table.ts | 4 +- .../config/automation/ha-automation-picker.ts | 45 +++++--- src/panels/config/script/ha-script-picker.ts | 106 ++++++++++-------- 3 files changed, 95 insertions(+), 60 deletions(-) diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 3230ede7f4..3477d61624 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -63,7 +63,7 @@ export interface DataTableSortColumnData { } export interface DataTableColumnData extends DataTableSortColumnData { - title: string; + title: TemplateResult | string; type?: "numeric" | "icon" | "icon-button"; template?: (data: any, row: T) => TemplateResult | string; width?: string; @@ -74,7 +74,7 @@ export interface DataTableColumnData extends DataTableSortColumnData { } type ClonedDataTableColumnData = Omit & { - title?: string; + title?: TemplateResult | string; }; export interface DataTableRowData { diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index 3dde3113f5..b12675425b 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -70,6 +70,7 @@ class HaAutomationPicker extends LitElement { return { ...automation, name: computeStateName(automation), + last_triggered: automation.attributes.last_triggered || undefined, }; }); } @@ -97,23 +98,41 @@ class HaAutomationPicker extends LitElement { filterable: true, direction: "asc", grows: true, - template: (name, automation: any) => html` - ${name} -
- ${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")} -
- `, + template: narrow + ? (name, automation: any) => + html` + ${name} +
+ ${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")} +
+ ` + : 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` + + ${this.hass.localize("ui.card.automation.trigger")} + + `, + width: "20%", template: (_info, automation: any) => html` { - 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} -
- ${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")} -
- `, - }, - info: { - title: "", - type: "icon-button", - template: (_info, script) => html` - - - - `, - }, - edit: { - title: "", - type: "icon-button", - template: (_info, script: any) => html` - - - - - - `, + template: narrow + ? (name, script: any) => html` + ${name} +
+ ${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")} +
+ ` + : 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` + + + + `, + }; + columns.edit = { + title: "", + type: "icon-button", + template: (_info, script: any) => html` + + + + + + `, + }; + 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" From b6f59d3c984aae2e7fafce4f2d31da76700cba0a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Apr 2021 11:28:56 -0700 Subject: [PATCH 06/14] Fix the automation picker icons (#8790) --- .../config/automation/ha-automation-picker.ts | 62 +++++++++++-------- 1 file changed, 36 insertions(+), 26 deletions(-) diff --git a/src/panels/config/automation/ha-automation-picker.ts b/src/panels/config/automation/ha-automation-picker.ts index b12675425b..f6d8834fa6 100644 --- a/src/panels/config/automation/ha-automation-picker.ts +++ b/src/panels/config/automation/ha-automation-picker.ts @@ -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, @@ -148,14 +155,15 @@ class HaAutomationPicker extends LitElement { title: "", type: "icon-button", template: (_info, automation) => html` - + > + + `, }; columns.trace = { @@ -169,13 +177,14 @@ class HaAutomationPicker extends LitElement { : undefined )} > - + )} + .disabled=${!automation.attributes.id} + > + + ${!automation.attributes.id ? html` @@ -199,25 +208,26 @@ class HaAutomationPicker extends LitElement { : undefined )} > - + > - ${!automation.attributes.id - ? html` - - ${this.hass.localize( - "ui.panel.config.automation.picker.only_editable" - )} - - ` - : ""} + ${ + !automation.attributes.id + ? html` + + ${this.hass.localize( + "ui.panel.config.automation.picker.only_editable" + )} + + ` + : "" + } `, }; return columns; From 401064d3c8d359de250daa8c241142b3224287b9 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Thu, 1 Apr 2021 11:29:08 -0700 Subject: [PATCH 07/14] Render script execution state (#8789) --- gallery/src/data/traces/basic_trace.ts | 49 ++------ gallery/src/data/traces/mock-demo-trace.ts | 44 +++++++ gallery/src/data/traces/motion-light-trace.ts | 49 ++------ .../demos/demo-automation-trace-timeline.ts | 87 ++++++++++++++ src/components/trace/hat-trace-timeline.ts | 110 ++++++++++++++---- src/data/trace.ts | 26 ++++- yarn.lock | 2 +- 7 files changed, 260 insertions(+), 107 deletions(-) create mode 100644 gallery/src/data/traces/mock-demo-trace.ts create mode 100644 gallery/src/demos/demo-automation-trace-timeline.ts diff --git a/gallery/src/data/traces/basic_trace.ts b/gallery/src/data/traces/basic_trace.ts index 24afe1fa07..1df0446a1e 100644 --- a/gallery/src/data/traces/basic_trace.ts +++ b/gallery/src/data/traces/basic_trace.ts @@ -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: [ { diff --git a/gallery/src/data/traces/mock-demo-trace.ts b/gallery/src/data/traces/mock-demo-trace.ts new file mode 100644 index 0000000000..04679bb007 --- /dev/null +++ b/gallery/src/data/traces/mock-demo-trace.ts @@ -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, + 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 || [], +}); diff --git a/gallery/src/data/traces/motion-light-trace.ts b/gallery/src/data/traces/motion-light-trace.ts index 9992989124..b735e28b03 100644 --- a/gallery/src/data/traces/motion-light-trace.ts +++ b/gallery/src/data/traces/motion-light-trace.ts @@ -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: "Paulus’s 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: "Paulus’s 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: [ { diff --git a/gallery/src/demos/demo-automation-trace-timeline.ts b/gallery/src/demos/demo-automation-trace-timeline.ts new file mode 100644 index 0000000000..2a8ecce1fa --- /dev/null +++ b/gallery/src/demos/demo-automation-trace-timeline.ts @@ -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` + +
+ + +
+
+ ` + )} + `; + } + + 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; + } +} diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index ac1b957004..f453fa8f36 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -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"; @@ -34,6 +36,7 @@ import { 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; @@ -273,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), @@ -421,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`

${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` - ${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} `); } @@ -506,6 +572,10 @@ export class HaAutomationTracer extends LitElement { ha-timeline[data-path] { cursor: pointer; } + .error { + --timeline-ball-color: var(--error-color); + color: var(--error-color); + } `, ]; } diff --git a/src/data/trace.ts b/src/data/trace.ts index 244ca952c6..fd38797ac8 100644 --- a/src/data/trace.ts +++ b/src/data/trace.ts @@ -54,22 +54,40 @@ 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; context: Context; - variables: Record; config: AutomationConfig; + error?: string; } interface TraceTypes { diff --git a/yarn.lock b/yarn.lock index 85e91b6876..da1bba399b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -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== From deca6f03ba118b3fd4582a6e9f889bac71ffe2ec Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 1 Apr 2021 22:33:47 +0200 Subject: [PATCH 08/14] Improve ensureArray and use it in tracing (#8785) * Improve ensureArray and use it in tracing * Fix typing Co-authored-by: Paulus Schoutsen --- src/common/ensure-array.ts | 8 +-- src/components/ha-target-picker.ts | 64 +++++++++++++----------- src/components/trace/hat-script-graph.ts | 32 ++++++------ src/data/script.ts | 2 +- src/data/script_i18n.ts | 3 +- src/data/trace.ts | 6 +-- 6 files changed, 60 insertions(+), 55 deletions(-) diff --git a/src/common/ensure-array.ts b/src/common/ensure-array.ts index 43d3dbb1f5..4290c040c1 100644 --- a/src/common/ensure-array.ts +++ b/src/common/ensure-array.ts @@ -1,6 +1,8 @@ -export const ensureArray = (value?: any) => { - if (!value || Array.isArray(value)) { +export function ensureArray(value: undefined): undefined; +export function ensureArray(value: T | T[]): T[]; +export function ensureArray(value) { + if (value === undefined || Array.isArray(value)) { return value; } return [value]; -}; +} diff --git a/src/components/ha-target-picker.ts b/src/components/ha-target-picker.ts index 574c1b36f2..11eabcae30 100644 --- a/src/components/ha-target-picker.ts +++ b/src/components/ha-target-picker.ts @@ -125,35 +125,41 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) { return html``; } return html`
- ${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 + ); + }) + : ""}
${this._renderPicker()}
diff --git a/src/components/trace/hat-script-graph.ts b/src/components/trace/hat-script-graph.ts index d347adad77..7a5e04cac7 100644 --- a/src/components/trace/hat-script-graph.ts +++ b/src/components/trace/hat-script-graph.ts @@ -48,6 +48,7 @@ import { WaitAction, WaitForTriggerAction, } from "../../data/script"; +import { ensureArray } from "../../common/ensure-array"; declare global { interface HASSDomEvents { @@ -412,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`
@@ -435,16 +434,13 @@ class HatScriptGraph extends LitElement { ${trigger_nodes}
- ${(!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) + )} - ${(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}`) + )}
( if (actionType === "wait_for_trigger") { const config = action as WaitForTriggerAction; - return `Wait for ${config.wait_for_trigger + return `Wait for ${ensureArray(config.wait_for_trigger) .map((trigger) => describeTrigger(trigger)) .join(", ")}`; } diff --git a/src/data/trace.ts b/src/data/trace.ts index fd38797ac8..6953353b87 100644 --- a/src/data/trace.ts +++ b/src/data/trace.ts @@ -1,6 +1,6 @@ import { strStartsWith } from "../common/string/starts-with"; import { HomeAssistant, Context } from "../types"; -import { AutomationConfig } from "./automation"; +import { ManualAutomationConfig } from "./automation"; interface BaseTraceStep { path: string; @@ -86,7 +86,7 @@ export interface AutomationTrace { export interface AutomationTraceExtended extends AutomationTrace { trace: Record; context: Context; - config: AutomationConfig; + config: ManualAutomationConfig; error?: string; } @@ -138,7 +138,7 @@ export const loadTraceContexts = ( }); export const getDataFromPath = ( - config: AutomationConfig, + config: ManualAutomationConfig, path: string ): any => { const parts = path.split("/").reverse(); From 8e3a7576ea403b5038d26795e1b869f30ba19408 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 1 Apr 2021 22:49:59 +0200 Subject: [PATCH 09/14] Align has template functions (#8784) Co-authored-by: Paulus Schoutsen --- src/common/string/has-template.ts | 5 +++-- src/data/script_i18n.ts | 10 +++++----- src/data/template.ts | 1 - .../developer-tools/service/developer-tools-service.ts | 2 +- 4 files changed, 9 insertions(+), 9 deletions(-) delete mode 100644 src/data/template.ts diff --git a/src/common/string/has-template.ts b/src/common/string/has-template.ts index b54af3307d..c86d7f6e8e 100644 --- a/src/common/string/has-template.ts +++ b/src/common/string/has-template.ts @@ -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; }; diff --git a/src/data/script_i18n.ts b/src/data/script_i18n.ts index a1dd8fbdd8..3b20e12865 100644 --- a/src/data/script_i18n.ts +++ b/src/data/script_i18n.ts @@ -1,6 +1,7 @@ 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"; @@ -14,7 +15,6 @@ import { VariablesAction, EventAction, } from "./script"; -import { isDynamicTemplate } from "./template"; export const describeAction = ( hass: HomeAssistant, @@ -35,7 +35,7 @@ export const describeAction = ( if ( config.service_template || - (config.service && isDynamicTemplate(config.service)) + (config.service && isTemplate(config.service)) ) { base = "Call a service based on a template"; } else if (config.service) { @@ -63,7 +63,7 @@ export const describeAction = ( let renderValues = true; for (const targetThing of keyConf) { - if (isDynamicTemplate(targetThing)) { + if (isTemplate(targetThing)) { targets.push(`templated ${label}`); renderValues = false; break; @@ -92,7 +92,7 @@ export const describeAction = ( if (typeof config.delay === "number") { duration = `for ${secondsToDuration(config.delay)!}`; } else if (typeof config.delay === "string") { - duration = isDynamicTemplate(config.delay) + duration = isTemplate(config.delay) ? "based on a template" : `for ${config.delay}`; } else { @@ -124,7 +124,7 @@ export const describeAction = ( if (actionType === "fire_event") { const config = action as EventAction; - if (isDynamicTemplate(config.event)) { + if (isTemplate(config.event)) { return "Fire event based on a template"; } return `Fire event ${config.event}`; diff --git a/src/data/template.ts b/src/data/template.ts deleted file mode 100644 index 452a36043a..0000000000 --- a/src/data/template.ts +++ /dev/null @@ -1 +0,0 @@ -export const isDynamicTemplate = (value: string) => value.includes("{{"); diff --git a/src/panels/developer-tools/service/developer-tools-service.ts b/src/panels/developer-tools/service/developer-tools-service.ts index 0f0452b823..4ad74bcd53 100644 --- a/src/panels/developer-tools/service/developer-tools-service.ts +++ b/src/panels/developer-tools/service/developer-tools-service.ts @@ -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 { From c11bbcf442c88e577841f3b442f180a038037a24 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Thu, 1 Apr 2021 23:37:46 +0200 Subject: [PATCH 10/14] Add blueprint config to trace (#8751) Co-authored-by: Paulus Schoutsen --- src/data/automation.ts | 3 + src/data/trace.ts | 6 +- .../config/automation/ha-automation-editor.ts | 65 +++++++++---------- .../ha-automation-trace-blueprint-config.ts | 34 ++++++++++ .../automation/trace/ha-automation-trace.ts | 24 ++++++- 5 files changed, 97 insertions(+), 35 deletions(-) create mode 100644 src/panels/config/automation/trace/ha-automation-trace-blueprint-config.ts diff --git a/src/data/automation.ts b/src/data/automation.ts index acf8e04fa2..d137f76600 100644 --- a/src/data/automation.ts +++ b/src/data/automation.ts @@ -238,6 +238,9 @@ export const deleteAutomation = (hass: HomeAssistant, id: string) => let inititialAutomationEditorData: Partial | undefined; +export const getAutomationConfig = (hass: HomeAssistant, id: string) => + hass.callApi("GET", `config/automation/config/${id}`); + export const showAutomationEditor = ( el: HTMLElement, data?: Partial diff --git a/src/data/trace.ts b/src/data/trace.ts index 6953353b87..abe9d06830 100644 --- a/src/data/trace.ts +++ b/src/data/trace.ts @@ -1,6 +1,9 @@ import { strStartsWith } from "../common/string/starts-with"; import { HomeAssistant, Context } from "../types"; -import { ManualAutomationConfig } from "./automation"; +import { + BlueprintAutomationConfig, + ManualAutomationConfig, +} from "./automation"; interface BaseTraceStep { path: string; @@ -87,6 +90,7 @@ export interface AutomationTraceExtended extends AutomationTrace { trace: Record; context: Context; config: ManualAutomationConfig; + blueprint_inputs?: BlueprintAutomationConfig; error?: string; } diff --git a/src/panels/config/automation/ha-automation-editor.ts b/src/panels/config/automation/ha-automation-editor.ts index a7af6b6879..19c73ac31b 100644 --- a/src/panels/config/automation/ha-automation-editor.ts +++ b/src/panels/config/automation/ha-automation-editor.ts @@ -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( - "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; diff --git a/src/panels/config/automation/trace/ha-automation-trace-blueprint-config.ts b/src/panels/config/automation/trace/ha-automation-trace-blueprint-config.ts new file mode 100644 index 0000000000..4f9d02403e --- /dev/null +++ b/src/panels/config/automation/trace/ha-automation-trace-blueprint-config.ts @@ -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` + + `; + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-automation-trace-blueprint-config": HaAutomationTraceBlueprintConfig; + } +} diff --git a/src/panels/config/automation/trace/ha-automation-trace.ts b/src/panels/config/automation/trace/ha-automation-trace.ts index 13f9f51c2e..0d712fb5f7 100644 --- a/src/panels/config/automation/trace/ha-automation-trace.ts +++ b/src/panels/config/automation/trace/ha-automation-trace.ts @@ -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 { @@ -70,7 +71,8 @@ export class HaAutomationTrace extends LitElement { | "details" | "config" | "timeline" - | "logbook" = "details"; + | "logbook" + | "blueprint" = "details"; protected render(): TemplateResult { const stateObj = this._entityId @@ -197,6 +199,19 @@ export class HaAutomationTrace extends LitElement {
` )} + ${this._trace.blueprint_inputs + ? html` +
+ Blueprint Config +
+ ` + : ""}
${this._selected === undefined || this._logbookEntries === undefined || @@ -227,6 +242,13 @@ export class HaAutomationTrace extends LitElement { .entries=${this._logbookEntries} > ` + : this._view === "blueprint" + ? html` + + ` : html` Date: Thu, 1 Apr 2021 15:10:17 -0700 Subject: [PATCH 11/14] Improve keyboard nav (#8794) --- src/components/trace/hat-trace-timeline.ts | 24 +++++++++++++------ .../trace/ha-automation-trace-path-details.ts | 4 ++-- .../automation/trace/ha-automation-trace.ts | 10 ++++---- src/panels/config/automation/trace/styles.ts | 2 ++ 4 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/components/trace/hat-trace-timeline.ts b/src/components/trace/hat-trace-timeline.ts index f453fa8f36..742c6c839c 100644 --- a/src/components/trace/hat-trace-timeline.ts +++ b/src/components/trace/hat-trace-timeline.ts @@ -541,17 +541,20 @@ export class HaAutomationTracer extends LitElement { this.shadowRoot!.querySelectorAll( "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; @@ -572,6 +575,13 @@ 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); diff --git a/src/panels/config/automation/trace/ha-automation-trace-path-details.ts b/src/panels/config/automation/trace/ha-automation-trace-path-details.ts index c9efd51048..aa893efc49 100644 --- a/src/panels/config/automation/trace/ha-automation-trace-path-details.ts +++ b/src/panels/config/automation/trace/ha-automation-trace-path-details.ts @@ -57,13 +57,13 @@ export class HaAutomationTracePathDetails extends LitElement { ["logbook", "Related logbook entries"], ].map( ([view, label]) => html` -
${label} -
+ ` )} diff --git a/src/panels/config/automation/trace/ha-automation-trace.ts b/src/panels/config/automation/trace/ha-automation-trace.ts index 0d712fb5f7..940c7fc97f 100644 --- a/src/panels/config/automation/trace/ha-automation-trace.ts +++ b/src/panels/config/automation/trace/ha-automation-trace.ts @@ -122,7 +122,7 @@ export class HaAutomationTrace extends LitElement { class="linkButton" href="/config/automation/edit/${this.automationId}" > - + @@ -190,18 +190,20 @@ export class HaAutomationTrace extends LitElement { ["config", "Automation Config"], ].map( ([view, label]) => html` -
${label} -
+ ` )} ${this._trace.blueprint_inputs ? html` -
*.active { From 01be5243de87f344b7cd06e7eee8c831a9322894 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Apr 2021 00:11:15 +0200 Subject: [PATCH 12/14] Option flows dont have result (#8787) --- src/data/data_entry_flow.ts | 2 +- src/dialogs/config-flow/dialog-data-entry-flow.ts | 2 +- src/dialogs/config-flow/step-flow-create-entry.ts | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/data/data_entry_flow.ts b/src/data/data_entry_flow.ts index f0ec11fc5f..d1447b1c74 100644 --- a/src/data/data_entry_flow.ts +++ b/src/data/data_entry_flow.ts @@ -45,7 +45,7 @@ export interface DataEntryFlowStepCreateEntry { flow_id: string; handler: string; title: string; - result: ConfigEntry; + result?: ConfigEntry; description: string; description_placeholders: Record; } diff --git a/src/dialogs/config-flow/dialog-data-entry-flow.ts b/src/dialogs/config-flow/dialog-data-entry-flow.ts index 1fdbe9b715..c4cb084fab 100644 --- a/src/dialogs/config-flow/dialog-data-entry-flow.ts +++ b/src/dialogs/config-flow/dialog-data-entry-flow.ts @@ -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 { diff --git a/src/dialogs/config-flow/step-flow-create-entry.ts b/src/dialogs/config-flow/step-flow-create-entry.ts index f6f2d96b2a..46b67ab1a6 100644 --- a/src/dialogs/config-flow/step-flow-create-entry.ts +++ b/src/dialogs/config-flow/step-flow-create-entry.ts @@ -43,7 +43,7 @@ class StepFlowCreateEntry extends LitElement {

Success!

${this.flowConfig.renderCreateEntryDescription(this.hass, this.step)} - ${this.step.result.state === "not_loaded" + ${this.step.result?.state === "not_loaded" ? html`${localize( "ui.panel.config.integrations.config_flow.not_loaded" From 8301dffb218a5334203bff692515cf9a573def26 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Apr 2021 00:14:02 +0200 Subject: [PATCH 13/14] Bumped version to 20210402.0 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index e888b8ccdd..a6b1c3d459 100644 --- a/setup.py +++ b/setup.py @@ -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", From 05ea3b8187e750c80da10788d105f92bd057e752 Mon Sep 17 00:00:00 2001 From: Bram Kragten Date: Fri, 2 Apr 2021 00:33:11 +0200 Subject: [PATCH 14/14] Make version number based on UTC time (#8796) --- script/version_bump.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/script/version_bump.js b/script/version_bump.js index 197dca4196..35ad851f15 100755 --- a/script/version_bump.js +++ b/script/version_bump.js @@ -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) {