mirror of
https://github.com/home-assistant/frontend.git
synced 2026-08-24 23:59:18 +00:00
Compare commits
73
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ba22a12a20 | ||
|
|
098b54f749 | ||
|
|
4c6a7091a6 | ||
|
|
322cb35526 | ||
|
|
c34f6bea2b | ||
|
|
41bf0652b0 | ||
|
|
23af40743b | ||
|
|
c4326b4f3a | ||
|
|
d248f5614f | ||
|
|
a4da7b26ea | ||
|
|
3c49cdf3c0 | ||
|
|
26af81d1a4 | ||
|
|
2a08f2d79b | ||
|
|
a5be02b743 | ||
|
|
4228871f00 | ||
|
|
9a7a8fd377 | ||
|
|
8b82882e15 | ||
|
|
2701015eda | ||
|
|
1991a9e493 | ||
|
|
2b72c54194 | ||
|
|
a7cb2fe7a7 | ||
|
|
51ea0c8201 | ||
|
|
ead7081bc6 | ||
|
|
ee982b1899 | ||
|
|
e8b100a39e | ||
|
|
50c361db62 | ||
|
|
e7a8d15a13 | ||
|
|
fbd0409837 | ||
|
|
a0d100611f | ||
|
|
a969bf1065 | ||
|
|
a153330610 | ||
|
|
bd2f1ca3a8 | ||
|
|
3263034416 | ||
|
|
82b28b547a | ||
|
|
61c2c750b4 | ||
|
|
117690ee70 | ||
|
|
e753de85eb | ||
|
|
a240019968 | ||
|
|
0bdf4b8777 | ||
|
|
6337828ed8 | ||
|
|
b8e5af652b | ||
|
|
e4ae29e8b5 | ||
|
|
08231dbbb0 | ||
|
|
0ca656933d | ||
|
|
b23cf8eba4 | ||
|
|
61b546415d | ||
|
|
4e1b709303 | ||
|
|
34e65b302d | ||
|
|
336d0e1b9d | ||
|
|
58d4cf8d84 | ||
|
|
d3453aff37 | ||
|
|
64ff2e414c | ||
|
|
2ca25c980f | ||
|
|
73d93bc601 | ||
|
|
5ca6a8aced | ||
|
|
7ff4993e0b | ||
|
|
4e6fbacccc | ||
|
|
2958d49e36 | ||
|
|
92289dc7ea | ||
|
|
f6c1a890e4 | ||
|
|
d06321ed43 | ||
|
|
3c3d8d9974 | ||
|
|
4f39fa482d | ||
|
|
5d0fe3236c | ||
|
|
b86142ae50 | ||
|
|
5d2f3ee5e8 | ||
|
|
e3f7c631a7 | ||
|
|
49f9d95853 | ||
|
|
db3d7701b5 | ||
|
|
3e55acf531 | ||
|
|
f102618d9d | ||
|
|
a3c02b511d | ||
|
|
74111d248e |
@@ -89,13 +89,13 @@ jobs:
|
||||
env:
|
||||
IS_TEST: "true"
|
||||
- name: Upload bundle stats
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: frontend-bundle-stats
|
||||
path: build/stats/*.json
|
||||
if-no-files-found: error
|
||||
- name: Upload frontend build
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: frontend-build
|
||||
path: hass_frontend/
|
||||
|
||||
@@ -57,14 +57,14 @@ jobs:
|
||||
run: tar -czvf translations.tar.gz translations
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: wheels
|
||||
path: dist/home_assistant_frontend*.whl
|
||||
if-no-files-found: error
|
||||
|
||||
- name: Upload translations
|
||||
uses: actions/upload-artifact@b7c566a772e6b6bfb58ed0dc250532a479d7789f # v6.0.0
|
||||
uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0
|
||||
with:
|
||||
name: translations
|
||||
path: translations.tar.gz
|
||||
|
||||
+14
-14
@@ -30,7 +30,7 @@
|
||||
"@braintree/sanitize-url": "7.1.2",
|
||||
"@codemirror/autocomplete": "6.20.0",
|
||||
"@codemirror/commands": "6.10.2",
|
||||
"@codemirror/language": "6.12.1",
|
||||
"@codemirror/language": "6.12.2",
|
||||
"@codemirror/legacy-modes": "6.5.2",
|
||||
"@codemirror/search": "6.6.0",
|
||||
"@codemirror/state": "6.5.4",
|
||||
@@ -52,7 +52,7 @@
|
||||
"@fullcalendar/list": "6.1.20",
|
||||
"@fullcalendar/luxon3": "6.1.20",
|
||||
"@fullcalendar/timegrid": "6.1.20",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.2",
|
||||
"@home-assistant/webawesome": "3.2.1-ha.3",
|
||||
"@lezer/highlight": "1.2.3",
|
||||
"@lit-labs/motion": "1.1.0",
|
||||
"@lit-labs/observers": "2.1.0",
|
||||
@@ -83,7 +83,7 @@
|
||||
"@mdi/js": "7.4.47",
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.18",
|
||||
"@swc/helpers": "0.5.19",
|
||||
"@thomasloven/round-slider": "0.6.0",
|
||||
"@tsparticles/engine": "3.9.1",
|
||||
"@tsparticles/preset-links": "3.2.0",
|
||||
@@ -92,7 +92,7 @@
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"@webcomponents/webcomponentsjs": "2.8.0",
|
||||
"app-datepicker": "5.1.1",
|
||||
"barcode-detector": "3.0.8",
|
||||
"barcode-detector": "3.1.0",
|
||||
"color-name": "2.1.0",
|
||||
"comlink": "4.4.2",
|
||||
"core-js": "3.48.0",
|
||||
@@ -106,7 +106,7 @@
|
||||
"element-internals-polyfill": "3.0.2",
|
||||
"fuse.js": "7.1.0",
|
||||
"google-timezones-json": "1.2.0",
|
||||
"gulp-zopfli-green": "6.0.2",
|
||||
"gulp-zopfli-green": "7.0.0",
|
||||
"hls.js": "1.6.15",
|
||||
"home-assistant-js-websocket": "9.6.0",
|
||||
"idb-keyval": "6.2.2",
|
||||
@@ -149,7 +149,7 @@
|
||||
"@babel/plugin-transform-runtime": "7.29.0",
|
||||
"@babel/preset-env": "7.29.0",
|
||||
"@bundle-stats/plugin-webpack-filter": "4.21.10",
|
||||
"@html-eslint/eslint-plugin": "0.56.0",
|
||||
"@html-eslint/eslint-plugin": "0.57.1",
|
||||
"@lokalise/node-api": "15.6.1",
|
||||
"@octokit/auth-oauth-device": "8.0.3",
|
||||
"@octokit/plugin-retry": "8.1.0",
|
||||
@@ -172,7 +172,7 @@
|
||||
"@types/mocha": "10.0.10",
|
||||
"@types/qrcode": "1.5.6",
|
||||
"@types/sortablejs": "1.15.9",
|
||||
"@types/tar": "6.1.13",
|
||||
"@types/tar": "7.0.87",
|
||||
"@types/ua-parser-js": "0.7.39",
|
||||
"@types/webspeechapi": "0.0.29",
|
||||
"@vitest/coverage-v8": "4.0.18",
|
||||
@@ -188,7 +188,7 @@
|
||||
"eslint-plugin-lit": "2.2.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.1",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
"eslint-plugin-wc": "3.0.2",
|
||||
"eslint-plugin-wc": "3.1.0",
|
||||
"fancy-log": "2.0.0",
|
||||
"fs-extra": "11.3.3",
|
||||
"glob": "13.0.6",
|
||||
@@ -198,9 +198,9 @@
|
||||
"gulp-rename": "2.1.0",
|
||||
"html-minifier-terser": "7.2.0",
|
||||
"husky": "9.1.7",
|
||||
"jsdom": "28.0.0",
|
||||
"jsdom": "28.1.0",
|
||||
"jszip": "3.10.1",
|
||||
"lint-staged": "16.2.7",
|
||||
"lint-staged": "16.3.0",
|
||||
"lit-analyzer": "2.0.3",
|
||||
"lodash.merge": "4.6.2",
|
||||
"lodash.template": "4.5.0",
|
||||
@@ -210,12 +210,12 @@
|
||||
"rspack-manifest-plugin": "5.2.1",
|
||||
"serve": "14.2.5",
|
||||
"sinon": "21.0.1",
|
||||
"tar": "7.5.8",
|
||||
"tar": "7.5.9",
|
||||
"terser-webpack-plugin": "5.3.16",
|
||||
"ts-lit-plugin": "2.0.2",
|
||||
"typescript": "5.9.3",
|
||||
"typescript-eslint": "8.54.0",
|
||||
"vite-tsconfig-paths": "6.0.5",
|
||||
"typescript-eslint": "8.56.1",
|
||||
"vite-tsconfig-paths": "6.1.1",
|
||||
"vitest": "4.0.18",
|
||||
"webpack-stats-plugin": "1.1.3",
|
||||
"webpackbar": "7.0.0",
|
||||
@@ -235,6 +235,6 @@
|
||||
},
|
||||
"packageManager": "[email protected]",
|
||||
"volta": {
|
||||
"node": "24.13.1"
|
||||
"node": "24.14.0"
|
||||
}
|
||||
}
|
||||
|
||||
+1
-1
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
|
||||
|
||||
[project]
|
||||
name = "home-assistant-frontend"
|
||||
version = "20260302.0"
|
||||
version = "20260128.0"
|
||||
license = "Apache-2.0"
|
||||
license-files = ["LICENSE*"]
|
||||
description = "The Home Assistant frontend"
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import { genClientId } from "home-assistant-js-websocket";
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { keyed } from "lit/directives/keyed";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import "../components/ha-alert";
|
||||
@@ -23,6 +23,7 @@ import type {
|
||||
DataEntryFlowStepForm,
|
||||
} from "../data/data_entry_flow";
|
||||
import "./ha-auth-form";
|
||||
import type { HaAuthForm } from "./ha-auth-form";
|
||||
|
||||
type State = "loading" | "error" | "step";
|
||||
|
||||
@@ -52,6 +53,8 @@ export class HaAuthFlow extends LitElement {
|
||||
|
||||
@state() private _submitting = false;
|
||||
|
||||
@query("ha-auth-form") private _form?: HaAuthForm;
|
||||
|
||||
createRenderRoot() {
|
||||
return this;
|
||||
}
|
||||
@@ -179,7 +182,7 @@ export class HaAuthFlow extends LitElement {
|
||||
<div class="action">
|
||||
<ha-button
|
||||
@click=${this._handleSubmit}
|
||||
.disabled=${this._submitting}
|
||||
.loading=${this._submitting}
|
||||
>
|
||||
${this.step.type === "form"
|
||||
? this.localize("ui.panel.page-authorize.form.next")
|
||||
@@ -370,6 +373,11 @@ export class HaAuthFlow extends LitElement {
|
||||
this._providerChanged(this.authProvider);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this._form?.reportValidity()) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._submitting = true;
|
||||
|
||||
const postData = { ...this._stepData, client_id: this.clientId };
|
||||
|
||||
@@ -12,6 +12,10 @@ export class HaAuthFormString extends HaFormString {
|
||||
return this;
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this.querySelector("ha-auth-textfield")?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<style>
|
||||
|
||||
@@ -1,3 +1,24 @@
|
||||
import { deepActiveElement } from "../dom/deep-active-element";
|
||||
|
||||
const getClipboardFallbackRoot = (): HTMLElement => {
|
||||
const activeElement = deepActiveElement();
|
||||
if (activeElement instanceof HTMLElement) {
|
||||
let root: Node = activeElement.getRootNode();
|
||||
let host: HTMLElement | null = null;
|
||||
|
||||
while (root instanceof ShadowRoot && root.host instanceof HTMLElement) {
|
||||
host = root.host;
|
||||
root = root.host.getRootNode();
|
||||
}
|
||||
|
||||
if (host) {
|
||||
return host;
|
||||
}
|
||||
}
|
||||
|
||||
return document.body;
|
||||
};
|
||||
|
||||
export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
if (navigator.clipboard) {
|
||||
try {
|
||||
@@ -8,10 +29,15 @@ export const copyToClipboard = async (str, rootEl?: HTMLElement) => {
|
||||
}
|
||||
}
|
||||
|
||||
const root = rootEl ?? document.body;
|
||||
const root = rootEl || getClipboardFallbackRoot();
|
||||
|
||||
const el = document.createElement("textarea");
|
||||
el.value = str;
|
||||
el.setAttribute("readonly", "");
|
||||
el.style.position = "fixed";
|
||||
el.style.top = "0";
|
||||
el.style.left = "0";
|
||||
el.style.opacity = "0";
|
||||
root.appendChild(el);
|
||||
el.select();
|
||||
document.execCommand("copy");
|
||||
|
||||
@@ -672,11 +672,11 @@ export class HaAssistChat extends LitElement {
|
||||
--markdown-code-background-color: var(--primary-background-color);
|
||||
--markdown-code-text-color: var(--primary-text-color);
|
||||
--markdown-list-indent: 1.15em;
|
||||
&:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
ha-markdown:not(:has(ha-markdown-element)) {
|
||||
min-height: 1lh;
|
||||
min-width: 1lh;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
.bouncer {
|
||||
width: 48px;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mdiClose } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-icon-button";
|
||||
@@ -133,6 +133,17 @@ export class HaBaseTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
@queryAll("ha-textfield") private _inputs?: HaTextField[];
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._inputs?.every((input) => input.reportValidity()) ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
${this.label
|
||||
|
||||
@@ -406,6 +406,18 @@ export class HaBottomSheet extends ScrollableFadeMixin(LitElement) {
|
||||
transform: var(--dialog-transform);
|
||||
transition: var(--dialog-transition);
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
wa-drawer {
|
||||
--wa-color-surface-raised: transparent;
|
||||
--spacing: 0;
|
||||
--size: var(--ha-bottom-sheet-height, auto);
|
||||
--show-duration: 1ms;
|
||||
--hide-duration: 1ms;
|
||||
}
|
||||
wa-drawer::part(dialog) {
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
wa-drawer::part(dialog)::backdrop {
|
||||
-webkit-backdrop-filter: var(
|
||||
--ha-bottom-sheet-scrim-backdrop-filter,
|
||||
|
||||
@@ -33,6 +33,7 @@ export class HaControlButton extends LitElement {
|
||||
--control-button-background-color: var(--disabled-color);
|
||||
--control-button-background-opacity: 0.2;
|
||||
--control-button-border-radius: var(--ha-border-radius-md);
|
||||
--control-button-font-weight: var(--ha-font-weight-medium);
|
||||
--control-button-padding: 8px;
|
||||
--mdc-icon-size: 20px;
|
||||
--ha-ripple-color: var(--secondary-text-color);
|
||||
@@ -59,7 +60,7 @@ export class HaControlButton extends LitElement {
|
||||
box-sizing: border-box;
|
||||
line-height: inherit;
|
||||
font-family: var(--ha-font-family-body);
|
||||
font-weight: var(--ha-font-weight-medium);
|
||||
font-weight: var(--control-button-font-weight);
|
||||
outline: none;
|
||||
overflow: hidden;
|
||||
background: none;
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { mdiCalendar } from "@mdi/js";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { firstWeekdayIndex } from "../common/datetime/first_weekday";
|
||||
import { formatDateNumeric } from "../common/datetime/format_date";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
@@ -9,6 +9,7 @@ import { TimeZone } from "../data/translation";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import "./ha-svg-icon";
|
||||
import "./ha-textfield";
|
||||
import type { HaTextField } from "./ha-textfield";
|
||||
|
||||
const loadDatePickerDialog = () => import("./ha-dialog-date-picker");
|
||||
|
||||
@@ -52,6 +53,12 @@ export class HaDateInput extends LitElement {
|
||||
|
||||
@property({ attribute: "can-clear", type: Boolean }) public canClear = false;
|
||||
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
render() {
|
||||
return html`<ha-textfield
|
||||
.label=${this.label}
|
||||
|
||||
+21
-21
@@ -410,29 +410,29 @@ export class HaDialog extends ScrollableFadeMixin(LitElement) {
|
||||
@media all and (max-width: 450px), all and (max-height: 500px) {
|
||||
:host([type="standard"]) {
|
||||
--ha-dialog-border-radius: 0;
|
||||
}
|
||||
|
||||
wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
:host([type="standard"]) wa-dialog {
|
||||
/* Make the container fill the whole screen width and not the safe width */
|
||||
--full-width: var(--ha-dialog-width-full, 100vw);
|
||||
--width: var(--full-width);
|
||||
}
|
||||
|
||||
wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
:host([type="standard"]) wa-dialog::part(dialog) {
|
||||
/* Make the dialog fill the whole screen height and not the safe height */
|
||||
min-height: var(--ha-dialog-min-height, 100vh);
|
||||
min-height: var(--ha-dialog-min-height, 100dvh);
|
||||
max-height: var(--ha-dialog-max-height, 100vh);
|
||||
max-height: var(--ha-dialog-max-height, 100dvh);
|
||||
margin-top: 0;
|
||||
margin-bottom: 0;
|
||||
/* Use safe area as padding instead of the container size */
|
||||
padding-top: var(--safe-area-inset-top);
|
||||
padding-bottom: var(--safe-area-inset-bottom);
|
||||
padding-left: var(--safe-area-inset-left);
|
||||
padding-right: var(--safe-area-inset-right);
|
||||
/* Reset the transform to center the dialog */
|
||||
transform: none;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { mdiMinusThick, mdiPlusThick } from "@mdi/js";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-base-time-input";
|
||||
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
|
||||
import "./ha-button-toggle-group";
|
||||
|
||||
export interface HaDurationData {
|
||||
days?: number;
|
||||
@@ -19,7 +19,7 @@ export interface HaDurationData {
|
||||
const FIELDS = ["milliseconds", "seconds", "minutes", "hours", "days"];
|
||||
|
||||
@customElement("ha-duration-input")
|
||||
class HaDurationInput extends LitElement {
|
||||
export class HaDurationInput extends LitElement {
|
||||
@property({ attribute: false }) public data?: HaDurationData;
|
||||
|
||||
@property() public label?: string;
|
||||
@@ -42,8 +42,19 @@ class HaDurationInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-base-time-input", true) private _input?: HaBaseTimeInput;
|
||||
|
||||
private _toggleNegative = false;
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="row">
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-form";
|
||||
import "../ha-expansion-panel";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
@@ -35,6 +36,12 @@ export class HaFormExpandable extends LitElement implements HaFormElement {
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
@query("ha-form", true) private _form?: HaForm;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._form?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
private _renderDescription() {
|
||||
const description = this.computeHelper?.(this.schema);
|
||||
return description ? html`<p>${description}</p>` : nothing;
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
import type { TemplateResult, PropertyValues } from "lit";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormFloatData,
|
||||
HaFormFloatSchema,
|
||||
} from "./types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("ha-form-float")
|
||||
export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
@@ -25,12 +25,15 @@ export class HaFormFloat extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield") private _input?: HaTextField;
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -1,14 +1,15 @@
|
||||
import "./ha-form";
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, queryAll } from "lit/decorators";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormGridSchema,
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormGridSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
|
||||
@customElement("ha-form-grid")
|
||||
export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
@@ -33,9 +34,22 @@ export class HaFormGrid extends LitElement implements HaFormElement {
|
||||
key: string
|
||||
) => string;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
@queryAll("ha-form", true) private _forms?: HaForm[];
|
||||
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const forms = this._forms ?? [];
|
||||
let valid = true;
|
||||
for (const form of forms) {
|
||||
if (!form.reportValidity()) {
|
||||
valid = false;
|
||||
}
|
||||
}
|
||||
return valid;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
|
||||
@@ -2,10 +2,11 @@ import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-slider";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import "../ha-checkbox";
|
||||
import type { HaCheckbox } from "../ha-checkbox";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
import type {
|
||||
@@ -13,7 +14,6 @@ import type {
|
||||
HaFormIntegerData,
|
||||
HaFormIntegerSchema,
|
||||
} from "./types";
|
||||
import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
|
||||
@customElement("ha-form-integer")
|
||||
export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
@@ -29,24 +29,39 @@ export class HaFormInteger extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield ha-slider") private _input?:
|
||||
@query("ha-textfield, ha-slider", true) private _input?:
|
||||
| HaTextField
|
||||
| HTMLInputElement;
|
||||
|
||||
private _lastValue?: HaFormIntegerData;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const showSlider = this._showSlider();
|
||||
if (showSlider && this.schema.required && isNaN(Number(this.data))) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!showSlider) {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (
|
||||
private _showSlider(): boolean {
|
||||
return (
|
||||
this.schema.valueMin !== undefined &&
|
||||
this.schema.valueMax !== undefined &&
|
||||
this.schema.valueMax - this.schema.valueMin < 256
|
||||
) {
|
||||
);
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
if (this._showSlider()) {
|
||||
return html`
|
||||
<div>
|
||||
${this.label}
|
||||
|
||||
@@ -44,6 +44,13 @@ export class HaFormMultiSelect extends LitElement implements HaFormElement {
|
||||
this._dropdown?.focus();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (!this.schema.required || (this.data && this.data.length > 0)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const options = Array.isArray(this.schema.options)
|
||||
? this.schema.options
|
||||
|
||||
@@ -8,16 +8,17 @@ import type { LocalizeFunc } from "../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-button";
|
||||
import "../ha-dropdown";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
import "../ha-dropdown-item";
|
||||
import "../ha-svg-icon";
|
||||
import "./ha-form";
|
||||
import type { HaForm } from "./ha-form";
|
||||
import type {
|
||||
HaFormDataContainer,
|
||||
HaFormElement,
|
||||
HaFormOptionalActionsSchema,
|
||||
HaFormSchema,
|
||||
} from "./types";
|
||||
import type { HaDropdownSelectEvent } from "../ha-dropdown";
|
||||
|
||||
const NO_ACTIONS = [];
|
||||
|
||||
@@ -53,6 +54,11 @@ export class HaFormOptionalActions extends LitElement implements HaFormElement {
|
||||
this.renderRoot.querySelector("ha-form")?.focus();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const form = this.renderRoot.querySelector<HaForm>("ha-form");
|
||||
return form ? form.reportValidity() : true;
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("data")) {
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationInput } from "../ha-duration-input";
|
||||
import type { HaFormElement, HaFormTimeData, HaFormTimeSchema } from "./types";
|
||||
|
||||
@customElement("ha-form-positive_time_period_dict")
|
||||
@@ -14,12 +15,15 @@ export class HaFormTimePeriod extends LitElement implements HaFormElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-time-input", true) private _input?: HTMLElement;
|
||||
@query("ha-duration-input", true) private _input?: HaDurationInput;
|
||||
|
||||
public focus() {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { TemplateResult } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
import type {
|
||||
HaFormElement,
|
||||
HaFormSelectData,
|
||||
HaFormSelectSchema,
|
||||
} from "./types";
|
||||
import type { SelectSelector } from "../../data/selector";
|
||||
import "../ha-selector/ha-selector-select";
|
||||
|
||||
@customElement("ha-form-select")
|
||||
export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
@@ -41,6 +41,13 @@ export class HaFormSelect extends LitElement implements HaFormElement {
|
||||
})
|
||||
);
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (!this.schema.required || this.data) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<ha-selector-select
|
||||
|
||||
@@ -37,12 +37,15 @@ export class HaFormString extends LitElement implements HaFormElement {
|
||||
|
||||
@state() protected unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield") private _input?: HaTextField;
|
||||
@query("ha-textfield", true) private _input?: HaTextField;
|
||||
|
||||
public focus(): void {
|
||||
if (this._input) {
|
||||
this._input.focus();
|
||||
}
|
||||
static shadowRootOptions = {
|
||||
...LitElement.shadowRootOptions,
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type { PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, ReactiveElement } from "lit";
|
||||
import { css, html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
@@ -24,7 +24,7 @@ const LOAD_ELEMENTS = {
|
||||
};
|
||||
|
||||
const getValue = (obj, item) =>
|
||||
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
|
||||
obj ? (!item.name || item.flatten ? obj : obj[item.name]) : undefined;
|
||||
|
||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
|
||||
|
||||
@@ -76,22 +76,64 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
return {};
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
public reportValidity(): boolean {
|
||||
const root = this.renderRoot.querySelector(".root");
|
||||
if (!root) {
|
||||
return;
|
||||
return true;
|
||||
}
|
||||
for (const child of root.children) {
|
||||
if (child.tagName !== "HA-ALERT") {
|
||||
if (child instanceof ReactiveElement) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await child.updateComplete;
|
||||
}
|
||||
(child as HTMLElement).focus();
|
||||
break;
|
||||
|
||||
const elements = [...root.children].filter(
|
||||
(child) => child.localName !== "ha-alert"
|
||||
) as (HTMLElement & { reportValidity?: () => boolean })[];
|
||||
|
||||
let isValid = true;
|
||||
let firstInvalidElement: HTMLElement | undefined;
|
||||
|
||||
this.schema.forEach((item, index) => {
|
||||
const element = elements[index];
|
||||
if (!element) {
|
||||
return;
|
||||
}
|
||||
|
||||
let elementValid = true;
|
||||
|
||||
if (
|
||||
"reportValidity" in element &&
|
||||
typeof element.reportValidity === "function"
|
||||
) {
|
||||
elementValid = element.reportValidity();
|
||||
} else if (
|
||||
item.required &&
|
||||
!(
|
||||
"type" in item && ["boolean", "constant"].includes(item.type ?? "")
|
||||
) &&
|
||||
!(
|
||||
"selector" in item &&
|
||||
("boolean" in item.selector || "constant" in item.selector)
|
||||
)
|
||||
) {
|
||||
const value = getValue(this.data, item);
|
||||
elementValid = value !== undefined && value !== null && value !== "";
|
||||
}
|
||||
|
||||
if (!elementValid) {
|
||||
isValid = false;
|
||||
if (!firstInvalidElement) {
|
||||
firstInvalidElement = element;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (firstInvalidElement) {
|
||||
firstInvalidElement.focus?.();
|
||||
}
|
||||
|
||||
return isValid;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
@@ -105,11 +147,6 @@ export class HaForm extends LitElement implements HaFormElement {
|
||||
}
|
||||
}
|
||||
|
||||
static shadowRootOptions: ShadowRootInit = {
|
||||
mode: "open",
|
||||
delegatesFocus: true,
|
||||
};
|
||||
|
||||
protected render(): TemplateResult {
|
||||
return html`
|
||||
<div class="root" part="root">
|
||||
|
||||
@@ -38,7 +38,7 @@ export class HaBadge extends LitElement {
|
||||
font-weight: var(--ha-heading-badge-font-weight, 400);
|
||||
line-height: var(--ha-heading-badge-line-height, 20px);
|
||||
letter-spacing: 0.1px;
|
||||
--mdc-icon-size: 14px;
|
||||
--mdc-icon-size: 16px;
|
||||
}
|
||||
::slotted([slot="icon"]) {
|
||||
--ha-icon-display: block;
|
||||
|
||||
@@ -84,13 +84,11 @@ export class HaMarkdown extends LitElement {
|
||||
ha-markdown-element > :is(ol, ul) {
|
||||
padding-inline-start: var(--markdown-list-indent, revert);
|
||||
}
|
||||
li {
|
||||
&:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
& > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
}
|
||||
li:has(input[type="checkbox"]) {
|
||||
list-style: none;
|
||||
}
|
||||
li:has(input[type="checkbox"]) > input[type="checkbox"] {
|
||||
margin-left: 0;
|
||||
}
|
||||
svg {
|
||||
background-color: var(--markdown-svg-background-color, none);
|
||||
@@ -137,10 +135,10 @@ export class HaMarkdown extends LitElement {
|
||||
--markdown-table-border-width: 0;
|
||||
--markdown-table-padding-inline: 0;
|
||||
--markdown-table-padding-block: 0;
|
||||
th,
|
||||
td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
}
|
||||
table[role="presentation"] th,
|
||||
table[role="presentation"] td {
|
||||
vertical-align: middle;
|
||||
}
|
||||
table[role="presentation"] td[valign="top"],
|
||||
table[role="presentation"] th[valign="top"] {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { DateSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-date-input";
|
||||
import type { HaDateInput } from "../ha-date-input";
|
||||
|
||||
@customElement("ha-selector-date")
|
||||
export class HaDateSelector extends LitElement {
|
||||
@@ -20,6 +21,12 @@ export class HaDateSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-date-input", true) private _input?: HaDateInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-date-input
|
||||
|
||||
@@ -29,6 +29,10 @@ export class HaDateTimeSelector extends LitElement {
|
||||
|
||||
@query("ha-time-input") private _timeInput!: HaTimeInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._dateInput.reportValidity() && this._timeInput.reportValidity();
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const values =
|
||||
typeof this.value === "string" ? this.value.split(" ") : undefined;
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import memoizeOne from "memoize-one";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import type { DurationSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HaDurationData } from "../ha-duration-input";
|
||||
import "../ha-duration-input";
|
||||
import type { HaDurationData, HaDurationInput } from "../ha-duration-input";
|
||||
|
||||
@customElement("ha-selector-duration")
|
||||
export class HaTimeDuration extends LitElement {
|
||||
@@ -25,6 +25,12 @@ export class HaTimeDuration extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = true;
|
||||
|
||||
@query("ha-duration-input", true) private _input?: HaDurationInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
private _data = memoizeOne(
|
||||
(value?: HaDurationData | string | number): HaDurationData | undefined => {
|
||||
if (typeof value === "number") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { NumberSelector } from "../../data/selector";
|
||||
@@ -8,6 +8,7 @@ import type { HomeAssistant } from "../../types";
|
||||
import "../ha-input-helper-text";
|
||||
import "../ha-slider";
|
||||
import "../ha-textfield";
|
||||
import type { HaTextField } from "../ha-textfield";
|
||||
|
||||
@customElement("ha-selector-number")
|
||||
export class HaNumberSelector extends LitElement {
|
||||
@@ -30,8 +31,14 @@ export class HaNumberSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
|
||||
@query("ha-textfield", true) private _input?: HaTextField | HTMLInputElement;
|
||||
|
||||
private _valueStr = "";
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues) {
|
||||
if (changedProps.has("value")) {
|
||||
if (this._valueStr === "" || this.value !== Number(this._valueStr)) {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
|
||||
import { LitElement, css, html } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { ensureArray } from "../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../common/dom/fire_event";
|
||||
import type { StringSelector } from "../../data/selector";
|
||||
@@ -32,11 +32,18 @@ export class HaTextSelector extends LitElement {
|
||||
|
||||
@state() private _unmaskedPassword = false;
|
||||
|
||||
@query("ha-textfield, ha-textarea") private _input?: HTMLInputElement;
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
(
|
||||
this.renderRoot.querySelector("ha-textarea, ha-textfield") as HTMLElement
|
||||
)?.focus();
|
||||
this._input?.focus();
|
||||
}
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (this.selector.text?.multiple) {
|
||||
return true;
|
||||
}
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import type { TimeSelector } from "../../data/selector";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import "../ha-time-input";
|
||||
import type { HaTimeInput } from "../ha-time-input";
|
||||
|
||||
@customElement("ha-selector-time")
|
||||
export class HaTimeSelector extends LitElement {
|
||||
@@ -20,6 +21,12 @@ export class HaTimeSelector extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public required = false;
|
||||
|
||||
@query("ha-time-input") private _input?: HaTimeInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-time-input
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { PropertyValues } from "lit";
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
|
||||
import type { Selector } from "../../data/selector";
|
||||
@@ -94,9 +94,27 @@ export class HaSelector extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public context?: Record<string, any>;
|
||||
|
||||
@query("#selector", true) private _selectorElement?: HTMLElement;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
if (
|
||||
this._selectorElement &&
|
||||
"reportValidity" in this._selectorElement &&
|
||||
typeof this._selectorElement.reportValidity === "function"
|
||||
) {
|
||||
return this._selectorElement?.reportValidity() ?? true;
|
||||
}
|
||||
if (this.required) {
|
||||
return (
|
||||
this.value !== undefined && this.value !== null && this.value !== ""
|
||||
);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public async focus() {
|
||||
await this.updateComplete;
|
||||
(this.renderRoot.querySelector("#selector") as HTMLElement)?.focus();
|
||||
this._selectorElement?.focus();
|
||||
}
|
||||
|
||||
private get _type() {
|
||||
|
||||
@@ -37,8 +37,8 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
|
||||
import type { UpdateEntity } from "../data/update";
|
||||
import { updateCanInstall } from "../data/update";
|
||||
import { showEditSidebarDialog } from "../dialogs/sidebar/show-dialog-edit-sidebar";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { ScrollableFadeMixin } from "../mixins/scrollable-fade-mixin";
|
||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
|
||||
import { actionHandler } from "../panels/lovelace/common/directives/action-handler-directive";
|
||||
import { haStyleScrollbar } from "../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo, Route } from "../types";
|
||||
@@ -981,7 +981,7 @@ class HaSidebar extends SubscribeMixin(ScrollableFadeMixin(LitElement)) {
|
||||
ha-md-list-item,
|
||||
ha-md-list-item .item-text,
|
||||
.title {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
`,
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { useAmPm } from "../common/datetime/use_am_pm";
|
||||
import { fireEvent } from "../common/dom/fire_event";
|
||||
import type { FrontendLocaleData } from "../data/translation";
|
||||
import "./ha-base-time-input";
|
||||
import type { TimeChangedEvent } from "./ha-base-time-input";
|
||||
import type { ValueChangedEvent } from "../types";
|
||||
import "./ha-base-time-input";
|
||||
import type { HaBaseTimeInput, TimeChangedEvent } from "./ha-base-time-input";
|
||||
|
||||
@customElement("ha-time-input")
|
||||
export class HaTimeInput extends LitElement {
|
||||
@@ -26,6 +26,12 @@ export class HaTimeInput extends LitElement {
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public clearable?: boolean;
|
||||
|
||||
@query("ha-base-time-input") private _input?: HaBaseTimeInput;
|
||||
|
||||
public reportValidity(): boolean {
|
||||
return this._input?.reportValidity() ?? true;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const useAMPM = useAmPm(this.locale);
|
||||
|
||||
|
||||
@@ -46,7 +46,7 @@ export class HaTopAppBarFixed extends TopAppBarFixedBase {
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar__title {
|
||||
|
||||
@@ -298,7 +298,7 @@ export class TopAppBarBaseBase extends BaseElement {
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.mdc-top-app-bar {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
.mdc-top-app-bar--pane.mdc-top-app-bar--fixed-scrolled {
|
||||
|
||||
@@ -37,9 +37,11 @@ export interface LovelaceViewHeaderConfig {
|
||||
badges_wrap?: "wrap" | "scroll";
|
||||
}
|
||||
|
||||
export const DEFAULT_FOOTER_MAX_WIDTH_PX = 600;
|
||||
|
||||
export interface LovelaceViewFooterConfig {
|
||||
card?: LovelaceCardConfig;
|
||||
column_span?: number;
|
||||
max_width?: number;
|
||||
}
|
||||
|
||||
export interface LovelaceViewSidebarConfig {
|
||||
|
||||
@@ -7,10 +7,23 @@ export type SystemLogLevel =
|
||||
| "info"
|
||||
| "debug";
|
||||
|
||||
export type SystemLogErrorType =
|
||||
| "auth"
|
||||
| "connection"
|
||||
| "invalid_response"
|
||||
| "rate_limit"
|
||||
| "server"
|
||||
| "slow_setup"
|
||||
| "timeout"
|
||||
| "ssl"
|
||||
| "statistics"
|
||||
| "dns";
|
||||
|
||||
export interface LoggedError {
|
||||
name: string;
|
||||
message: [string];
|
||||
level: SystemLogLevel;
|
||||
error_type?: SystemLogErrorType;
|
||||
source: [string, number];
|
||||
exception: string;
|
||||
count: number;
|
||||
|
||||
@@ -2,10 +2,10 @@ import type { LitElement, PropertyValues } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import { computeLocalize } from "../common/translations/localize";
|
||||
import { computeDirectionStyles } from "../common/util/compute_rtl";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import type { Constructor, Resources } from "../types";
|
||||
import { getLocalLanguage, getTranslation } from "../util/common-translation";
|
||||
import { translationMetadata } from "../resources/translations-metadata";
|
||||
import { computeDirectionStyles } from "../common/util/compute_rtl";
|
||||
|
||||
const empty = () => "";
|
||||
|
||||
@@ -28,16 +28,16 @@ export const litLocalizeLiteMixin = <T extends Constructor<LitElement>>(
|
||||
this._initializeLocalizeLite();
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues) {
|
||||
super.firstUpdated(changedProps);
|
||||
computeDirectionStyles(
|
||||
translationMetadata.translations[this.language!].isRTL,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues) {
|
||||
super.willUpdate(changedProperties);
|
||||
|
||||
if (!this.updated || changedProperties.has("language")) {
|
||||
computeDirectionStyles(
|
||||
translationMetadata.translations[this.language!].isRTL,
|
||||
this
|
||||
);
|
||||
}
|
||||
|
||||
if (changedProperties.get("language")) {
|
||||
this._resources = undefined;
|
||||
this._initializeLocalizeLite();
|
||||
|
||||
@@ -1,30 +1,17 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isTemplate } from "../../../../../common/string/has-template";
|
||||
import type { WaitAction } from "../../../../../data/script";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../../components/ha-form/types";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "wait_template",
|
||||
selector: {
|
||||
template: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "timeout",
|
||||
required: false,
|
||||
selector: {
|
||||
text: {},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "continue_on_timeout",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const;
|
||||
type TimeoutType = "string_template" | "object_template" | "duration";
|
||||
|
||||
@customElement("ha-automation-action-wait_template")
|
||||
export class HaWaitAction extends LitElement implements ActionElement {
|
||||
@@ -38,12 +25,47 @@ export class HaWaitAction extends LitElement implements ActionElement {
|
||||
return { wait_template: "", continue_on_timeout: true };
|
||||
}
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(timeoutType: TimeoutType) =>
|
||||
[
|
||||
{
|
||||
name: "wait_template",
|
||||
selector: { template: {} },
|
||||
},
|
||||
{
|
||||
name: "timeout",
|
||||
required: false,
|
||||
selector:
|
||||
timeoutType === "string_template"
|
||||
? { template: {} }
|
||||
: timeoutType === "object_template"
|
||||
? { object: {} }
|
||||
: { duration: { enable_millisecond: true } },
|
||||
},
|
||||
{
|
||||
name: "continue_on_timeout",
|
||||
selector: { boolean: {} },
|
||||
},
|
||||
] as const satisfies readonly HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const timeout = this.action.timeout;
|
||||
const timeoutType: TimeoutType =
|
||||
typeof timeout === "string" && isTemplate(timeout)
|
||||
? "string_template"
|
||||
: typeof timeout === "object" &&
|
||||
timeout !== null &&
|
||||
Object.values(timeout).some(
|
||||
(v) => typeof v === "string" && isTemplate(v)
|
||||
)
|
||||
? "object_template"
|
||||
: "duration";
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${this.action}
|
||||
.schema=${SCHEMA}
|
||||
.schema=${this._schema(timeoutType)}
|
||||
.disabled=${this.disabled}
|
||||
.computeLabel=${this._computeLabelCallback}
|
||||
></ha-form>
|
||||
@@ -51,7 +73,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
|
||||
}
|
||||
|
||||
private _computeLabelCallback = (
|
||||
schema: SchemaUnion<typeof SCHEMA>
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
): string =>
|
||||
this.hass.localize(
|
||||
`ui.panel.config.automation.editor.actions.type.wait_template.${
|
||||
|
||||
@@ -27,19 +27,15 @@ import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-fab";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-icon";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import type {
|
||||
@@ -72,27 +68,22 @@ import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import "../../../layouts/hass-subpage";
|
||||
import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type {
|
||||
Entries,
|
||||
HomeAssistant,
|
||||
Route,
|
||||
ValueChangedEvent,
|
||||
} from "../../../types";
|
||||
import type { Entries, ValueChangedEvent } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode";
|
||||
import {
|
||||
type EntityRegistryUpdate,
|
||||
showAutomationSaveDialog,
|
||||
} from "./automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveDialog } from "./automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveTimeoutDialog } from "./automation-save-timeout-dialog/show-dialog-automation-save-timeout";
|
||||
import "./blueprint-automation-editor";
|
||||
import {
|
||||
AutomationScriptEditorMixin,
|
||||
automationScriptEditorStyles,
|
||||
} from "./ha-automation-script-editor-mixin";
|
||||
import "./manual-automation-editor";
|
||||
import type { HaManualAutomationEditor } from "./manual-automation-editor";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
@@ -119,53 +110,13 @@ declare global {
|
||||
}
|
||||
|
||||
@customElement("ha-automation-editor")
|
||||
export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
KeyboardShortcutMixin(LitElement)
|
||||
export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationConfig>(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public automationId: string | null = null;
|
||||
|
||||
@property({ attribute: false }) public entityId: string | null = null;
|
||||
|
||||
@property({ attribute: false }) public automations!: AutomationEntity[];
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _config?: AutomationConfig;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
|
||||
@state() private _yamlErrors?: string;
|
||||
|
||||
@state() private _entityId?: string;
|
||||
|
||||
@state() private _mode: "gui" | "yaml" = "gui";
|
||||
|
||||
@state() private _readOnly = false;
|
||||
|
||||
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@state() private _blueprintConfig?: BlueprintAutomationConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
|
||||
transformer: function (this: HaAutomationEditor, value) {
|
||||
return value.find(({ entity_id }) => entity_id === this._entityId);
|
||||
},
|
||||
watch: ["_entityId"],
|
||||
})
|
||||
private _registryEntry?: EntityRegistryEntry;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
_entityRegistry!: EntityRegistryEntry[];
|
||||
@@ -180,24 +131,18 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
private _configSubscriptionsId = 1;
|
||||
|
||||
private _entityRegistryUpdate?: EntityRegistryUpdate;
|
||||
|
||||
private _newAutomationId?: string;
|
||||
|
||||
private _entityRegCreated?: (
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
|
||||
apply: (config) => this._applyUndoRedo(config),
|
||||
currentConfig: () => this._config!,
|
||||
currentConfig: () => this.config!,
|
||||
});
|
||||
|
||||
protected willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
this._entityRegCreated &&
|
||||
this.entityRegCreated &&
|
||||
this._newAutomationId &&
|
||||
changedProps.has("_entityRegistry")
|
||||
) {
|
||||
@@ -207,26 +152,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
entity.unique_id === this._newAutomationId
|
||||
);
|
||||
if (automation) {
|
||||
this._entityRegCreated(automation);
|
||||
this._entityRegCreated = undefined;
|
||||
this.entityRegCreated(automation);
|
||||
this.entityRegCreated = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config) {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`;
|
||||
if (!this.config) {
|
||||
return this.renderLoading();
|
||||
}
|
||||
|
||||
const stateObj = this._entityId
|
||||
? this.hass.states[this._entityId]
|
||||
const stateObj = this.currentEntityId
|
||||
? this.hass.states[this.currentEntityId]
|
||||
: undefined;
|
||||
|
||||
const useBlueprint = "use_blueprint" in this._config;
|
||||
const useBlueprint = "use_blueprint" in this.config;
|
||||
const shortcutIcon = isMac
|
||||
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
|
||||
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
|
||||
@@ -236,11 +177,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.backCallback=${this._backTapped}
|
||||
.header=${this._config.alias ||
|
||||
.backCallback=${this.backTapped}
|
||||
.header=${this.config.alias ||
|
||||
this.hass.localize("ui.panel.config.automation.editor.default_name")}
|
||||
>
|
||||
${this._mode === "gui" && !this.narrow
|
||||
${this.mode === "gui" && !this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.undo")}
|
||||
@@ -284,7 +225,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
</span>
|
||||
</ha-tooltip>`
|
||||
: nothing}
|
||||
${this._config?.id && !this.narrow
|
||||
${this.config?.id && !this.narrow
|
||||
? html`
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@@ -308,7 +249,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${this._mode === "gui" && this.narrow
|
||||
${this.mode === "gui" && this.narrow
|
||||
? html`<ha-dropdown-item
|
||||
value="undo"
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
@@ -342,7 +283,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
<ha-dropdown-item .disabled=${!stateObj} value="category">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
|
||||
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.automation ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
@@ -366,9 +307,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
<ha-dropdown-item
|
||||
value="rename"
|
||||
.disabled=${this._readOnly ||
|
||||
.disabled=${this.readOnly ||
|
||||
!this.automationId ||
|
||||
this._mode === "yaml"}
|
||||
this.mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.automation.editor.rename")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
@@ -377,7 +318,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
@click=${this._promptAutomationMode}
|
||||
.disabled=${this._readOnly || this._mode === "yaml"}
|
||||
.disabled=${this.readOnly || this.mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.change_mode"
|
||||
@@ -391,12 +332,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
: nothing}
|
||||
|
||||
<ha-dropdown-item
|
||||
.disabled=${!!this._blueprintConfig ||
|
||||
(!this._readOnly && !this.automationId)}
|
||||
.disabled=${!!this.blueprintConfig ||
|
||||
(!this.readOnly && !this.automationId)}
|
||||
value="duplicate"
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._readOnly
|
||||
this.readOnly
|
||||
? "ui.panel.config.automation.editor.migrate"
|
||||
: "ui.panel.config.automation.editor.duplicate"
|
||||
)}
|
||||
@@ -410,7 +351,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
value="take_control"
|
||||
.disabled=${this._readOnly}
|
||||
.disabled=${this.readOnly}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.take_control"
|
||||
@@ -422,7 +363,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
<ha-dropdown-item value="toggle_yaml_mode">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
|
||||
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
@@ -456,10 +397,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div
|
||||
class=${this._mode === "yaml" ? "yaml-mode" : ""}
|
||||
class=${this.mode === "yaml" ? "yaml-mode" : ""}
|
||||
@subscribe-automation-config=${this._subscribeAutomationConfig}
|
||||
>
|
||||
${this._mode === "gui"
|
||||
${this.mode === "gui"
|
||||
? html`
|
||||
<div>
|
||||
${useBlueprint
|
||||
@@ -469,10 +410,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.saving=${this._saving}
|
||||
.dirty=${this._dirty}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.dirty=${this.dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
></blueprint-automation-editor>
|
||||
@@ -483,16 +424,16 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.stateObj=${stateObj}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.dirty=${this._dirty}
|
||||
.saving=${this._saving}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.dirty=${this.dirty}
|
||||
.saving=${this.saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
${this.errors || stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
@@ -501,7 +442,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
${this.errors || this.validationErrors}
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
@@ -510,7 +451,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this._blueprintConfig
|
||||
${this.blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.confirm_take_control"
|
||||
@@ -518,21 +459,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
<div slot="action" style="display: flex;">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._takeControlSave}
|
||||
@click=${this.takeControlSave}
|
||||
>${this.hass.localize(
|
||||
"ui.common.yes"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._revertBlueprint}
|
||||
@click=${this.revertBlueprint}
|
||||
>${this.hass.localize(
|
||||
"ui.common.no"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
: this.readOnly
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
dismissable
|
||||
@@ -575,7 +516,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
: this.mode === "yaml"
|
||||
? html`${stateObj?.state === "off"
|
||||
? html`
|
||||
<ha-alert alert-type="info">
|
||||
@@ -598,7 +539,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
.readOnly=${this.readOnly}
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
.showErrors=${false}
|
||||
@@ -606,9 +547,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
></ha-yaml-editor>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${this._dirty ? "dirty" : ""}
|
||||
class=${this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this._saving}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._handleSaveAutomation}
|
||||
>
|
||||
@@ -645,7 +586,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this.hass
|
||||
) {
|
||||
const initData = getAutomationEditorInitData();
|
||||
this._dirty = !!initData;
|
||||
this.dirty = !!initData;
|
||||
let baseConfig: Partial<AutomationConfig> = { description: "" };
|
||||
if (!initData || !("use_blueprint" in initData)) {
|
||||
baseConfig = {
|
||||
@@ -656,35 +597,35 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
actions: [],
|
||||
};
|
||||
}
|
||||
this._config = {
|
||||
this.config = {
|
||||
...baseConfig,
|
||||
...(initData ? normalizeAutomationConfig(initData) : initData),
|
||||
} as AutomationConfig;
|
||||
this._entityId = undefined;
|
||||
this._readOnly = false;
|
||||
this.currentEntityId = undefined;
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getAutomationStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this._config = normalizeAutomationConfig(c.config);
|
||||
this.config = normalizeAutomationConfig(c.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
this._entityId = this.entityId;
|
||||
this._dirty = false;
|
||||
this._readOnly = true;
|
||||
this.currentEntityId = this.entityId;
|
||||
this.dirty = false;
|
||||
this.readOnly = true;
|
||||
}
|
||||
|
||||
if (
|
||||
changedProps.has("automations") &&
|
||||
this.automationId &&
|
||||
!this._entityId
|
||||
!this.currentEntityId
|
||||
) {
|
||||
this._setEntityId();
|
||||
}
|
||||
|
||||
if (changedProps.has("_config")) {
|
||||
if (changedProps.has("config")) {
|
||||
Object.values(this._configSubscriptions).forEach((sub) =>
|
||||
sub(this._config)
|
||||
sub(this.config)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -693,24 +634,24 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
const automation = this.automations.find(
|
||||
(entity: AutomationEntity) => entity.attributes.id === this.automationId
|
||||
);
|
||||
this._entityId = automation?.entity_id;
|
||||
this.currentEntityId = automation?.entity_id;
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this._validationErrors = undefined;
|
||||
if (!this._entityId || !this._config) {
|
||||
this.validationErrors = undefined;
|
||||
if (!this.currentEntityId || !this.config) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
const stateObj = this.hass.states[this.currentEntityId];
|
||||
if (stateObj?.state !== UNAVAILABLE) {
|
||||
return;
|
||||
}
|
||||
const validation = await validateConfig(this.hass, {
|
||||
triggers: this._config.triggers,
|
||||
conditions: this._config.conditions,
|
||||
actions: this._config.actions,
|
||||
triggers: this.config.triggers,
|
||||
conditions: this.config.conditions,
|
||||
actions: this.config.actions,
|
||||
});
|
||||
this._validationErrors = (
|
||||
this.validationErrors = (
|
||||
Object.entries(validation) as Entries<typeof validation>
|
||||
).map(([key, value]) =>
|
||||
value.valid
|
||||
@@ -728,9 +669,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this.hass,
|
||||
this.automationId as string
|
||||
);
|
||||
this._dirty = false;
|
||||
this._readOnly = false;
|
||||
this._config = normalizeAutomationConfig(config);
|
||||
this.dirty = false;
|
||||
this.readOnly = false;
|
||||
this.config = normalizeAutomationConfig(config);
|
||||
this._checkValidation();
|
||||
} catch (err: any) {
|
||||
const entity = this._entityRegistry.find(
|
||||
@@ -761,34 +702,27 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
|
||||
ev.stopPropagation();
|
||||
|
||||
if (this._config) {
|
||||
this._undoRedoController.commit(this._config);
|
||||
if (this.config) {
|
||||
this._undoRedoController.commit(this.config);
|
||||
}
|
||||
|
||||
this._config = ev.detail.value;
|
||||
if (this._readOnly) {
|
||||
this.config = ev.detail.value;
|
||||
if (this.readOnly) {
|
||||
return;
|
||||
}
|
||||
this._dirty = true;
|
||||
this._errors = undefined;
|
||||
this.dirty = true;
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
private _showInfo() {
|
||||
if (!this.hass || !this._entityId) {
|
||||
if (!this.hass || !this.currentEntityId) {
|
||||
return;
|
||||
}
|
||||
fireEvent(this, "hass-more-info", { entityId: this._entityId });
|
||||
}
|
||||
|
||||
private _showSettings() {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._entityId!,
|
||||
view: "settings",
|
||||
});
|
||||
fireEvent(this, "hass-more-info", { entityId: this.currentEntityId });
|
||||
}
|
||||
|
||||
private _editCategory() {
|
||||
if (!this._registryEntry) {
|
||||
if (!this.registryEntry) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.scene.picker.no_category_support"
|
||||
@@ -801,36 +735,36 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
showAssignCategoryDialog(this, {
|
||||
scope: "automation",
|
||||
entityReg: this._registryEntry,
|
||||
entityReg: this.registryEntry,
|
||||
});
|
||||
}
|
||||
|
||||
private async _showTrace() {
|
||||
if (this._config?.id) {
|
||||
const result = await this._confirmUnsavedChanged();
|
||||
if (this.config?.id) {
|
||||
const result = await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
navigate(
|
||||
`/config/automation/trace/${encodeURIComponent(this._config.id)}`
|
||||
`/config/automation/trace/${encodeURIComponent(this.config.id)}`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _runActions() {
|
||||
if (!this.hass || !this._entityId) {
|
||||
if (!this.hass || !this.currentEntityId) {
|
||||
return;
|
||||
}
|
||||
triggerAutomationActions(
|
||||
this.hass,
|
||||
this.hass.states[this._entityId].entity_id
|
||||
this.hass.states[this.currentEntityId].entity_id
|
||||
);
|
||||
}
|
||||
|
||||
private async _toggle(): Promise<void> {
|
||||
if (!this.hass || !this._entityId) {
|
||||
if (!this.hass || !this.currentEntityId) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
const stateObj = this.hass.states[this.currentEntityId];
|
||||
const service = stateObj.state === "off" ? "turn_on" : "turn_off";
|
||||
await this.hass.callService("automation", service, {
|
||||
entity_id: stateObj.entity_id,
|
||||
@@ -838,42 +772,42 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private _preprocessYaml() {
|
||||
if (!this._config) {
|
||||
if (!this.config) {
|
||||
return {};
|
||||
}
|
||||
const cleanConfig: AutomationConfig = { ...this._config };
|
||||
const cleanConfig: AutomationConfig = { ...this.config };
|
||||
delete cleanConfig.id;
|
||||
return cleanConfig;
|
||||
}
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._dirty = true;
|
||||
this.dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlErrors = ev.detail.errorMsg;
|
||||
this.yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._config = {
|
||||
id: this._config?.id,
|
||||
this.yamlErrors = undefined;
|
||||
this.config = {
|
||||
id: this.config?.id,
|
||||
...normalizeAutomationConfig(ev.detail.value),
|
||||
};
|
||||
this._errors = undefined;
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
private async _confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this._dirty) {
|
||||
protected async confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.dirty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
showAutomationSaveDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
domain: "automation",
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this._config = config;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
|
||||
const id = this.automationId || String(Date.now());
|
||||
@@ -889,8 +823,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
onDiscard: () => resolve(true),
|
||||
entityRegistryUpdate: this._entityRegistryUpdate,
|
||||
entityRegistryEntry: this._registryEntry,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.registryEntry,
|
||||
title: this.hass.localize(
|
||||
this.automationId
|
||||
? "ui.panel.config.automation.editor.leave.unsaved_confirm_title"
|
||||
@@ -906,15 +840,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
});
|
||||
}
|
||||
|
||||
private _backTapped = async () => {
|
||||
const result = await this._confirmUnsavedChanged();
|
||||
if (result) {
|
||||
afterNextRender(() => goBack("/config"));
|
||||
}
|
||||
};
|
||||
|
||||
private async _takeControl() {
|
||||
const config = this._config as BlueprintAutomationConfig;
|
||||
const config = this.config as BlueprintAutomationConfig;
|
||||
|
||||
try {
|
||||
const result = await substituteBlueprint(
|
||||
@@ -931,35 +858,20 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
description: config.description,
|
||||
};
|
||||
|
||||
this._blueprintConfig = config;
|
||||
this._config = newConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
this.blueprintConfig = config;
|
||||
this.config = newConfig;
|
||||
if (this.mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
|
||||
}
|
||||
this._readOnly = true;
|
||||
this._errors = undefined;
|
||||
this.readOnly = true;
|
||||
this.errors = undefined;
|
||||
} catch (err: any) {
|
||||
this._errors = err.message;
|
||||
this.errors = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _revertBlueprint() {
|
||||
this._config = this._blueprintConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
}
|
||||
this._blueprintConfig = undefined;
|
||||
this._readOnly = false;
|
||||
}
|
||||
|
||||
private _takeControlSave() {
|
||||
this._readOnly = false;
|
||||
this._dirty = true;
|
||||
this._blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
const result = this._readOnly
|
||||
const result = this.readOnly
|
||||
? await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.migrate_automation"
|
||||
@@ -968,12 +880,12 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
"ui.panel.config.automation.picker.migrate_automation_description"
|
||||
),
|
||||
})
|
||||
: await this._confirmUnsavedChanged();
|
||||
: await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
showAutomationEditor({
|
||||
...this._config,
|
||||
...this.config,
|
||||
id: undefined,
|
||||
alias: this._readOnly ? this._config?.alias : undefined,
|
||||
alias: this.readOnly ? this.config?.alias : undefined,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -985,7 +897,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.automation.picker.delete_confirm_text",
|
||||
{ name: this._config?.alias }
|
||||
{ name: this.config?.alias }
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
@@ -1001,43 +913,21 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
}
|
||||
|
||||
private async _switchUiMode() {
|
||||
if (this._yamlErrors) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.switch_ui_yaml_error"
|
||||
)}<br /><br />${this._yamlErrors}`,
|
||||
confirmText: this.hass!.localize("ui.common.continue"),
|
||||
destructive: true,
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._mode = "gui";
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._mode = "yaml";
|
||||
}
|
||||
|
||||
private async _promptAutomationAlias(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
showAutomationSaveDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
domain: "automation",
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this._config = config;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
entityRegistryUpdate: this._entityRegistryUpdate,
|
||||
entityRegistryEntry: this._registryEntry,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.registryEntry,
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -1045,10 +935,10 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
private async _promptAutomationMode(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
showAutomationModeDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
updateConfig: (config) => {
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve();
|
||||
},
|
||||
@@ -1058,9 +948,9 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private async _handleSaveAutomation(): Promise<void> {
|
||||
if (this._yamlErrors) {
|
||||
if (this.yamlErrors) {
|
||||
showToast(this, {
|
||||
message: this._yamlErrors,
|
||||
message: this.yamlErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -1082,22 +972,22 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
}
|
||||
|
||||
private async _saveAutomation(id): Promise<void> {
|
||||
this._saving = true;
|
||||
this._validationErrors = undefined;
|
||||
this.saving = true;
|
||||
this.validationErrors = undefined;
|
||||
|
||||
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
|
||||
if (this._entityRegistryUpdate !== undefined && !this._entityId) {
|
||||
if (this.entityRegistryUpdate !== undefined && !this.currentEntityId) {
|
||||
this._newAutomationId = id;
|
||||
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
|
||||
this._entityRegCreated = resolve;
|
||||
this.entityRegCreated = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
try {
|
||||
await saveAutomationConfig(this.hass, id, this._config!);
|
||||
await saveAutomationConfig(this.hass, id, this.config!);
|
||||
|
||||
if (this._entityRegistryUpdate !== undefined) {
|
||||
let entityId = this._entityId;
|
||||
if (this.entityRegistryUpdate !== undefined) {
|
||||
let entityId = this.currentEntityId;
|
||||
|
||||
// wait for automation to appear in entity registry when creating a new automation
|
||||
if (entityRegPromise) {
|
||||
@@ -1131,23 +1021,23 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
if (entityId) {
|
||||
await updateEntityRegistryEntry(this.hass, entityId, {
|
||||
categories: {
|
||||
automation: this._entityRegistryUpdate.category || null,
|
||||
automation: this.entityRegistryUpdate.category || null,
|
||||
},
|
||||
labels: this._entityRegistryUpdate.labels || [],
|
||||
area_id: this._entityRegistryUpdate.area || null,
|
||||
labels: this.entityRegistryUpdate.labels || [],
|
||||
area_id: this.entityRegistryUpdate.area || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
this.dirty = false;
|
||||
} catch (errors: any) {
|
||||
this._errors = errors.body?.message || errors.error || errors.body;
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showToast(this, {
|
||||
message: errors.body?.message || errors.error || errors.body,
|
||||
});
|
||||
throw errors;
|
||||
} finally {
|
||||
this._saving = false;
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1157,7 +1047,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
ev.detail.unsub = () => {
|
||||
delete this._configSubscriptions[id];
|
||||
};
|
||||
ev.detail.callback(this._config);
|
||||
ev.detail.callback(this.config);
|
||||
}
|
||||
|
||||
protected supportedShortcuts(): SupportedShortcuts {
|
||||
@@ -1173,14 +1063,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
};
|
||||
}
|
||||
|
||||
protected get isDirty() {
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this._confirmUnsavedChanged();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
private _collapseAll() {
|
||||
this._manualEditor?.collapseAll();
|
||||
@@ -1205,8 +1087,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
|
||||
private _applyUndoRedo(config: AutomationConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
@@ -1235,7 +1117,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this._showInfo();
|
||||
break;
|
||||
case "settings":
|
||||
this._showSettings();
|
||||
this.showSettings();
|
||||
break;
|
||||
case "category":
|
||||
this._editCategory();
|
||||
@@ -1256,11 +1138,11 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
this._takeControl();
|
||||
break;
|
||||
case "toggle_yaml_mode":
|
||||
if (this._mode === "gui") {
|
||||
this._switchYamlMode();
|
||||
if (this.mode === "gui") {
|
||||
this.switchYamlMode();
|
||||
break;
|
||||
}
|
||||
this._switchUiMode();
|
||||
this.switchUiMode();
|
||||
break;
|
||||
case "disable":
|
||||
this._toggle();
|
||||
@@ -1277,25 +1159,8 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
automationScriptEditorStyles,
|
||||
css`
|
||||
:host {
|
||||
--ha-automation-editor-max-width: var(
|
||||
--ha-automation-editor-width,
|
||||
1540px
|
||||
);
|
||||
}
|
||||
ha-fade-in {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
manual-automation-editor,
|
||||
blueprint-automation-editor {
|
||||
margin: 0 auto;
|
||||
@@ -1309,17 +1174,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: var(--ha-border-radius-square);
|
||||
--code-mirror-height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-entity-toggle {
|
||||
margin-right: 8px;
|
||||
margin-inline-end: 8px;
|
||||
@@ -1335,24 +1189,6 @@ export class HaAutomationEditor extends PreventUnsavedMixin(
|
||||
max-width: 1040px;
|
||||
padding: 28px 20px 0;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
ha-tooltip ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
ha-tooltip .shortcut {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
import { consume } from "@lit/context";
|
||||
import type { CSSResult, TemplateResult, LitElement } from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { goBack } from "../../../common/navigate";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import { showConfirmationDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import type { Constructor, HomeAssistant, Route } from "../../../types";
|
||||
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
|
||||
import "../../../components/ha-fade-in";
|
||||
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
|
||||
|
||||
/** Minimum config shape shared by both AutomationConfig and ScriptConfig. */
|
||||
interface BaseEditorConfig {
|
||||
alias?: string;
|
||||
}
|
||||
|
||||
/** Shared CSS styles for both automation and script editors. */
|
||||
export const automationScriptEditorStyles: CSSResult = css`
|
||||
:host {
|
||||
--ha-automation-editor-max-width: var(--ha-automation-editor-width, 1540px);
|
||||
}
|
||||
ha-fade-in {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: var(--ha-border-radius-square);
|
||||
--code-mirror-height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
ha-tooltip ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
ha-tooltip .shortcut {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
`;
|
||||
|
||||
export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
superClass: Constructor<LitElement>
|
||||
) => {
|
||||
class AutomationScriptEditorClass extends superClass {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@property({ attribute: false }) public entityId: string | null = null;
|
||||
|
||||
@state() protected dirty = false;
|
||||
|
||||
@state() protected errors?: string;
|
||||
|
||||
@state() protected yamlErrors?: string;
|
||||
|
||||
@state() protected currentEntityId?: string;
|
||||
|
||||
@state() protected mode: "gui" | "yaml" = "gui";
|
||||
|
||||
@state() protected readOnly = false;
|
||||
|
||||
@state() protected saving = false;
|
||||
|
||||
@state() protected validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@state() protected config?: TConfig;
|
||||
|
||||
@state() protected blueprintConfig?: TConfig;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
|
||||
transformer: function (this: { currentEntityId?: string }, value) {
|
||||
return value.find(
|
||||
({ entity_id }) => entity_id === this.currentEntityId
|
||||
);
|
||||
},
|
||||
watch: ["currentEntityId"],
|
||||
})
|
||||
protected registryEntry?: EntityRegistryEntry;
|
||||
|
||||
protected entityRegistryUpdate?: EntityRegistryUpdate;
|
||||
|
||||
protected entityRegCreated?: (
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
protected renderLoading(): TemplateResult {
|
||||
return html`
|
||||
<ha-fade-in .delay=${500}>
|
||||
<ha-spinner size="large"></ha-spinner>
|
||||
</ha-fade-in>
|
||||
`;
|
||||
}
|
||||
|
||||
protected showSettings() {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this.currentEntityId!,
|
||||
view: "settings",
|
||||
});
|
||||
}
|
||||
|
||||
protected async switchUiMode() {
|
||||
if (this.yamlErrors) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.switch_ui_yaml_error"
|
||||
)}<br /><br />${this.yamlErrors}`,
|
||||
confirmText: this.hass!.localize("ui.common.continue"),
|
||||
destructive: true,
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this.yamlErrors = undefined;
|
||||
this.mode = "gui";
|
||||
}
|
||||
|
||||
protected switchYamlMode() {
|
||||
this.mode = "yaml";
|
||||
}
|
||||
|
||||
protected takeControlSave() {
|
||||
this.readOnly = false;
|
||||
this.dirty = true;
|
||||
this.blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
protected revertBlueprint() {
|
||||
this.config = this.blueprintConfig;
|
||||
if (this.mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
|
||||
}
|
||||
this.blueprintConfig = undefined;
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
protected backTapped = async () => {
|
||||
const result = await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
afterNextRender(() => goBack("/config"));
|
||||
}
|
||||
};
|
||||
|
||||
protected get isDirty() {
|
||||
return this.dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this.confirmUnsavedChanged();
|
||||
}
|
||||
|
||||
/**
|
||||
* Asks whether unsaved changes should be discarded.
|
||||
* Subclasses must override this to show a confirmation dialog.
|
||||
* @returns true to proceed (discard/save changes), false to cancel.
|
||||
*/
|
||||
protected confirmUnsavedChanged(): Promise<boolean> {
|
||||
return Promise.resolve(true);
|
||||
}
|
||||
}
|
||||
return AutomationScriptEditorClass;
|
||||
};
|
||||
@@ -279,7 +279,7 @@ export default class HaAutomationSidebarAction extends LitElement {
|
||||
<span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span>
|
||||
</div>
|
||||
</ha-dropdown-item>
|
||||
<wa-divider></wa-divider>`
|
||||
<wa-divider slot="menu-items"></wa-divider>`
|
||||
: nothing}
|
||||
<ha-dropdown-item
|
||||
slot="menu-items"
|
||||
|
||||
@@ -21,6 +21,7 @@ import "../../../components/ha-menu-button";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tip";
|
||||
import "../../../components/ha-top-app-bar-fixed";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { CloudStatus } from "../../../data/cloud";
|
||||
import type { RepairsIssue } from "../../../data/repairs";
|
||||
import {
|
||||
@@ -211,6 +212,17 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const quickBarLabel = [
|
||||
this.hass.localize("ui.dialogs.quick-bar.title"),
|
||||
this.hass.enableShortcuts && !isMobileClient
|
||||
? isMac
|
||||
? "(⌘ + K)"
|
||||
: "(Ctrl + K)"
|
||||
: undefined,
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(" ");
|
||||
|
||||
const { updates: canInstallUpdates, total: totalUpdates } =
|
||||
this._filterUpdateEntitiesParameterized(
|
||||
this.hass.states,
|
||||
@@ -231,10 +243,15 @@ class HaConfigDashboard extends SubscribeMixin(LitElement) {
|
||||
|
||||
<ha-icon-button
|
||||
slot="actionItems"
|
||||
.label=${this.hass.localize("ui.dialogs.quick-bar.title")}
|
||||
id="button-quick-bar"
|
||||
.label=${quickBarLabel}
|
||||
.path=${mdiMagnify}
|
||||
hide-title
|
||||
@click=${this._showQuickBar}
|
||||
></ha-icon-button>
|
||||
<ha-tooltip placement="bottom" for="button-quick-bar"
|
||||
>${quickBarLabel}</ha-tooltip
|
||||
>
|
||||
<ha-dropdown slot="actionItems" @wa-select=${this._handleMenuAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
|
||||
@@ -110,6 +110,13 @@ class DialogSystemLogDetail extends LitElement {
|
||||
${item.name}<br />
|
||||
${this.hass.localize("ui.panel.config.logs.detail.source")}:
|
||||
${item.source.join(":")}
|
||||
<br />
|
||||
${this.hass.localize("ui.panel.config.logs.classification")}:
|
||||
${item.error_type
|
||||
? this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.logs.other")}
|
||||
${integration
|
||||
? html`
|
||||
<br />
|
||||
|
||||
@@ -1,10 +1,16 @@
|
||||
import {
|
||||
mdiDotsVertical,
|
||||
mdiChevronDown,
|
||||
mdiChip,
|
||||
mdiDns,
|
||||
mdiDownload,
|
||||
mdiFilterVariant,
|
||||
mdiFilterVariantRemove,
|
||||
mdiPackageVariant,
|
||||
mdiPuzzle,
|
||||
mdiRadar,
|
||||
mdiRefresh,
|
||||
mdiText,
|
||||
mdiVolumeHigh,
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, TemplateResult } from "lit";
|
||||
@@ -17,10 +23,14 @@ import { navigate } from "../../../common/navigate";
|
||||
import { stringCompare } from "../../../common/string/compare";
|
||||
import { extractSearchParam } from "../../../common/url/search-params";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/chips/ha-assist-chip";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-generic-picker";
|
||||
import "../../../components/ha-icon-button";
|
||||
import type { HaGenericPicker } from "../../../components/ha-generic-picker";
|
||||
import type { PickerComboBoxItem } from "../../../components/ha-picker-combo-box";
|
||||
import "../../../components/search-input";
|
||||
import "../../../components/search-input-outlined";
|
||||
import type { LogProvider } from "../../../data/error_log";
|
||||
import { fetchHassioAddonsInfo } from "../../../data/hassio/addon";
|
||||
import { showAlertDialog } from "../../../dialogs/generic/show-dialog-box";
|
||||
@@ -28,6 +38,7 @@ import "../../../layouts/hass-subpage";
|
||||
import { mdiHomeAssistant } from "../../../resources/home-assistant-logo-svg";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { HomeAssistant, Route, ValueChangedEvent } from "../../../types";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
import "./error-log-card";
|
||||
import "./system-log-card";
|
||||
import type { SystemLogCard } from "./system-log-card";
|
||||
@@ -81,13 +92,9 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
@state() private _logProviders = logProviders;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
const systemLog = this.systemLog;
|
||||
if (systemLog && systemLog.loaded) {
|
||||
systemLog.fetchData();
|
||||
}
|
||||
}
|
||||
@state() private _showSystemLogFilters = false;
|
||||
|
||||
@state() private _systemLogFiltersCount = 0;
|
||||
|
||||
protected firstUpdated(changedProps): void {
|
||||
super.firstUpdated(changedProps);
|
||||
@@ -98,37 +105,140 @@ export class HaConfigLogs extends LitElement {
|
||||
this._filter = ev.detail.value;
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const search = this.narrow
|
||||
? html`
|
||||
<div slot="header">
|
||||
<search-input
|
||||
class="header"
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="search">
|
||||
<search-input
|
||||
@value-changed=${this._filterChanged}
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
></search-input>
|
||||
</div>
|
||||
`;
|
||||
private _toggleSystemLogFilters = () => {
|
||||
this._showSystemLogFilters = !this._showSystemLogFilters;
|
||||
};
|
||||
|
||||
private _handleSystemLogFiltersChanged(ev: CustomEvent) {
|
||||
this._showSystemLogFilters = ev.detail.open;
|
||||
this._systemLogFiltersCount = ev.detail.count;
|
||||
}
|
||||
|
||||
private _downloadSystemLog = () => {
|
||||
this.systemLog?.downloadLogs();
|
||||
};
|
||||
|
||||
private _refreshSystemLog = () => {
|
||||
this.systemLog?.fetchData();
|
||||
};
|
||||
|
||||
private _clearSystemLog = () => {
|
||||
this.systemLog?.clearLogs();
|
||||
};
|
||||
|
||||
private _clearSystemLogFilters = () => {
|
||||
this.systemLog?.clearFilters();
|
||||
};
|
||||
|
||||
private _handleSystemLogOverflowAction(ev: HaDropdownSelectEvent): void {
|
||||
if (ev.detail.item.value === "show-full-logs") {
|
||||
this._showDetail();
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult {
|
||||
const showSystemLog = this._selectedLogProvider === "core" && !this._detail;
|
||||
const selectedProvider = this._getActiveProvider(this._selectedLogProvider);
|
||||
const header =
|
||||
selectedProvider?.primary ||
|
||||
this.hass.localize("ui.panel.config.logs.caption");
|
||||
|
||||
const searchRow = html`
|
||||
<div
|
||||
class="search-row ${showSystemLog
|
||||
? "with-filters"
|
||||
: ""} ${showSystemLog && this._showSystemLogFilters && !this.narrow
|
||||
? "with-pane"
|
||||
: ""}"
|
||||
>
|
||||
${showSystemLog
|
||||
? this._showSystemLogFilters && !this.narrow
|
||||
? html`
|
||||
<div class="filter-controls">
|
||||
<div class="relative filter-button">
|
||||
<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
active
|
||||
@click=${this._toggleSystemLogFilters}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._systemLogFiltersCount
|
||||
? html`<div class="badge">
|
||||
${this._systemLogFiltersCount}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
<ha-icon-button
|
||||
.path=${mdiFilterVariantRemove}
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.clear_filter"
|
||||
)}
|
||||
.disabled=${!this._systemLogFiltersCount}
|
||||
@click=${this._clearSystemLogFilters}
|
||||
></ha-icon-button>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="relative filter-button">
|
||||
<ha-assist-chip
|
||||
.label=${this.hass.localize(
|
||||
"ui.components.subpage-data-table.filters"
|
||||
)}
|
||||
.active=${this._showSystemLogFilters ||
|
||||
Boolean(this._systemLogFiltersCount)}
|
||||
@click=${this._toggleSystemLogFilters}
|
||||
>
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiFilterVariant}
|
||||
></ha-svg-icon>
|
||||
</ha-assist-chip>
|
||||
${this._systemLogFiltersCount
|
||||
? html`<div class="badge">
|
||||
${this._systemLogFiltersCount}
|
||||
</div>`
|
||||
: nothing}
|
||||
</div>
|
||||
`
|
||||
: nothing}
|
||||
|
||||
<search-input-outlined
|
||||
class="search-input"
|
||||
.hass=${this.hass}
|
||||
.filter=${this._filter}
|
||||
.label=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
.placeholder=${this.hass.localize("ui.panel.config.logs.search")}
|
||||
@value-changed=${this._filterChanged}
|
||||
></search-input-outlined>
|
||||
|
||||
${showSystemLog
|
||||
? html`
|
||||
<ha-assist-chip
|
||||
class="clear-chip"
|
||||
.label=${this.hass.localize("ui.panel.config.logs.clear")}
|
||||
.disabled=${!this.systemLog?.hasItems}
|
||||
@click=${this._clearSystemLog}
|
||||
></ha-assist-chip>
|
||||
`
|
||||
: nothing}
|
||||
</div>
|
||||
`;
|
||||
|
||||
const search = this.narrow
|
||||
? html`<div slot="header">${searchRow}</div>`
|
||||
: searchRow;
|
||||
|
||||
return html`
|
||||
<hass-subpage
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.logs.caption")}
|
||||
.header=${header}
|
||||
back-path="/config/system"
|
||||
>
|
||||
${isComponentLoaded(this.hass, "hassio") && this._logProviders
|
||||
@@ -164,17 +274,48 @@ export class HaConfigLogs extends LitElement {
|
||||
</ha-generic-picker>
|
||||
`
|
||||
: nothing}
|
||||
${showSystemLog
|
||||
? html`
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadSystemLog}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.download_logs"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.path=${mdiRefresh}
|
||||
@click=${this._refreshSystemLog}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown
|
||||
slot="toolbar-icon"
|
||||
@wa-select=${this._handleSystemLogOverflowAction}
|
||||
>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="show-full-logs">
|
||||
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
|
||||
${this.hass.localize("ui.panel.config.logs.show_full_logs")}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
`
|
||||
: nothing}
|
||||
${search}
|
||||
<div class="content">
|
||||
${this._selectedLogProvider === "core" && !this._detail
|
||||
${showSystemLog
|
||||
? html`
|
||||
<system-log-card
|
||||
.hass=${this.hass}
|
||||
.header=${this._logProviders.find(
|
||||
(p) => p.key === this._selectedLogProvider
|
||||
)!.name}
|
||||
.filter=${this._filter}
|
||||
@switch-log-view=${this._showDetail}
|
||||
.showFilters=${this._showSystemLogFilters}
|
||||
@system-log-filters-changed=${this
|
||||
._handleSystemLogFiltersChanged}
|
||||
></system-log-card>
|
||||
`
|
||||
: html`<error-log-card
|
||||
@@ -194,6 +335,7 @@ export class HaConfigLogs extends LitElement {
|
||||
|
||||
private _showDetail() {
|
||||
this._detail = !this._detail;
|
||||
this._showSystemLogFilters = false;
|
||||
}
|
||||
|
||||
private _openPicker(ev: Event) {
|
||||
@@ -208,6 +350,8 @@ export class HaConfigLogs extends LitElement {
|
||||
}
|
||||
this._selectedLogProvider = provider;
|
||||
this._filter = "";
|
||||
this._showSystemLogFilters = false;
|
||||
this._systemLogFiltersCount = 0;
|
||||
navigate(`/config/logs?provider=${this._selectedLogProvider}`);
|
||||
}
|
||||
|
||||
@@ -342,24 +486,101 @@ export class HaConfigLogs extends LitElement {
|
||||
-webkit-user-select: initial;
|
||||
-moz-user-select: initial;
|
||||
}
|
||||
.search {
|
||||
.search-row {
|
||||
position: sticky;
|
||||
top: 0;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 56px;
|
||||
width: 100%;
|
||||
gap: var(--ha-space-4);
|
||||
padding: 0 var(--ha-space-4);
|
||||
background: var(--primary-background-color);
|
||||
border-bottom: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
search-input {
|
||||
|
||||
.search-row.with-pane {
|
||||
display: grid;
|
||||
grid-template-columns:
|
||||
var(--sidepane-width, 250px) minmax(0, 1fr)
|
||||
auto;
|
||||
align-items: center;
|
||||
gap: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.search-row.with-pane .filter-controls {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
min-width: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
padding: 0 var(--ha-space-4);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.search-row.with-pane .search-input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
}
|
||||
|
||||
.search-row.with-pane .clear-chip {
|
||||
justify-self: end;
|
||||
margin-inline-start: var(--ha-space-4);
|
||||
margin-inline-end: var(--ha-space-4);
|
||||
}
|
||||
|
||||
search-input-outlined {
|
||||
display: block;
|
||||
--mdc-text-field-fill-color: var(--sidebar-background-color);
|
||||
--mdc-text-field-idle-line-color: var(--divider-color);
|
||||
flex: 1;
|
||||
}
|
||||
search-input.header {
|
||||
--mdc-ripple-color: transparant;
|
||||
margin-left: -16px;
|
||||
margin-inline-start: -16px;
|
||||
margin-inline-end: initial;
|
||||
|
||||
.relative {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.badge {
|
||||
position: absolute;
|
||||
top: -4px;
|
||||
right: -4px;
|
||||
inset-inline-end: -4px;
|
||||
inset-inline-start: initial;
|
||||
min-width: 16px;
|
||||
box-sizing: border-box;
|
||||
border-radius: var(--ha-border-radius-circle);
|
||||
font-size: var(--ha-font-size-xs);
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
background-color: var(--primary-color);
|
||||
line-height: var(--ha-line-height-normal);
|
||||
text-align: center;
|
||||
padding: 0 2px;
|
||||
color: var(--text-primary-color);
|
||||
}
|
||||
.content {
|
||||
direction: ltr;
|
||||
height: calc(
|
||||
100vh -
|
||||
1px - var(--header-height, 0px) - var(
|
||||
--safe-area-inset-top,
|
||||
0px
|
||||
) - var(--safe-area-inset-bottom, 0px) -
|
||||
56px
|
||||
);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
ha-assist-chip {
|
||||
--ha-assist-chip-container-shape: 10px;
|
||||
--ha-assist-chip-container-color: var(--card-background-color);
|
||||
}
|
||||
|
||||
.clear-chip {
|
||||
white-space: nowrap;
|
||||
}
|
||||
ha-generic-picker {
|
||||
--md-list-item-leading-icon-color: var(--ha-color-primary-50);
|
||||
|
||||
@@ -1,21 +1,15 @@
|
||||
import { mdiDotsVertical, mdiDownload, mdiRefresh, mdiText } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import "../../../components/buttons/ha-call-service-button";
|
||||
import "../../../components/ha-card";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-filter-states";
|
||||
import "../../../components/ha-list";
|
||||
import "../../../components/ha-list-item";
|
||||
import "../../../components/ha-spinner";
|
||||
import { getSignedPath } from "../../../data/auth";
|
||||
import { getErrorLogDownloadUrl } from "../../../data/error_log";
|
||||
import { domainToName } from "../../../data/integration";
|
||||
import type { LoggedError } from "../../../data/system_log";
|
||||
import type { LoggedError, SystemLogErrorType } from "../../../data/system_log";
|
||||
import {
|
||||
fetchSystemLog,
|
||||
getLoggedErrorIntegration,
|
||||
@@ -25,7 +19,6 @@ import type { HomeAssistant } from "../../../types";
|
||||
import { fileDownload } from "../../../util/file_download";
|
||||
import { showSystemLogDetailDialog } from "./show-dialog-system-log-detail";
|
||||
import { formatSystemLogTime } from "./util";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("system-log-card")
|
||||
export class SystemLogCard extends LitElement {
|
||||
@@ -33,15 +26,45 @@ export class SystemLogCard extends LitElement {
|
||||
|
||||
@property() public filter = "";
|
||||
|
||||
@property() public header?: string;
|
||||
|
||||
public loaded = false;
|
||||
@property({ type: Boolean, attribute: "show-filters" })
|
||||
public showFilters = false;
|
||||
|
||||
@state() private _items?: LoggedError[];
|
||||
|
||||
@state() private _levelFilter: string[] = [];
|
||||
|
||||
@state() private _errorTypeFilter: (SystemLogErrorType | "unknown")[] = [];
|
||||
|
||||
public async fetchData(): Promise<void> {
|
||||
this._items = undefined;
|
||||
this._items = await fetchSystemLog(this.hass!);
|
||||
this._items = await fetchSystemLog(this.hass);
|
||||
}
|
||||
|
||||
public async clearLogs(): Promise<void> {
|
||||
await this.hass.callService("system_log", "clear");
|
||||
this._items = [];
|
||||
}
|
||||
|
||||
public async downloadLogs(): Promise<void> {
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const downloadUrl = getErrorLogDownloadUrl(this.hass);
|
||||
const logFileName = `home-assistant_${timeString}.log`;
|
||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
public get activeFiltersCount(): number {
|
||||
return this._levelFilter.length + this._errorTypeFilter.length;
|
||||
}
|
||||
|
||||
public get hasItems(): boolean {
|
||||
return (this._items?.length || 0) > 0;
|
||||
}
|
||||
|
||||
public clearFilters(): void {
|
||||
this._levelFilter = [];
|
||||
this._errorTypeFilter = [];
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _timestamp(item: LoggedError): string {
|
||||
@@ -64,8 +87,30 @@ export class SystemLogCard extends LitElement {
|
||||
}
|
||||
|
||||
private _getFilteredItems = memoizeOne(
|
||||
(localize: LocalizeFunc, items: LoggedError[], filter: string) =>
|
||||
(
|
||||
localize: LocalizeFunc,
|
||||
items: LoggedError[],
|
||||
filter: string,
|
||||
levelFilter: string[],
|
||||
errorTypeFilter: (SystemLogErrorType | "unknown")[]
|
||||
) =>
|
||||
items.filter((item: LoggedError) => {
|
||||
if (levelFilter.length && !levelFilter.includes(item.level)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (errorTypeFilter.length) {
|
||||
const matchesKnown =
|
||||
item.error_type !== undefined &&
|
||||
errorTypeFilter.includes(item.error_type);
|
||||
const matchesUnknown =
|
||||
item.error_type === undefined &&
|
||||
errorTypeFilter.includes("unknown");
|
||||
if (!matchesKnown && !matchesUnknown) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (filter) {
|
||||
const integration = getLoggedErrorIntegration(item);
|
||||
return (
|
||||
@@ -74,6 +119,14 @@ export class SystemLogCard extends LitElement {
|
||||
) ||
|
||||
item.source[0].toLowerCase().includes(filter) ||
|
||||
item.name.toLowerCase().includes(filter) ||
|
||||
(item.error_type &&
|
||||
(item.error_type.includes(filter) ||
|
||||
this.hass
|
||||
.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)
|
||||
.toLowerCase()
|
||||
.includes(filter))) ||
|
||||
(integration &&
|
||||
domainToName(localize, integration)
|
||||
.toLowerCase()
|
||||
@@ -82,203 +135,203 @@ export class SystemLogCard extends LitElement {
|
||||
this._multipleMessages(item).toLowerCase().includes(filter)
|
||||
);
|
||||
}
|
||||
|
||||
return item;
|
||||
})
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const filteredItems = this._items
|
||||
? this._getFilteredItems(
|
||||
this.hass.localize,
|
||||
this._items,
|
||||
this.filter.toLowerCase()
|
||||
)
|
||||
: [];
|
||||
if (this._items === undefined) {
|
||||
return html`
|
||||
<div class="loading-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
const filteredItems = this._getFilteredItems(
|
||||
this.hass.localize,
|
||||
this._items,
|
||||
this.filter.toLowerCase(),
|
||||
this._levelFilter,
|
||||
this._errorTypeFilter
|
||||
);
|
||||
|
||||
const levels = [...new Set(this._items.map((item) => item.level))];
|
||||
const errorTypes = [
|
||||
...new Set(
|
||||
this._items
|
||||
.map((item) => item.error_type)
|
||||
.filter((type): type is SystemLogErrorType => Boolean(type))
|
||||
),
|
||||
];
|
||||
|
||||
const integrations = filteredItems.length
|
||||
? filteredItems.map((item) => getLoggedErrorIntegration(item))
|
||||
: [];
|
||||
return html`
|
||||
<div class="system-log-intro">
|
||||
<ha-card outlined>
|
||||
${this._items === undefined
|
||||
? html`
|
||||
<div class="loading-container">
|
||||
<ha-spinner></ha-spinner>
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="header">
|
||||
<h1 class="card-header">${this.header || "Logs"}</h1>
|
||||
<div class="header-buttons">
|
||||
<ha-icon-button
|
||||
.path=${mdiDownload}
|
||||
@click=${this._downloadLogs}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.download_logs"
|
||||
)}
|
||||
></ha-icon-button>
|
||||
<ha-icon-button
|
||||
.path=${mdiRefresh}
|
||||
@click=${this.fetchData}
|
||||
.label=${this.hass.localize("ui.common.refresh")}
|
||||
></ha-icon-button>
|
||||
|
||||
<ha-dropdown @wa-select=${this._handleOverflowAction}>
|
||||
<ha-icon-button
|
||||
slot="trigger"
|
||||
.path=${mdiDotsVertical}
|
||||
.label=${this.hass.localize("ui.common.menu")}
|
||||
></ha-icon-button>
|
||||
<ha-dropdown-item value="show-full-logs">
|
||||
<ha-svg-icon slot="icon" .path=${mdiText}></ha-svg-icon>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.show_full_logs"
|
||||
)}
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
${this._items.length === 0
|
||||
? html`
|
||||
<div class="card-content empty-content">
|
||||
${this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
const hasActiveFilters = this.activeFiltersCount > 0;
|
||||
|
||||
const listContent =
|
||||
this._items.length === 0
|
||||
? html`
|
||||
<div class="card-content empty-content">
|
||||
${this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
`
|
||||
: filteredItems.length === 0 && (this.filter || hasActiveFilters)
|
||||
? html`
|
||||
<div class="card-content">
|
||||
${this.filter
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.logs.no_issues_search",
|
||||
{
|
||||
term: this.filter,
|
||||
}
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.logs.no_issues")}
|
||||
</div>
|
||||
`
|
||||
: html`
|
||||
<div class="list-wrapper">
|
||||
<ha-list>
|
||||
${filteredItems.map(
|
||||
(item, idx) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openLog}
|
||||
.logItem=${item}
|
||||
twoline
|
||||
>
|
||||
${item.message[0]}
|
||||
<span slot="secondary" class="secondary">
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.level.${item.level}`
|
||||
)}</span
|
||||
>) `}
|
||||
${item.error_type
|
||||
? html`(<span class="error-type-text"
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${item.error_type}`
|
||||
)}</span
|
||||
>) `
|
||||
: nothing}
|
||||
${integrations[idx]
|
||||
? `${domainToName(
|
||||
this.hass.localize,
|
||||
integrations[idx]!
|
||||
)}${
|
||||
isCustomIntegrationError(item)
|
||||
? ` (${this.hass.localize(
|
||||
"ui.panel.config.logs.custom_integration"
|
||||
)})`
|
||||
: ""
|
||||
}`
|
||||
: item.source[0]}
|
||||
${item.count > 1
|
||||
? html` - ${this._multipleMessages(item)} `
|
||||
: nothing}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
: filteredItems.length === 0 && this.filter
|
||||
? html`<div class="card-content">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.logs.no_issues_search",
|
||||
{ term: this.filter }
|
||||
)}
|
||||
</div>`
|
||||
: html`<ha-list
|
||||
>${filteredItems.map(
|
||||
(item, idx) => html`
|
||||
<ha-list-item
|
||||
@click=${this._openLog}
|
||||
.logItem=${item}
|
||||
twoline
|
||||
>
|
||||
${item.message[0]}
|
||||
<span slot="secondary" class="secondary">
|
||||
${this._timestamp(item)} –
|
||||
${html`(<span class=${item.level}
|
||||
>${this.hass.localize(
|
||||
`ui.panel.config.logs.level.${item.level}`
|
||||
)}</span
|
||||
>) `}
|
||||
${integrations[idx]
|
||||
? `${domainToName(
|
||||
this.hass!.localize,
|
||||
integrations[idx]!
|
||||
)}${
|
||||
isCustomIntegrationError(item)
|
||||
? ` (${this.hass.localize(
|
||||
"ui.panel.config.logs.custom_integration"
|
||||
)})`
|
||||
: ""
|
||||
}`
|
||||
: item.source[0]}
|
||||
${item.count > 1
|
||||
? html` - ${this._multipleMessages(item)} `
|
||||
: nothing}
|
||||
</span>
|
||||
</ha-list-item>
|
||||
`
|
||||
)}</ha-list
|
||||
>`}
|
||||
)}
|
||||
</ha-list>
|
||||
</div>
|
||||
`;
|
||||
|
||||
<div class="card-actions">
|
||||
<ha-call-service-button
|
||||
.hass=${this.hass}
|
||||
domain="system_log"
|
||||
service="clear"
|
||||
>${this.hass.localize(
|
||||
"ui.panel.config.logs.clear"
|
||||
)}</ha-call-service-button
|
||||
>
|
||||
</div>
|
||||
`}
|
||||
</ha-card>
|
||||
</div>
|
||||
`;
|
||||
return this.showFilters
|
||||
? html`
|
||||
<div class="content-layout">
|
||||
<div class="pane">
|
||||
<div class="pane-content">
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.level_filter"
|
||||
)}
|
||||
.states=${levels.map((level) => ({
|
||||
value: level,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.logs.level.${level}`
|
||||
),
|
||||
}))}
|
||||
.value=${this._levelFilter}
|
||||
@data-table-filter-changed=${this._levelFilterChanged}
|
||||
></ha-filter-states>
|
||||
<ha-filter-states
|
||||
.hass=${this.hass}
|
||||
.label=${this.hass.localize(
|
||||
"ui.panel.config.logs.classification"
|
||||
)}
|
||||
.states=${[
|
||||
...errorTypes.map((errorType) => ({
|
||||
value: errorType,
|
||||
label: this.hass.localize(
|
||||
`ui.panel.config.logs.error_type.${errorType}`
|
||||
),
|
||||
})),
|
||||
{
|
||||
value: "unknown",
|
||||
label: this.hass.localize("ui.panel.config.logs.other"),
|
||||
},
|
||||
]}
|
||||
.value=${this._errorTypeFilter}
|
||||
@data-table-filter-changed=${this._errorTypeFilterChanged}
|
||||
></ha-filter-states>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="content-main">${listContent}</div>
|
||||
</div>
|
||||
`
|
||||
: listContent;
|
||||
}
|
||||
|
||||
protected firstUpdated(changedProps): void {
|
||||
super.firstUpdated(changedProps);
|
||||
this.fetchData();
|
||||
this.loaded = true;
|
||||
this.addEventListener("hass-service-called", (ev) =>
|
||||
this.serviceCalled(ev)
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _levelFilterChanged(ev): void {
|
||||
this._levelFilter = ev.detail.value || [];
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _errorTypeFilterChanged(ev): void {
|
||||
this._errorTypeFilter = ev.detail.value || [];
|
||||
this._notifyFiltersState();
|
||||
}
|
||||
|
||||
private _notifyFiltersState(): void {
|
||||
this.dispatchEvent(
|
||||
new CustomEvent("system-log-filters-changed", {
|
||||
detail: {
|
||||
open: this.showFilters,
|
||||
count: this.activeFiltersCount,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
protected serviceCalled(ev): void {
|
||||
// Check if this is for us
|
||||
if (ev.detail.success && ev.detail.domain === "system_log") {
|
||||
// Do the right thing depending on service
|
||||
if (ev.detail.service === "clear") {
|
||||
this._items = [];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _handleOverflowAction(ev: HaDropdownSelectEvent) {
|
||||
if (ev.detail.item.value === "show-full-logs") {
|
||||
// @ts-ignore
|
||||
fireEvent(this, "switch-log-view");
|
||||
}
|
||||
}
|
||||
|
||||
private async _downloadLogs() {
|
||||
const timeString = new Date().toISOString().replace(/:/g, "-");
|
||||
const downloadUrl = getErrorLogDownloadUrl(this.hass);
|
||||
const logFileName = `home-assistant_${timeString}.log`;
|
||||
const signedUrl = await getSignedPath(this.hass, downloadUrl);
|
||||
fileDownload(signedUrl.path, logFileName);
|
||||
}
|
||||
|
||||
private _openLog(ev: Event): void {
|
||||
const item = (ev.currentTarget as any).logItem;
|
||||
showSystemLogDetailDialog(this, { item });
|
||||
}
|
||||
|
||||
static styles = css`
|
||||
ha-card {
|
||||
padding-top: 8px;
|
||||
:host {
|
||||
display: block;
|
||||
direction: var(--direction);
|
||||
min-height: 0;
|
||||
height: 100%;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
:host {
|
||||
direction: var(--direction);
|
||||
}
|
||||
ha-list {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
.header {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 0 16px;
|
||||
}
|
||||
|
||||
.header-buttons {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
.card-header {
|
||||
color: var(--ha-card-header-color, var(--primary-text-color));
|
||||
font-family: var(--ha-card-header-font-family, inherit);
|
||||
font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
|
||||
letter-spacing: -0.012em;
|
||||
line-height: var(--ha-line-height-expanded);
|
||||
display: block;
|
||||
margin-block-start: 0px;
|
||||
font-weight: var(--ha-font-weight-normal);
|
||||
}
|
||||
|
||||
.system-log-intro {
|
||||
margin: 16px;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.loading-container {
|
||||
@@ -288,6 +341,39 @@ export class SystemLogCard extends LitElement {
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.content-layout {
|
||||
display: flex;
|
||||
min-height: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.content-main {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.list-wrapper {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
min-height: 100%;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.pane {
|
||||
flex: 0 0 var(--sidepane-width, 250px);
|
||||
width: var(--sidepane-width, 250px);
|
||||
border-inline-end: 1px solid var(--divider-color);
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 0;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.pane-content {
|
||||
overflow: auto;
|
||||
background: var(--primary-background-color);
|
||||
}
|
||||
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
}
|
||||
@@ -296,15 +382,33 @@ export class SystemLogCard extends LitElement {
|
||||
color: var(--warning-color);
|
||||
}
|
||||
|
||||
.error-type-text {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.card-content {
|
||||
border-top: 1px solid var(--divider-color);
|
||||
padding-top: 16px;
|
||||
padding-bottom: 16px;
|
||||
min-height: 100%;
|
||||
background: var(--card-background-color);
|
||||
}
|
||||
|
||||
.row-secondary {
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
@media (max-width: 900px) {
|
||||
.content-layout {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.pane {
|
||||
width: 100%;
|
||||
border-inline-end: none;
|
||||
border-top: 1px solid var(--divider-color);
|
||||
}
|
||||
}
|
||||
`;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import "@home-assistant/webawesome/dist/components/divider/divider";
|
||||
import { consume } from "@lit/context";
|
||||
import {
|
||||
mdiAppleKeyboardCommand,
|
||||
mdiCog,
|
||||
@@ -22,15 +21,13 @@ import {
|
||||
} from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { UndoRedoController } from "../../../common/controllers/undo-redo-controller";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { slugify } from "../../../common/string/slugify";
|
||||
import { promiseTimeout } from "../../../common/util/promise-timeout";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/ha-button";
|
||||
import "../../../components/ha-dropdown";
|
||||
import "../../../components/ha-dropdown-item";
|
||||
@@ -40,7 +37,6 @@ import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-yaml-editor";
|
||||
import { substituteBlueprint } from "../../../data/blueprint";
|
||||
import { validateConfig } from "../../../data/config";
|
||||
import { fullEntitiesContext } from "../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import {
|
||||
type EntityRegistryEntry,
|
||||
@@ -67,88 +63,47 @@ import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin";
|
||||
import { PreventUnsavedMixin } from "../../../mixins/prevent-unsaved-mixin";
|
||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
|
||||
import { haStyle } from "../../../resources/styles";
|
||||
import type { Entries, HomeAssistant, Route } from "../../../types";
|
||||
import type { Entries } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import { showAutomationModeDialog } from "../automation/automation-mode-dialog/show-dialog-automation-mode";
|
||||
import type { EntityRegistryUpdate } from "../automation/automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveDialog } from "../automation/automation-save-dialog/show-dialog-automation-save";
|
||||
import { showAutomationSaveTimeoutDialog } from "../automation/automation-save-timeout-dialog/show-dialog-automation-save-timeout";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
import "./blueprint-script-editor";
|
||||
import {
|
||||
AutomationScriptEditorMixin,
|
||||
automationScriptEditorStyles,
|
||||
} from "../automation/ha-automation-script-editor-mixin";
|
||||
import "./manual-script-editor";
|
||||
import type { HaManualScriptEditor } from "./manual-script-editor";
|
||||
import type { HaDropdownSelectEvent } from "../../../components/ha-dropdown";
|
||||
|
||||
@customElement("ha-script-editor")
|
||||
export class HaScriptEditor extends SubscribeMixin(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
AutomationScriptEditorMixin<ScriptConfig>(
|
||||
PreventUnsavedMixin(KeyboardShortcutMixin(LitElement))
|
||||
)
|
||||
) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public scriptId: string | null = null;
|
||||
|
||||
@property({ attribute: false }) public entityId: string | null = null;
|
||||
|
||||
@property({ attribute: false }) public entityRegistry!: EntityRegistryEntry[];
|
||||
|
||||
@property({ attribute: "is-wide", type: Boolean }) public isWide = false;
|
||||
|
||||
@property({ type: Boolean }) public narrow = false;
|
||||
|
||||
@property({ attribute: false }) public route!: Route;
|
||||
|
||||
@state() private _config?: ScriptConfig;
|
||||
|
||||
@state() private _dirty = false;
|
||||
|
||||
@state() private _errors?: string;
|
||||
|
||||
@state() private _yamlErrors?: string;
|
||||
|
||||
@state() private _entityId?: string;
|
||||
|
||||
@state() private _mode: "gui" | "yaml" = "gui";
|
||||
|
||||
@state() private _readOnly = false;
|
||||
|
||||
@state()
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
@transform<EntityRegistryEntry[], EntityRegistryEntry>({
|
||||
transformer: function (this: HaScriptEditor, value) {
|
||||
return value.find(({ entity_id }) => entity_id === this._entityId);
|
||||
},
|
||||
watch: ["_entityId"],
|
||||
})
|
||||
private _registryEntry?: EntityRegistryEntry;
|
||||
|
||||
@query("manual-script-editor")
|
||||
private _manualEditor?: HaManualScriptEditor;
|
||||
|
||||
@state() private _validationErrors?: (string | TemplateResult)[];
|
||||
|
||||
@state() private _blueprintConfig?: BlueprintScriptConfig;
|
||||
|
||||
@state() private _saving = false;
|
||||
|
||||
private _entityRegistryUpdate?: EntityRegistryUpdate;
|
||||
|
||||
private _newScriptId?: string;
|
||||
|
||||
private _entityRegCreated?: (
|
||||
value: PromiseLike<EntityRegistryEntry> | EntityRegistryEntry
|
||||
) => void;
|
||||
|
||||
private _undoRedoController = new UndoRedoController<ScriptConfig>(this, {
|
||||
apply: (config) => this._applyUndoRedo(config),
|
||||
currentConfig: () => this._config!,
|
||||
currentConfig: () => this.config!,
|
||||
});
|
||||
|
||||
protected willUpdate(changedProps) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (
|
||||
this._entityRegCreated &&
|
||||
this.entityRegCreated &&
|
||||
this._newScriptId &&
|
||||
changedProps.has("entityRegistry")
|
||||
) {
|
||||
@@ -157,22 +112,22 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
entity.platform === "script" && entity.unique_id === this._newScriptId
|
||||
);
|
||||
if (script) {
|
||||
this._entityRegCreated(script);
|
||||
this._entityRegCreated = undefined;
|
||||
this.entityRegCreated(script);
|
||||
this.entityRegCreated = undefined;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected render(): TemplateResult | typeof nothing {
|
||||
if (!this._config) {
|
||||
return nothing;
|
||||
if (!this.config) {
|
||||
return this.renderLoading();
|
||||
}
|
||||
|
||||
const stateObj = this._entityId
|
||||
? this.hass.states[this._entityId]
|
||||
const stateObj = this.currentEntityId
|
||||
? this.hass.states[this.currentEntityId]
|
||||
: undefined;
|
||||
|
||||
const useBlueprint = "use_blueprint" in this._config;
|
||||
const useBlueprint = "use_blueprint" in this.config;
|
||||
const shortcutIcon = isMac
|
||||
? html`<ha-svg-icon .path=${mdiAppleKeyboardCommand}></ha-svg-icon>`
|
||||
: this.hass.localize("ui.panel.config.automation.editor.ctrl");
|
||||
@@ -182,11 +137,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.route=${this.route}
|
||||
.backCallback=${this._backTapped}
|
||||
.header=${this._config.alias ||
|
||||
.backCallback=${this.backTapped}
|
||||
.header=${this.config.alias ||
|
||||
this.hass.localize("ui.panel.config.script.editor.default_name")}
|
||||
>
|
||||
${this._mode === "gui" && !this.narrow
|
||||
${this.mode === "gui" && !this.narrow
|
||||
? html`<ha-icon-button
|
||||
slot="toolbar-icon"
|
||||
.label=${this.hass.localize("ui.common.undo")}
|
||||
@@ -252,7 +207,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.path=${mdiDotsVertical}
|
||||
></ha-icon-button>
|
||||
|
||||
${this._mode === "gui" && this.narrow
|
||||
${this.mode === "gui" && this.narrow
|
||||
? html`<ha-dropdown-item
|
||||
value="undo"
|
||||
.disabled=${!this._undoRedoController.canUndo}
|
||||
@@ -286,7 +241,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
<ha-dropdown-item .disabled=${!stateObj} value="category">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.scene.picker.${this._registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
|
||||
`ui.panel.config.scene.picker.${this.registryEntry?.categories?.script ? "edit_category" : "assign_category"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiTag}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
@@ -307,10 +262,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
></ha-svg-icon>
|
||||
</ha-dropdown-item>`
|
||||
: nothing}
|
||||
${!useBlueprint && !("fields" in this._config)
|
||||
${!useBlueprint && !("fields" in this.config)
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
.disabled=${this._readOnly || this._mode === "yaml"}
|
||||
.disabled=${this.readOnly || this.mode === "yaml"}
|
||||
value="add_fields"
|
||||
>
|
||||
${this.hass.localize(
|
||||
@@ -326,9 +281,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
<ha-dropdown-item
|
||||
value="rename"
|
||||
.disabled=${!this.scriptId ||
|
||||
this._readOnly ||
|
||||
this._mode === "yaml"}
|
||||
.disabled=${!this.scriptId || this.readOnly || this.mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize("ui.panel.config.script.editor.rename")}
|
||||
<ha-svg-icon slot="icon" .path=${mdiRenameBox}></ha-svg-icon>
|
||||
@@ -337,7 +290,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
value="change_mode"
|
||||
.disabled=${this._readOnly || this._mode === "yaml"}
|
||||
.disabled=${this.readOnly || this.mode === "yaml"}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.change_mode"
|
||||
@@ -351,12 +304,12 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
: nothing}
|
||||
|
||||
<ha-dropdown-item
|
||||
.disabled=${!!this._blueprintConfig ||
|
||||
(!this._readOnly && !this.scriptId)}
|
||||
.disabled=${!!this.blueprintConfig ||
|
||||
(!this.readOnly && !this.scriptId)}
|
||||
value="duplicate"
|
||||
>
|
||||
${this.hass.localize(
|
||||
this._readOnly
|
||||
this.readOnly
|
||||
? "ui.panel.config.script.editor.migrate"
|
||||
: "ui.panel.config.script.editor.duplicate"
|
||||
)}
|
||||
@@ -370,7 +323,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
? html`
|
||||
<ha-dropdown-item
|
||||
value="take_control"
|
||||
.disabled=${this._readOnly}
|
||||
.disabled=${this.readOnly}
|
||||
>
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.take_control"
|
||||
@@ -382,7 +335,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
<ha-dropdown-item value="toggle_yaml_mode">
|
||||
${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.edit_${this._mode === "gui" ? "yaml" : "ui"}`
|
||||
`ui.panel.config.automation.editor.edit_${this.mode === "gui" ? "yaml" : "ui"}`
|
||||
)}
|
||||
<ha-svg-icon slot="icon" .path=${mdiPlaylistEdit}></ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
@@ -390,7 +343,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
<wa-divider></wa-divider>
|
||||
|
||||
<ha-dropdown-item
|
||||
.disabled=${this._readOnly || !this.scriptId}
|
||||
.disabled=${this.readOnly || !this.scriptId}
|
||||
value="delete"
|
||||
.variant=${this.scriptId ? "danger" : "default"}
|
||||
>
|
||||
@@ -403,8 +356,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
</ha-svg-icon>
|
||||
</ha-dropdown-item>
|
||||
</ha-dropdown>
|
||||
<div class=${this._mode === "yaml" ? "yaml-mode" : ""}>
|
||||
${this._mode === "gui"
|
||||
<div class=${this.mode === "yaml" ? "yaml-mode" : ""}>
|
||||
${this.mode === "gui"
|
||||
? html`
|
||||
<div>
|
||||
${useBlueprint
|
||||
@@ -413,10 +366,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.saving=${this._saving}
|
||||
.dirty=${this._dirty}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.dirty=${this.dirty}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-script=${this._handleSaveScript}
|
||||
></blueprint-script-editor>
|
||||
@@ -426,16 +379,16 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.isWide=${this.isWide}
|
||||
.config=${this._config}
|
||||
.disabled=${this._readOnly}
|
||||
.dirty=${this._dirty}
|
||||
.saving=${this._saving}
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.dirty=${this.dirty}
|
||||
.saving=${this.saving}
|
||||
@value-changed=${this._valueChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@save-script=${this._handleSaveScript}
|
||||
>
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._errors || stateObj?.state === UNAVAILABLE
|
||||
${this.errors || stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${stateObj?.state === UNAVAILABLE
|
||||
@@ -444,7 +397,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
)
|
||||
: undefined}
|
||||
>
|
||||
${this._errors || this._validationErrors}
|
||||
${this.errors || this.validationErrors}
|
||||
${stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
@@ -453,7 +406,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
: nothing}
|
||||
</ha-alert>`
|
||||
: nothing}
|
||||
${this._blueprintConfig
|
||||
${this.blueprintConfig
|
||||
? html`<ha-alert alert-type="info">
|
||||
${this.hass.localize(
|
||||
"ui.panel.config.script.editor.confirm_take_control"
|
||||
@@ -461,21 +414,21 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
<div slot="action" style="display: flex;">
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._takeControlSave}
|
||||
@click=${this.takeControlSave}
|
||||
>${this.hass.localize(
|
||||
"ui.common.yes"
|
||||
)}</ha-button
|
||||
>
|
||||
<ha-button
|
||||
appearance="plain"
|
||||
@click=${this._revertBlueprint}
|
||||
@click=${this.revertBlueprint}
|
||||
>${this.hass.localize(
|
||||
"ui.common.no"
|
||||
)}</ha-button
|
||||
>
|
||||
</div>
|
||||
</ha-alert>`
|
||||
: this._readOnly
|
||||
: this.readOnly
|
||||
? html`<ha-alert
|
||||
alert-type="warning"
|
||||
dismissable
|
||||
@@ -498,11 +451,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
`}
|
||||
</div>
|
||||
`
|
||||
: this._mode === "yaml"
|
||||
: this.mode === "yaml"
|
||||
? html`<ha-yaml-editor
|
||||
.hass=${this.hass}
|
||||
.defaultValue=${this._preprocessYaml()}
|
||||
.readOnly=${this._readOnly}
|
||||
.readOnly=${this.readOnly}
|
||||
disable-fullscreen
|
||||
@value-changed=${this._yamlChanged}
|
||||
@editor-save=${this._handleSaveScript}
|
||||
@@ -510,9 +463,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
></ha-yaml-editor>
|
||||
<ha-fab
|
||||
slot="fab"
|
||||
class=${!this._readOnly && this._dirty ? "dirty" : ""}
|
||||
class=${!this.readOnly && this.dirty ? "dirty" : ""}
|
||||
.label=${this.hass.localize("ui.common.save")}
|
||||
.disabled=${this._saving}
|
||||
.disabled=${this.saving}
|
||||
extended
|
||||
@click=${this._handleSaveScript}
|
||||
>
|
||||
@@ -551,26 +504,26 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
const entity = this.entityRegistry.find(
|
||||
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
|
||||
);
|
||||
this._entityId = entity?.entity_id;
|
||||
this.currentEntityId = entity?.entity_id;
|
||||
}
|
||||
|
||||
if (changedProps.has("scriptId") && !this.scriptId && this.hass) {
|
||||
const initData = getScriptEditorInitData();
|
||||
this._dirty = !!initData;
|
||||
this.dirty = !!initData;
|
||||
const baseConfig: Partial<ScriptConfig> = {};
|
||||
if (!initData || !("use_blueprint" in initData)) {
|
||||
baseConfig.sequence = [];
|
||||
}
|
||||
this._config = {
|
||||
this.config = {
|
||||
...baseConfig,
|
||||
...initData,
|
||||
} as ScriptConfig;
|
||||
this._readOnly = false;
|
||||
this.readOnly = false;
|
||||
}
|
||||
|
||||
if (changedProps.has("entityId") && this.entityId) {
|
||||
getScriptStateConfig(this.hass, this.entityId).then((c) => {
|
||||
this._config = normalizeScriptConfig(c.config);
|
||||
this.config = normalizeScriptConfig(c.config);
|
||||
this._checkValidation();
|
||||
});
|
||||
const regEntry = this.entityRegistry.find(
|
||||
@@ -579,25 +532,25 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
if (regEntry?.unique_id) {
|
||||
this.scriptId = regEntry.unique_id;
|
||||
}
|
||||
this._entityId = this.entityId;
|
||||
this._dirty = false;
|
||||
this._readOnly = true;
|
||||
this.currentEntityId = this.entityId;
|
||||
this.dirty = false;
|
||||
this.readOnly = true;
|
||||
}
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this._validationErrors = undefined;
|
||||
if (!this._entityId || !this._config) {
|
||||
this.validationErrors = undefined;
|
||||
if (!this.currentEntityId || !this.config) {
|
||||
return;
|
||||
}
|
||||
const stateObj = this.hass.states[this._entityId];
|
||||
const stateObj = this.hass.states[this.currentEntityId];
|
||||
if (stateObj?.state !== UNAVAILABLE) {
|
||||
return;
|
||||
}
|
||||
const validation = await validateConfig(this.hass, {
|
||||
actions: this._config.sequence,
|
||||
actions: this.config.sequence,
|
||||
});
|
||||
this._validationErrors = (
|
||||
this.validationErrors = (
|
||||
Object.entries(validation) as Entries<typeof validation>
|
||||
).map(([key, value]) =>
|
||||
value.valid
|
||||
@@ -612,13 +565,13 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
private async _loadConfig() {
|
||||
fetchScriptFileConfig(this.hass, this.scriptId!).then(
|
||||
(config) => {
|
||||
this._dirty = false;
|
||||
this._readOnly = false;
|
||||
this._config = normalizeScriptConfig(config);
|
||||
this.dirty = false;
|
||||
this.readOnly = false;
|
||||
this.config = normalizeScriptConfig(config);
|
||||
const entity = this.entityRegistry.find(
|
||||
(ent) => ent.platform === "script" && ent.unique_id === this.scriptId
|
||||
);
|
||||
this._entityId = entity?.entity_id;
|
||||
this.currentEntityId = entity?.entity_id;
|
||||
this._checkValidation();
|
||||
},
|
||||
(resp) => {
|
||||
@@ -647,19 +600,19 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private _valueChanged(ev) {
|
||||
if (this._config) {
|
||||
this._undoRedoController.commit(this._config);
|
||||
if (this.config) {
|
||||
this._undoRedoController.commit(this.config);
|
||||
}
|
||||
|
||||
this._config = ev.detail.value;
|
||||
this._errors = undefined;
|
||||
this._dirty = true;
|
||||
this.config = ev.detail.value;
|
||||
this.errors = undefined;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private async _runScript() {
|
||||
if (hasScriptFields(this.hass, this._entityId!)) {
|
||||
if (hasScriptFields(this.hass, this.currentEntityId!)) {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._entityId!,
|
||||
entityId: this.currentEntityId!,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -667,20 +620,13 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
await triggerScript(this.hass, this.scriptId!);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.notification_toast.triggered", {
|
||||
name: this._config!.alias,
|
||||
name: this.config!.alias,
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
private _showSettings() {
|
||||
showMoreInfoDialog(this, {
|
||||
entityId: this._entityId!,
|
||||
view: "settings",
|
||||
});
|
||||
}
|
||||
|
||||
private _editCategory() {
|
||||
if (!this._registryEntry) {
|
||||
if (!this.registryEntry) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.scene.picker.no_category_support"
|
||||
@@ -693,7 +639,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
showAssignCategoryDialog(this, {
|
||||
scope: "script",
|
||||
entityReg: this._registryEntry,
|
||||
entityReg: this.registryEntry,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -730,7 +676,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
private async _showTrace() {
|
||||
if (this.scriptId) {
|
||||
const result = await this._confirmUnsavedChanged();
|
||||
const result = await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
navigate(`/config/script/trace/${this.scriptId}`);
|
||||
}
|
||||
@@ -738,47 +684,47 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private _addFields() {
|
||||
if ("fields" in this._config!) {
|
||||
if ("fields" in this.config!) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this._config) {
|
||||
this._undoRedoController.commit(this._config);
|
||||
if (this.config) {
|
||||
this._undoRedoController.commit(this.config);
|
||||
}
|
||||
|
||||
this._manualEditor?.addFields();
|
||||
this._dirty = true;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private _preprocessYaml() {
|
||||
return this._config;
|
||||
return this.config;
|
||||
}
|
||||
|
||||
private _yamlChanged(ev: CustomEvent) {
|
||||
ev.stopPropagation();
|
||||
this._dirty = true;
|
||||
this.dirty = true;
|
||||
if (!ev.detail.isValid) {
|
||||
this._yamlErrors = ev.detail.errorMsg;
|
||||
this.yamlErrors = ev.detail.errorMsg;
|
||||
return;
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._config = ev.detail.value;
|
||||
this._errors = undefined;
|
||||
this.yamlErrors = undefined;
|
||||
this.config = ev.detail.value;
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
private async _confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this._dirty) {
|
||||
protected async confirmUnsavedChanged(): Promise<boolean> {
|
||||
if (!this.dirty) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return new Promise<boolean>((resolve) => {
|
||||
showAutomationSaveDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
domain: "script",
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this._config = config;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
|
||||
const id = this.scriptId || String(Date.now());
|
||||
@@ -794,8 +740,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
onDiscard: () => resolve(true),
|
||||
entityRegistryUpdate: this._entityRegistryUpdate,
|
||||
entityRegistryEntry: this._registryEntry,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.registryEntry,
|
||||
title: this.hass.localize(
|
||||
this.scriptId
|
||||
? "ui.panel.config.script.editor.leave.unsaved_confirm_title"
|
||||
@@ -811,15 +757,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
});
|
||||
}
|
||||
|
||||
private _backTapped = async () => {
|
||||
const result = await this._confirmUnsavedChanged();
|
||||
if (result) {
|
||||
afterNextRender(() => goBack("/config"));
|
||||
}
|
||||
};
|
||||
|
||||
private async _takeControl() {
|
||||
const config = this._config as BlueprintScriptConfig;
|
||||
const config = this.config as BlueprintScriptConfig;
|
||||
|
||||
try {
|
||||
const result = await substituteBlueprint(
|
||||
@@ -835,35 +774,20 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
description: config.description,
|
||||
};
|
||||
|
||||
this._blueprintConfig = config;
|
||||
this._config = newConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
this.blueprintConfig = config;
|
||||
this.config = newConfig;
|
||||
if (this.mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this.config);
|
||||
}
|
||||
this._readOnly = true;
|
||||
this._errors = undefined;
|
||||
this.readOnly = true;
|
||||
this.errors = undefined;
|
||||
} catch (err: any) {
|
||||
this._errors = err.message;
|
||||
this.errors = err.message;
|
||||
}
|
||||
}
|
||||
|
||||
private _revertBlueprint() {
|
||||
this._config = this._blueprintConfig;
|
||||
if (this._mode === "yaml") {
|
||||
this.renderRoot.querySelector("ha-yaml-editor")?.setValue(this._config);
|
||||
}
|
||||
this._blueprintConfig = undefined;
|
||||
this._readOnly = false;
|
||||
}
|
||||
|
||||
private _takeControlSave() {
|
||||
this._readOnly = false;
|
||||
this._dirty = true;
|
||||
this._blueprintConfig = undefined;
|
||||
}
|
||||
|
||||
private async _duplicate() {
|
||||
const result = this._readOnly
|
||||
const result = this.readOnly
|
||||
? await showConfirmationDialog(this, {
|
||||
title: this.hass.localize(
|
||||
"ui.panel.config.script.picker.migrate_script"
|
||||
@@ -872,14 +796,14 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
"ui.panel.config.script.picker.migrate_script_description"
|
||||
),
|
||||
})
|
||||
: await this._confirmUnsavedChanged();
|
||||
: await this.confirmUnsavedChanged();
|
||||
if (result) {
|
||||
this._entityId = undefined;
|
||||
this.currentEntityId = undefined;
|
||||
showScriptEditor({
|
||||
...this._config,
|
||||
alias: this._readOnly
|
||||
? this._config?.alias
|
||||
: `${this._config?.alias} (${this.hass.localize(
|
||||
...this.config,
|
||||
alias: this.readOnly
|
||||
? this.config?.alias
|
||||
: `${this.config?.alias} (${this.hass.localize(
|
||||
"ui.panel.config.script.picker.duplicate"
|
||||
)})`,
|
||||
});
|
||||
@@ -893,7 +817,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
),
|
||||
text: this.hass.localize(
|
||||
"ui.panel.config.script.editor.delete_confirm_text",
|
||||
{ name: this._config?.alias }
|
||||
{ name: this.config?.alias }
|
||||
),
|
||||
confirmText: this.hass!.localize("ui.common.delete"),
|
||||
destructive: true,
|
||||
@@ -907,42 +831,20 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
goBack("/config");
|
||||
}
|
||||
|
||||
private async _switchUiMode() {
|
||||
if (this._yamlErrors) {
|
||||
const result = await showConfirmationDialog(this, {
|
||||
text: html`${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.switch_ui_yaml_error"
|
||||
)}<br /><br />${this._yamlErrors}`,
|
||||
confirmText: this.hass!.localize("ui.common.continue"),
|
||||
destructive: true,
|
||||
dismissText: this.hass!.localize("ui.common.cancel"),
|
||||
});
|
||||
if (!result) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
this._yamlErrors = undefined;
|
||||
this._mode = "gui";
|
||||
}
|
||||
|
||||
private _switchYamlMode() {
|
||||
this._mode = "yaml";
|
||||
}
|
||||
|
||||
private async _promptScriptAlias(): Promise<boolean> {
|
||||
return new Promise((resolve) => {
|
||||
showAutomationSaveDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
domain: "script",
|
||||
updateConfig: async (config, entityRegistryUpdate) => {
|
||||
this._config = config;
|
||||
this._entityRegistryUpdate = entityRegistryUpdate;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.entityRegistryUpdate = entityRegistryUpdate;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve(true);
|
||||
},
|
||||
onClose: () => resolve(false),
|
||||
entityRegistryUpdate: this._entityRegistryUpdate,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
entityRegistryEntry: this.entityRegistry.find(
|
||||
(entry) => entry.unique_id === this.scriptId
|
||||
),
|
||||
@@ -953,10 +855,10 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
private async _promptScriptMode(): Promise<void> {
|
||||
return new Promise((resolve) => {
|
||||
showAutomationModeDialog(this, {
|
||||
config: this._config!,
|
||||
config: this.config!,
|
||||
updateConfig: (config) => {
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
this.requestUpdate();
|
||||
resolve();
|
||||
},
|
||||
@@ -966,9 +868,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private async _handleSaveScript() {
|
||||
if (this._yamlErrors) {
|
||||
if (this.yamlErrors) {
|
||||
showToast(this, {
|
||||
message: this._yamlErrors,
|
||||
message: this.yamlErrors,
|
||||
});
|
||||
return;
|
||||
}
|
||||
@@ -980,9 +882,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
if (!saved) {
|
||||
return;
|
||||
}
|
||||
this._entityId = this._computeEntityIdFromAlias(this._config!.alias);
|
||||
this.currentEntityId = this._computeEntityIdFromAlias(this.config!.alias);
|
||||
}
|
||||
const id = this.scriptId || this._entityId || Date.now();
|
||||
const id = this.scriptId || this.currentEntityId || Date.now();
|
||||
|
||||
await this._saveScript(id);
|
||||
if (!this.scriptId) {
|
||||
@@ -991,13 +893,13 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
}
|
||||
|
||||
private async _saveScript(id): Promise<void> {
|
||||
this._saving = true;
|
||||
this.saving = true;
|
||||
|
||||
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
|
||||
if (this._entityRegistryUpdate !== undefined && !this.scriptId) {
|
||||
if (this.entityRegistryUpdate !== undefined && !this.scriptId) {
|
||||
this._newScriptId = id.toString();
|
||||
entityRegPromise = new Promise<EntityRegistryEntry>((resolve) => {
|
||||
this._entityRegCreated = resolve;
|
||||
this.entityRegCreated = resolve;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1005,11 +907,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
await this.hass!.callApi(
|
||||
"POST",
|
||||
"config/script/config/" + id,
|
||||
this._config
|
||||
this.config
|
||||
);
|
||||
|
||||
if (this._entityRegistryUpdate !== undefined) {
|
||||
let entityId = this._entityId;
|
||||
if (this.entityRegistryUpdate !== undefined) {
|
||||
let entityId = this.currentEntityId;
|
||||
|
||||
// wait for new script to appear in entity registry
|
||||
if (entityRegPromise) {
|
||||
@@ -1044,23 +946,23 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
if (entityId) {
|
||||
await updateEntityRegistryEntry(this.hass, entityId, {
|
||||
categories: {
|
||||
script: this._entityRegistryUpdate.category || null,
|
||||
script: this.entityRegistryUpdate.category || null,
|
||||
},
|
||||
labels: this._entityRegistryUpdate.labels || [],
|
||||
area_id: this._entityRegistryUpdate.area || null,
|
||||
labels: this.entityRegistryUpdate.labels || [],
|
||||
area_id: this.entityRegistryUpdate.area || null,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
this._dirty = false;
|
||||
this.dirty = false;
|
||||
} catch (errors: any) {
|
||||
this._errors = errors.body?.message || errors.error || errors.body;
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showToast(this, {
|
||||
message: errors.body?.message || errors.error || errors.body,
|
||||
});
|
||||
throw errors;
|
||||
} finally {
|
||||
this._saving = false;
|
||||
this.saving = false;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1077,14 +979,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
};
|
||||
}
|
||||
|
||||
protected get isDirty() {
|
||||
return this._dirty;
|
||||
}
|
||||
|
||||
protected async promptDiscardChanges() {
|
||||
return this._confirmUnsavedChanged();
|
||||
}
|
||||
|
||||
// @ts-ignore
|
||||
private _collapseAll() {
|
||||
this._manualEditor?.collapseAll();
|
||||
@@ -1109,8 +1003,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
|
||||
private _applyUndoRedo(config: ScriptConfig) {
|
||||
this._manualEditor?.triggerCloseSidebar();
|
||||
this._config = config;
|
||||
this._dirty = true;
|
||||
this.config = config;
|
||||
this.dirty = true;
|
||||
}
|
||||
|
||||
private _undo() {
|
||||
@@ -1139,7 +1033,7 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
this._showInfo();
|
||||
break;
|
||||
case "settings":
|
||||
this._showSettings();
|
||||
this.showSettings();
|
||||
break;
|
||||
case "category":
|
||||
this._editCategory();
|
||||
@@ -1163,11 +1057,11 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
this._takeControl();
|
||||
break;
|
||||
case "toggle_yaml_mode":
|
||||
if (this._mode === "gui") {
|
||||
this._switchYamlMode();
|
||||
if (this.mode === "gui") {
|
||||
this.switchYamlMode();
|
||||
break;
|
||||
}
|
||||
this._switchUiMode();
|
||||
this.switchUiMode();
|
||||
break;
|
||||
case "delete":
|
||||
this._deleteConfirm();
|
||||
@@ -1181,19 +1075,8 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
automationScriptEditorStyles,
|
||||
css`
|
||||
:host {
|
||||
--ha-automation-editor-max-width: var(
|
||||
--ha-automation-editor-width,
|
||||
1540px
|
||||
);
|
||||
}
|
||||
.yaml-mode {
|
||||
height: 100%;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
manual-script-editor,
|
||||
blueprint-script-editor {
|
||||
margin: 0 auto;
|
||||
@@ -1244,29 +1127,9 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
padding: 0 12px;
|
||||
}
|
||||
|
||||
ha-yaml-editor {
|
||||
flex-grow: 1;
|
||||
--actions-border-radius: var(--ha-border-radius-square);
|
||||
--code-mirror-height: 100%;
|
||||
min-height: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
span[slot="introduction"] a {
|
||||
color: var(--primary-color);
|
||||
}
|
||||
ha-fab {
|
||||
position: fixed;
|
||||
right: 16px;
|
||||
bottom: calc(-80px - var(--safe-area-inset-bottom));
|
||||
transition: bottom 0.3s;
|
||||
}
|
||||
ha-fab.dirty {
|
||||
bottom: calc(16px + var(--safe-area-inset-bottom, 0px));
|
||||
}
|
||||
.header {
|
||||
display: flex;
|
||||
margin: 16px 0;
|
||||
@@ -1280,15 +1143,6 @@ export class HaScriptEditor extends SubscribeMixin(
|
||||
.header a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
ha-tooltip ha-svg-icon {
|
||||
width: 12px;
|
||||
}
|
||||
ha-tooltip .shortcut {
|
||||
display: inline-flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
}
|
||||
`,
|
||||
];
|
||||
}
|
||||
|
||||
@@ -22,7 +22,6 @@ export class EnergyOverviewViewStrategy extends ReactiveElement {
|
||||
dense_section_placement: true,
|
||||
max_columns: 3,
|
||||
footer: {
|
||||
column_span: 1.1,
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -23,7 +23,6 @@ export class EnergyViewStrategy extends ReactiveElement {
|
||||
max_columns: 3,
|
||||
sections: [],
|
||||
footer: {
|
||||
column_span: 1.1,
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -21,7 +21,6 @@ export class GasViewStrategy extends ReactiveElement {
|
||||
max_columns: 3,
|
||||
sections: [{ type: "grid", cards: [], column_span: 3 }],
|
||||
footer: {
|
||||
column_span: 1.1,
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -22,7 +22,6 @@ export class WaterViewStrategy extends ReactiveElement {
|
||||
max_columns: 3,
|
||||
sections: [{ type: "grid", cards: [], column_span: 3 }],
|
||||
footer: {
|
||||
column_span: 1.1,
|
||||
card: {
|
||||
type: "energy-date-selection",
|
||||
collection_key: collectionKey,
|
||||
|
||||
@@ -17,6 +17,7 @@ import {
|
||||
SMALL_SCREEN_CONDITION,
|
||||
} from "../../lovelace/strategies/helpers/screen-conditions";
|
||||
import type { ToggleGroupCardConfig } from "../../lovelace/cards/types";
|
||||
import type { ButtonHeadingBadgeConfig } from "../../lovelace/heading-badges/types";
|
||||
|
||||
export interface LightViewStrategyConfig {
|
||||
type: "light";
|
||||
@@ -75,6 +76,7 @@ const processAreasForLight = (
|
||||
{
|
||||
type: "button",
|
||||
icon: "mdi:power",
|
||||
text: hass.localize("ui.panel.lovelace.strategy.light.off"),
|
||||
tap_action: {
|
||||
action: "perform-action",
|
||||
perform_action: "light.turn_on",
|
||||
@@ -89,11 +91,12 @@ const processAreasForLight = (
|
||||
conditions: [anyOnCondition],
|
||||
},
|
||||
],
|
||||
},
|
||||
} satisfies ButtonHeadingBadgeConfig,
|
||||
{
|
||||
type: "button",
|
||||
icon: "mdi:power",
|
||||
color: "amber",
|
||||
color: "orange",
|
||||
text: hass.localize("ui.panel.lovelace.strategy.light.on"),
|
||||
tap_action: {
|
||||
action: "perform-action",
|
||||
perform_action: "light.turn_off",
|
||||
@@ -102,7 +105,7 @@ const processAreasForLight = (
|
||||
},
|
||||
},
|
||||
visibility: [SMALL_SCREEN_CONDITION, anyOnCondition],
|
||||
},
|
||||
} satisfies ButtonHeadingBadgeConfig,
|
||||
] satisfies LovelaceCardConfig[],
|
||||
});
|
||||
|
||||
|
||||
@@ -610,18 +610,21 @@ class HuiPowerSankeyCard
|
||||
});
|
||||
|
||||
// Collect battery power (positive = discharge, negative = charge)
|
||||
// Sum all battery values first, then determine net direction.
|
||||
// Momentary power should only flow in one direction across all batteries.
|
||||
let net_battery = 0;
|
||||
prefs.energy_sources
|
||||
.filter((source) => source.type === "battery")
|
||||
.forEach((source) => {
|
||||
if (source.type === "battery" && source.stat_rate) {
|
||||
const value = this._getCurrentPower(source.stat_rate);
|
||||
if (value > 0) {
|
||||
from_battery += value;
|
||||
} else if (value < 0) {
|
||||
to_battery += Math.abs(value);
|
||||
}
|
||||
net_battery += this._getCurrentPower(source.stat_rate);
|
||||
}
|
||||
});
|
||||
if (net_battery > 0) {
|
||||
from_battery = net_battery;
|
||||
} else if (net_battery < 0) {
|
||||
to_battery = Math.abs(net_battery);
|
||||
}
|
||||
|
||||
// Calculate total consumption
|
||||
const used_total = from_grid + solar + from_battery - to_grid - to_battery;
|
||||
|
||||
@@ -91,7 +91,6 @@ export class HuiDialogEditViewFooter extends LitElement {
|
||||
<hui-view-footer-settings-editor
|
||||
.hass=${this.hass}
|
||||
.config=${this._config}
|
||||
.maxColumns=${this._params.maxColumns}
|
||||
@config-changed=${this._configChanged}
|
||||
></hui-view-footer-settings-editor>
|
||||
`;
|
||||
@@ -106,7 +105,7 @@ export class HuiDialogEditViewFooter extends LitElement {
|
||||
.hass=${this.hass}
|
||||
.open=${this._open}
|
||||
header-title=${title}
|
||||
.width=${this._yamlMode ? "full" : "large"}
|
||||
width="medium"
|
||||
@closed=${this._dialogClosed}
|
||||
class=${this._yamlMode ? "yaml-mode" : ""}
|
||||
>
|
||||
|
||||
@@ -1,53 +1,48 @@
|
||||
import { html, LitElement } from "lit";
|
||||
import { customElement, property } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import "../../../../components/ha-form/ha-form";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
} from "../../../../components/ha-form/types";
|
||||
import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/view";
|
||||
import {
|
||||
DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
type LovelaceViewFooterConfig,
|
||||
} from "../../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
const SCHEMA = [
|
||||
{
|
||||
name: "max_width",
|
||||
selector: {
|
||||
number: {
|
||||
min: 100,
|
||||
max: 1600,
|
||||
step: 10,
|
||||
unit_of_measurement: "px",
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[];
|
||||
|
||||
@customElement("hui-view-footer-settings-editor")
|
||||
export class HuiViewFooterSettingsEditor extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public config?: LovelaceViewFooterConfig;
|
||||
|
||||
@property({ attribute: false }) public maxColumns = 4;
|
||||
|
||||
private _schema = memoizeOne(
|
||||
(maxColumns: number) =>
|
||||
[
|
||||
{
|
||||
name: "column_span",
|
||||
selector: {
|
||||
number: {
|
||||
min: 1,
|
||||
max: maxColumns,
|
||||
slider_ticks: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
] as const satisfies HaFormSchema[]
|
||||
);
|
||||
|
||||
protected render() {
|
||||
const data = {
|
||||
column_span: this.config?.column_span || 1,
|
||||
max_width: this.config?.max_width || DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
};
|
||||
|
||||
const schema = this._schema(this.maxColumns);
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
.schema=${SCHEMA}
|
||||
.computeLabel=${this._computeLabel}
|
||||
.computeHelper=${this._computeHelper}
|
||||
@value-changed=${this._valueChanged}
|
||||
></ha-form>
|
||||
`;
|
||||
@@ -65,19 +60,10 @@ export class HuiViewFooterSettingsEditor extends LitElement {
|
||||
fireEvent(this, "config-changed", { config });
|
||||
}
|
||||
|
||||
private _computeLabel = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
private _computeLabel = (schema: SchemaUnion<typeof SCHEMA>) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}`
|
||||
);
|
||||
|
||||
private _computeHelper = (
|
||||
schema: SchemaUnion<ReturnType<typeof this._schema>>
|
||||
) =>
|
||||
this.hass.localize(
|
||||
`ui.panel.lovelace.editor.edit_view_footer.settings.${schema.name}_helper`
|
||||
) || "";
|
||||
}
|
||||
|
||||
declare global {
|
||||
|
||||
@@ -4,7 +4,6 @@ import type { LovelaceViewFooterConfig } from "../../../../data/lovelace/config/
|
||||
export interface EditViewFooterDialogParams {
|
||||
saveConfig: (config: LovelaceViewFooterConfig) => void;
|
||||
config: LovelaceViewFooterConfig;
|
||||
maxColumns: number;
|
||||
}
|
||||
|
||||
export const showEditViewFooterDialog = (
|
||||
|
||||
@@ -239,28 +239,15 @@ export class LovelacePanel extends LitElement {
|
||||
|
||||
const newConfig = checkLovelaceConfig(generatedConfig) as LovelaceConfig;
|
||||
|
||||
// Ask to regenerate if the config changed
|
||||
// Regenerate if the config changed
|
||||
if (!deepEqual(newConfig, oldConfig)) {
|
||||
this._askRegenerateStrategyConfig();
|
||||
this._regenerateStrategyConfig();
|
||||
}
|
||||
};
|
||||
|
||||
private _strategyConfigChanged = (ev: CustomEvent) => {
|
||||
ev.stopPropagation();
|
||||
this._askRegenerateStrategyConfig();
|
||||
};
|
||||
|
||||
private _askRegenerateStrategyConfig = () => {
|
||||
showToast(this, {
|
||||
message: this.hass!.localize("ui.panel.lovelace.changed_toast.message"),
|
||||
action: {
|
||||
action: () => this._regenerateStrategyConfig(),
|
||||
text: this.hass!.localize("ui.common.refresh"),
|
||||
},
|
||||
duration: -1,
|
||||
id: "regenerate-strategy-config",
|
||||
dismissable: false,
|
||||
});
|
||||
this._regenerateStrategyConfig();
|
||||
};
|
||||
|
||||
private async _regenerateStrategyConfig() {
|
||||
@@ -313,8 +300,14 @@ export class LovelacePanel extends LitElement {
|
||||
this._fetchConfigOnConnect = true;
|
||||
return;
|
||||
}
|
||||
if (!this.lovelace?.editMode && this._panelState !== "yaml-editor") {
|
||||
this._fetchConfig(false);
|
||||
return;
|
||||
}
|
||||
showToast(this, {
|
||||
message: this.hass!.localize("ui.panel.lovelace.changed_toast.message"),
|
||||
message: this.hass!.localize(
|
||||
"ui.panel.lovelace.externally_updated_toast.message"
|
||||
),
|
||||
action: {
|
||||
action: () => this._fetchConfig(false),
|
||||
text: this.hass!.localize("ui.common.refresh"),
|
||||
|
||||
@@ -108,7 +108,7 @@ export class HuiButtonHeadingBadge
|
||||
var(--ha-border-radius-pill)
|
||||
);
|
||||
--control-button-padding: 0;
|
||||
--mdc-icon-size: var(--ha-heading-badge-icon-size, 14px);
|
||||
--mdc-icon-size: var(--ha-heading-badge-icon-size, 16px);
|
||||
width: auto;
|
||||
height: var(--ha-heading-badge-size, 26px);
|
||||
min-width: var(--ha-heading-badge-size, 26px);
|
||||
@@ -121,6 +121,8 @@ export class HuiButtonHeadingBadge
|
||||
--control-button-icon-color: var(--color);
|
||||
--control-button-background-color: var(--color);
|
||||
--control-button-focus-color: var(--color);
|
||||
--control-button-background-opacity: 0.2;
|
||||
--control-button-font-weight: var(--ha-font-weight-bold);
|
||||
--ha-ripple-color: var(--color);
|
||||
}
|
||||
.content {
|
||||
|
||||
@@ -7,21 +7,20 @@ import { customElement, property, state } from "lit/decorators";
|
||||
import { classMap } from "lit/directives/class-map";
|
||||
import { array, assert, object, optional, string, type } from "superstruct";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-code-editor";
|
||||
import type { HaCodeEditor } from "../../components/ha-code-editor";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { LovelaceRawConfig } from "../../data/lovelace/config/types";
|
||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||
import {
|
||||
showAlertDialog,
|
||||
showConfirmationDialog,
|
||||
} from "../../dialogs/generic/show-dialog-box";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import type { Lovelace } from "./types";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import type { LovelaceRawConfig } from "../../data/lovelace/config/types";
|
||||
import { isStrategyDashboard } from "../../data/lovelace/config/types";
|
||||
|
||||
const lovelaceStruct = type({
|
||||
title: optional(string()),
|
||||
@@ -113,21 +112,7 @@ class LovelaceFullConfigEditor extends LitElement {
|
||||
oldLovelace.rawConfig !== this.lovelace.rawConfig &&
|
||||
!deepEqual(oldLovelace.rawConfig, this.lovelace.rawConfig)
|
||||
) {
|
||||
showToast(this, {
|
||||
message: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.lovelace_changed"
|
||||
),
|
||||
action: {
|
||||
action: () => {
|
||||
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
|
||||
},
|
||||
text: this.hass!.localize(
|
||||
"ui.panel.lovelace.editor.raw_editor.reload"
|
||||
),
|
||||
},
|
||||
duration: -1,
|
||||
dismissable: false,
|
||||
});
|
||||
this.yamlEditor.value = dump(this.lovelace!.rawConfig);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -76,6 +76,7 @@ import { showVoiceCommandDialog } from "../../dialogs/voice-command-dialog/show-
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant, PanelInfo } from "../../types";
|
||||
import { documentationUrl } from "../../util/documentation-url";
|
||||
import { isMac } from "../../util/is_mac";
|
||||
import { isMobileClient } from "../../util/is_mobile";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { showAreaRegistryDetailDialog } from "../config/areas/show-dialog-area-registry-detail";
|
||||
@@ -303,6 +304,12 @@ class HUIRoot extends LitElement {
|
||||
key: "ui.panel.lovelace.menu.search_home_assistant",
|
||||
buttonAction: this._showQuickBar,
|
||||
overflowAction: this._showQuickBar,
|
||||
suffix:
|
||||
this.hass.enableShortcuts && !isMobileClient
|
||||
? isMac
|
||||
? "(⌘ + K)"
|
||||
: "(Ctrl + K)"
|
||||
: undefined,
|
||||
visible: !this._editMode && !this.hass.kioskMode,
|
||||
overflow: this.narrow,
|
||||
},
|
||||
@@ -311,11 +318,11 @@ class HUIRoot extends LitElement {
|
||||
key: "ui.panel.lovelace.menu.assist_tooltip",
|
||||
buttonAction: this._showVoiceCommandDialog,
|
||||
overflowAction: this._showVoiceCommandDialog,
|
||||
suffix:
|
||||
this.hass.enableShortcuts && !isMobileClient ? "(A)" : undefined,
|
||||
visible:
|
||||
!this._editMode && this._conversation(this.hass.config.components),
|
||||
overflow: this.narrow,
|
||||
suffix:
|
||||
this.hass.enableShortcuts && !isMobileClient ? "(A)" : undefined,
|
||||
},
|
||||
{
|
||||
icon: mdiRefresh,
|
||||
@@ -406,6 +413,9 @@ class HUIRoot extends LitElement {
|
||||
`
|
||||
)}
|
||||
</ha-dropdown>
|
||||
<ha-tooltip placement="bottom" .for="button-${index}">
|
||||
${label}
|
||||
</ha-tooltip>
|
||||
`
|
||||
: html`
|
||||
<ha-icon-button
|
||||
|
||||
@@ -7,9 +7,10 @@ import { styleMap } from "lit/directives/style-map";
|
||||
import "../../../components/ha-ripple";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
import type {
|
||||
LovelaceViewConfig,
|
||||
LovelaceViewFooterConfig,
|
||||
import {
|
||||
DEFAULT_FOOTER_MAX_WIDTH_PX,
|
||||
type LovelaceViewConfig,
|
||||
type LovelaceViewFooterConfig,
|
||||
} from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import type { HuiCard } from "../cards/hui-card";
|
||||
@@ -19,7 +20,6 @@ import { showEditCardDialog } from "../editor/card-editor/show-edit-card-dialog"
|
||||
import { replaceView } from "../editor/config-util";
|
||||
import { showEditViewFooterDialog } from "../editor/view-footer/show-edit-view-footer-dialog";
|
||||
import type { Lovelace } from "../types";
|
||||
import { DEFAULT_MAX_COLUMNS } from "./hui-sections-view";
|
||||
|
||||
@customElement("hui-view-footer")
|
||||
export class HuiViewFooter extends LitElement {
|
||||
@@ -97,13 +97,8 @@ export class HuiViewFooter extends LitElement {
|
||||
}
|
||||
|
||||
private _configure() {
|
||||
const viewConfig = this.lovelace.config.views[
|
||||
this.viewIndex
|
||||
] as LovelaceViewConfig;
|
||||
|
||||
showEditViewFooterDialog(this, {
|
||||
config: this.config || {},
|
||||
maxColumns: viewConfig.max_columns || DEFAULT_MAX_COLUMNS,
|
||||
saveConfig: (newConfig: LovelaceViewFooterConfig) => {
|
||||
this._saveFooterConfig(newConfig);
|
||||
},
|
||||
@@ -180,13 +175,11 @@ export class HuiViewFooter extends LitElement {
|
||||
|
||||
if (!card && !editMode) return nothing;
|
||||
|
||||
const columnSpan = this.config?.column_span || 1;
|
||||
|
||||
return html`
|
||||
<div
|
||||
class=${classMap({ wrapper: true, "edit-mode": editMode })}
|
||||
style=${styleMap({
|
||||
"--footer-column-span": String(columnSpan),
|
||||
"--footer-max-width": `${this.config?.max_width || DEFAULT_FOOTER_MAX_WIDTH_PX}px`,
|
||||
})}
|
||||
>
|
||||
${editMode
|
||||
@@ -228,23 +221,18 @@ export class HuiViewFooter extends LitElement {
|
||||
|
||||
:host([sticky]) {
|
||||
position: sticky;
|
||||
bottom: 0;
|
||||
bottom: var(--row-gap);
|
||||
z-index: 4;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
padding: var(--ha-space-4) 0;
|
||||
padding-bottom: max(
|
||||
var(--ha-space-4),
|
||||
var(--safe-area-inset-bottom, 0px)
|
||||
padding: var(--ha-space-2) 0;
|
||||
padding-bottom: calc(
|
||||
max(var(--ha-space-2), var(--safe-area-inset-bottom, 0px))
|
||||
);
|
||||
box-sizing: content-box;
|
||||
margin: 0 auto;
|
||||
max-width: calc(
|
||||
var(--footer-column-span, 1) / var(--column-count, 1) * 100% +
|
||||
(var(--footer-column-span, 1) - var(--column-count, 1)) /
|
||||
var(--column-count, 1) * var(--column-gap, 32px)
|
||||
);
|
||||
max-width: var(--footer-max-width, 600px);
|
||||
}
|
||||
|
||||
.wrapper:not(.edit-mode) {
|
||||
@@ -315,7 +303,7 @@ export class HuiViewFooter extends LitElement {
|
||||
border-bottom-left-radius: 0px;
|
||||
border-bottom-right-radius: 0px;
|
||||
background: var(--secondary-background-color);
|
||||
--mdc-icon-button-size: 36px;
|
||||
--ha-icon-button-size: 36px;
|
||||
--mdc-icon-size: 20px;
|
||||
color: var(--primary-text-color);
|
||||
}
|
||||
|
||||
@@ -679,7 +679,7 @@ export class BarMediaPlayer extends SubscribeMixin(LitElement) {
|
||||
}
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
:host {
|
||||
transition: none;
|
||||
transition: 1ms;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+3
-17
@@ -186,24 +186,10 @@ export const haStyleDialog = css`
|
||||
var(--safe-area-inset-right, 0) var(--safe-area-inset-bottom, 0)
|
||||
var(--safe-area-inset-left, 0);
|
||||
--vertical-align-dialog: flex-end;
|
||||
}
|
||||
ha-dialog {
|
||||
--ha-dialog-border-radius: var(--ha-border-radius-square);
|
||||
ha-dialog,
|
||||
ha-adaptive-dialog {
|
||||
--mdc-dialog-min-width: 100vw;
|
||||
--mdc-dialog-max-width: 100vw;
|
||||
--mdc-dialog-min-height: 100vh;
|
||||
--mdc-dialog-min-height: 100svh;
|
||||
--mdc-dialog-max-height: 100vh;
|
||||
--mdc-dialog-max-height: 100svh;
|
||||
--dialog-container-padding: 0px;
|
||||
--dialog-surface-padding: var(--safe-area-inset-top, 0)
|
||||
var(--safe-area-inset-right, 0) var(--safe-area-inset-bottom, 0)
|
||||
var(--safe-area-inset-left, 0);
|
||||
--vertical-align-dialog: flex-end;
|
||||
}
|
||||
ha-dialog {
|
||||
--ha-dialog-border-radius: var(--ha-border-radius-square);
|
||||
}
|
||||
}
|
||||
}
|
||||
.error {
|
||||
color: var(--error-color);
|
||||
|
||||
@@ -62,9 +62,9 @@ export const coreStyles = css`
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
html {
|
||||
--ha-animation-duration-fast: 0ms;
|
||||
--ha-animation-duration-normal: 0ms;
|
||||
--ha-animation-duration-slow: 0ms;
|
||||
--ha-animation-duration-fast: 1ms;
|
||||
--ha-animation-duration-normal: 1ms;
|
||||
--ha-animation-duration-slow: 1ms;
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
+38
-22
@@ -548,7 +548,7 @@
|
||||
},
|
||||
"template": {
|
||||
"yaml_warning": "It appears you may be writing YAML into this template field (saw ''{string}''), which is likely incorrect. This field is intended for templates only (e.g. '{{ states(sensor.test) > 0 }}' ).",
|
||||
"learn_more": "Learn more about templating."
|
||||
"learn_more": "Learn more about templating"
|
||||
},
|
||||
"text": {
|
||||
"show_password": "Show password",
|
||||
@@ -679,9 +679,9 @@
|
||||
"no_entities": "You don't have any entities",
|
||||
"no_match": "No entities found for {term}",
|
||||
"show_entities": "Show entities",
|
||||
"new_entity": "Create a new entity",
|
||||
"new_entity": "Creates a new entity",
|
||||
"placeholder": "Select an entity",
|
||||
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n } helper.",
|
||||
"create_helper": "Create a new {domain, select, \n undefined {} \n other {{domain} }\n }helper",
|
||||
"unknown": "Unknown entity selected"
|
||||
},
|
||||
"entity-name-picker": {
|
||||
@@ -3812,7 +3812,7 @@
|
||||
"grid": {
|
||||
"title": "Electricity grid",
|
||||
"sub": "Configure the amount of energy that you consume from the grid and, if you produce energy, give back to the grid. This allows Home Assistant to track your whole home energy usage.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"learn_more": "More information on how to get started",
|
||||
"grid_connections": "Grid connections",
|
||||
"add_connection": "Add grid connection",
|
||||
"edit_connection": "Edit grid connection",
|
||||
@@ -3873,7 +3873,7 @@
|
||||
"solar": {
|
||||
"title": "Solar panels",
|
||||
"sub": "Let Home Assistant monitor your solar panels and give you insight on their performance.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"learn_more": "More information on how to get started",
|
||||
"solar_production": "Solar production",
|
||||
"edit_solar_production": "Edit solar production",
|
||||
"delete_solar_production": "Remove solar production",
|
||||
@@ -3896,7 +3896,7 @@
|
||||
"battery": {
|
||||
"title": "Home battery storage",
|
||||
"sub": "If you have a battery system, you can configure it to monitor how much energy was stored and used from your battery.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"learn_more": "More information on how to get started",
|
||||
"battery_systems": "Battery systems",
|
||||
"edit_battery_system": "Edit battery system",
|
||||
"delete_battery_system": "Remove battery system",
|
||||
@@ -3923,7 +3923,7 @@
|
||||
"gas": {
|
||||
"title": "Gas consumption",
|
||||
"sub": "Let Home Assistant monitor your gas usage.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"learn_more": "More information on how to get started",
|
||||
"gas_consumption": "Gas consumption",
|
||||
"edit_gas_source": "Edit gas source",
|
||||
"delete_gas_source": "Remove gas source",
|
||||
@@ -3952,7 +3952,7 @@
|
||||
"water": {
|
||||
"title": "Water consumption",
|
||||
"sub": "Let Home Assistant monitor your water usage.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"learn_more": "More information on how to get started",
|
||||
"water_consumption": "Water consumption",
|
||||
"edit_water_source": "Edit water source",
|
||||
"delete_water_source": "Remove water source",
|
||||
@@ -3978,7 +3978,7 @@
|
||||
"device_consumption": {
|
||||
"title": "Individual electrical devices",
|
||||
"sub": "Tracking the energy usage of individual devices allows Home Assistant to break down your energy usage by device.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"learn_more": "More information on how to get started",
|
||||
"add_stat": "Pick entity to track energy of",
|
||||
"selected_stat": "Tracking energy for",
|
||||
"devices": "Devices",
|
||||
@@ -3997,7 +3997,7 @@
|
||||
"device_consumption_water": {
|
||||
"title": "Individual water devices",
|
||||
"sub": "Tracking the water usage of individual devices allows Home Assistant to break down your water usage by device.",
|
||||
"learn_more": "More information on how to get started.",
|
||||
"learn_more": "More information on how to get started",
|
||||
"devices": "Devices",
|
||||
"add_device": "Add device",
|
||||
"dialog": {
|
||||
@@ -4195,6 +4195,21 @@
|
||||
"info": "INFO",
|
||||
"debug": "DEBUG"
|
||||
},
|
||||
"error_type": {
|
||||
"auth": "Authentication",
|
||||
"connection": "Connection",
|
||||
"invalid_response": "Invalid response",
|
||||
"rate_limit": "Rate limit",
|
||||
"server": "Server",
|
||||
"slow_setup": "Slow setup",
|
||||
"timeout": "Timeout",
|
||||
"ssl": "TLS/SSL",
|
||||
"statistics": "Statistics",
|
||||
"dns": "DNS"
|
||||
},
|
||||
"level_filter": "Level",
|
||||
"classification": "Classification",
|
||||
"other": "Other",
|
||||
"custom_integration": "custom integration",
|
||||
"error_from_custom_integration": "This error originated from a custom integration.",
|
||||
"show_full_logs": "Show raw logs",
|
||||
@@ -4219,6 +4234,7 @@
|
||||
"integration": "[%key:ui::panel::config::integrations::integration%]",
|
||||
"documentation": "documentation",
|
||||
"issues": "issues",
|
||||
"error_type": "Error type",
|
||||
"first_occurred": "First occurred",
|
||||
"number_of_occurrences": "{count} {count, plural,\n one {occurrence}\n other {occurrences}\n}",
|
||||
"last_logged": "Last logged"
|
||||
@@ -5749,7 +5765,7 @@
|
||||
"key_not_null": "The field key must not be empty.",
|
||||
"key_not_unique": "The field key must not be the same value as another field.",
|
||||
"fields": "Fields",
|
||||
"link_help_fields": "Learn more about fields.",
|
||||
"link_help_fields": "Learn more about fields",
|
||||
"add_fields": "Add fields",
|
||||
"add_field": "Add field",
|
||||
"field": "field",
|
||||
@@ -5777,7 +5793,7 @@
|
||||
"delete_confirm_text": "{name} will be permanently deleted.",
|
||||
"sequence": "Sequence",
|
||||
"sequence_sentence": "The sequence of actions of this script.",
|
||||
"link_available_actions": "Learn more about available actions.",
|
||||
"link_available_actions": "Learn more about available actions",
|
||||
"leave": {
|
||||
"unsaved_new_title": "Save new script?",
|
||||
"unsaved_new_text": "You can save your changes, or delete this script. You can't undo this action.",
|
||||
@@ -6050,7 +6066,7 @@
|
||||
"no_hooks_yet_link_integration": "webhook-based integration",
|
||||
"no_hooks_yet2": " or by creating a ",
|
||||
"no_hooks_yet_link_automation": "webhook automation",
|
||||
"link_learn_more": "Learn more about creating webhook-powered automations.",
|
||||
"link_learn_more": "Learn more about creating webhook-powered automations",
|
||||
"loading": "Loading…",
|
||||
"manage": "Manage",
|
||||
"disable_hook_error_msg": "Failed to disable webhook:"
|
||||
@@ -7042,7 +7058,7 @@
|
||||
"remove_node": "Remove foreign device",
|
||||
"remove_a_node": "Remove a device",
|
||||
"rebuild_network_routes": "Discover and assign new routes",
|
||||
"in_progress_inclusion_exclusion": "Z-Wave JS is searching for devices",
|
||||
"in_progress_inclusion_exclusion": "Z-Wave is searching for devices",
|
||||
"cancel_inclusion_exclusion": "Stop searching"
|
||||
},
|
||||
"dashboard": {
|
||||
@@ -7517,7 +7533,7 @@
|
||||
"caption": "Logs",
|
||||
"title": "Z-Wave logs",
|
||||
"log_level": "Log level",
|
||||
"subscribed_to_logs": "Subscribed to Z-Wave JS log messages…",
|
||||
"subscribed_to_logs": "Subscribed to Z-Wave log messages…",
|
||||
"log_level_changed": "Log level changed to: {level}",
|
||||
"download_logs": "Download logs"
|
||||
},
|
||||
@@ -7629,7 +7645,7 @@
|
||||
},
|
||||
"picker": {
|
||||
"title": "Select Z-Wave network",
|
||||
"no_entries": "No Z-Wave networks configured. Set up the Z-Wave JS integration first."
|
||||
"no_entries": "No Z-Wave networks configured. Set up the Z-Wave integration first."
|
||||
}
|
||||
},
|
||||
"matter": {
|
||||
@@ -8089,7 +8105,9 @@
|
||||
},
|
||||
"light": {
|
||||
"lights": "Lights",
|
||||
"other_lights": "Other lights"
|
||||
"other_lights": "Other lights",
|
||||
"on": "On",
|
||||
"off": "Off"
|
||||
},
|
||||
"security": {
|
||||
"devices": "Devices",
|
||||
@@ -8353,7 +8371,6 @@
|
||||
"unsaved_changes": "Unsaved changes",
|
||||
"saved": "Saved",
|
||||
"reload": "Reload",
|
||||
"lovelace_changed": "Your dashboard was updated, do you want to load the updated config in the editor and lose your current changes?",
|
||||
"confirm_reset_config_title": "Reset dashboard configuration?",
|
||||
"confirm_reset_config_text": "Your dashboard will be reset to an empty state. You can start fresh and build your dashboard from scratch.",
|
||||
"confirm_unsaved_changes": "You have unsaved changes, are you sure you want to exit?",
|
||||
@@ -8495,8 +8512,7 @@
|
||||
"edit_yaml": "[%key:ui::panel::lovelace::editor::edit_view::edit_yaml%]",
|
||||
"saving_failed": "[%key:ui::panel::lovelace::editor::edit_view::saving_failed%]",
|
||||
"settings": {
|
||||
"column_span": "Width",
|
||||
"column_span_helper": "[%key:ui::panel::lovelace::editor::edit_section::settings::column_span_helper%]"
|
||||
"max_width": "Max width"
|
||||
}
|
||||
},
|
||||
"edit_badges": {
|
||||
@@ -9676,8 +9692,8 @@
|
||||
"entity_unavailable": "Entity is currently unavailable: {entity}",
|
||||
"starting": "Home Assistant is starting. Not everything may be available yet."
|
||||
},
|
||||
"changed_toast": {
|
||||
"message": "Your dashboard was updated. Refresh to see changes?"
|
||||
"externally_updated_toast": {
|
||||
"message": "Dashboard updated in another session. Refreshing will discard your unsaved changes."
|
||||
},
|
||||
"components": {
|
||||
"timestamp-display": {
|
||||
|
||||
Reference in New Issue
Block a user