Compare commits

..
Author SHA1 Message Date
Aidan Timson ba22a12a20 Fix 2026-03-03 14:46:42 +00:00
Aidan Timson 098b54f749 Fix 2026-03-03 14:46:15 +00:00
Aidan Timson 4c6a7091a6 Filtering 2026-03-03 14:46:15 +00:00
Aidan Timson 322cb35526 More types 2026-03-03 14:46:15 +00:00
Aidan Timson c34f6bea2b Always show 2026-03-03 14:46:15 +00:00
Aidan Timson 41bf0652b0 Setup log classification 2026-03-03 14:46:15 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
23af40743b Update dependency lint-staged to v16.3.0 (#29954)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-03 14:36:48 +00:00
Paul BotteinandGitHub c4326b4f3a Use max width for dashboard footer (#29947) 2026-03-03 14:57:57 +01:00
Paul BotteinandGitHub d248f5614f Add label for toggle button in area strategy (#29949) 2026-03-03 13:34:05 +01:00
a4da7b26ea Fix copy to clipboard for wa dialogs (#29951)
Co-authored-by: Copilot <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-03-03 13:09:30 +01:00
WendelinandGitHub 3c49cdf3c0 ha-bottom-sheet reduce motion support (#29950) 2026-03-03 11:06:23 +00:00
Petar PetrovandGitHub 26af81d1a4 Use net battery power in power sankey card (#29940) 2026-03-03 11:53:54 +01:00
Aidan TimsonandGitHub 2a08f2d79b Add tooltip for config dashboard action button in toolbar (#29948) 2026-03-03 10:33:37 +01:00
Marcin BauerandGitHub a5be02b743 Add tooltip for Lovelace dropdown action button in top app bar (#29933) 2026-03-03 09:15:37 +00:00
sevorlandGitHub 4228871f00 Fix missing slot attribute on wa-divider in automation sidebar action (#29942) 2026-03-03 08:56:33 +00:00
WendelinandGitHub 9a7a8fd377 Add reportValidity in ha-form (#29884)
* Add validation for required fields in ha-auth-flow before submission

* Add reportValidity methods to form components for improved validation handling

* Remove async reportValidity funcs

* Review
2026-03-03 09:05:49 +02:00
WendelinandGitHub 8b82882e15 ha-authorize fix rtl check (#29937)
Add RTL direction handling in updated lifecycle method
2026-03-02 18:22:06 +01:00
2701015eda Fix data-table content bottom margin (#29805)
Co-authored-by: Copilot <[email protected]>
2026-03-02 17:02:31 +01:00
1991a9e493 Code editor fullscreen in dialogs (#29882)
Co-authored-by: Bram Kragten <[email protected]>
2026-03-02 17:01:13 +01:00
Petar PetrovandGitHub 2b72c54194 Migrate Energy date selector to new footer (#29867) 2026-03-02 17:00:34 +01:00
Paul BotteinandGitHub a7cb2fe7a7 Fix updates, discovered devices and repairs cards flickering (#29935) 2026-03-02 13:50:42 +00:00
Paul BotteinandGitHub 51ea0c8201 Fix sidebar not closing when reduced motion is enabled (#29934) 2026-03-02 13:19:26 +00:00
WendelinandGitHub ead7081bc6 Dialog: Add show event target check (#29927)
Add event phase check in _handleShow and _handleAfterShow methods
2026-03-02 11:41:52 +00:00
WendelinandGitHub ee982b1899 Add error translation for loading energy preferences (#29924) 2026-03-02 11:49:46 +02:00
Aidan TimsonandGitHub e8b100a39e Remove cache to fix re-add repo issue (#29926)
Remove cache to fix readd repo issue
2026-03-02 11:49:19 +02:00
50c361db62 Add mixin to remove code duplication in automation/script editors (#29842)
* Initial plan

* Changes before error encountered

Co-authored-by: wendevlin <[email protected]>

* Fix mixin: use function-body syntax for decorators, curried generics for type safety

Co-authored-by: wendevlin <[email protected]>

* Simplify automation/script editor mixin signature

* Add shared styles and loading animation to automation/script editor mixin

Co-authored-by: wendevlin <[email protected]>

* Remove underscore prefix from protected members per style guide

Co-authored-by: wendevlin <[email protected]>

---------

Co-authored-by: copilot-swe-agent[bot] <[email protected]>
Co-authored-by: wendevlin <[email protected]>
Co-authored-by: Wendelin <[email protected]>
2026-03-02 11:44:04 +02:00
sevorlandGitHub e7a8d15a13 Use ha-duration-input for wait_template timeout (#29862) 2026-03-02 09:07:51 +01:00
karwostsandGitHub fbd0409837 Init ha-form expansion elements to undefined instead of null (#29900)
* Init ha-form expansion elements to undefined instead of null

* revert change to error/warning
2026-03-02 09:29:06 +02:00
karwostsandGitHub a0d100611f Fix distribution card stub error (#29915)
* Fix distribution card stub error

* unit check not required
2026-03-02 09:06:10 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
a969bf1065 Bump actions/upload-artifact from 6.0.0 to 7.0.0 (#29922)
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 6.0.0 to 7.0.0.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/b7c566a772e6b6bfb58ed0dc250532a479d7789f...bbbca2ddaa5d8feaa63e36b76fdaad77386f024f)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-version: 7.0.0
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-03-02 06:10:19 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a153330610 Update dependency gulp-zopfli-green to v7 (#29919)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-03-02 07:06:41 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
bd2f1ca3a8 Update dependency @html-eslint/eslint-plugin to v0.57.1 (#29905)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 20:08:37 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3263034416 Update dependency @codemirror/language to v6.12.2 (#29904)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-28 20:07:43 +01:00
Paul BotteinandGitHub 82b28b547a Fix control select menu color in ios (#29892) 2026-02-27 17:26:04 +01:00
Bram KragtenandGitHub 61c2c750b4 Fix overflow for icon buttons (#29891) 2026-02-27 15:44:21 +00:00
Petar PetrovandGitHub 117690ee70 Fix sensor card graph not updating when value is unchanged (#29889) 2026-02-27 15:41:54 +00:00
Petar PetrovandGitHub e753de85eb Make hui-sections-view always fill the screen so footer is at the bottom (#29890) 2026-02-27 15:39:21 +00:00
Paul BotteinandGitHub a240019968 Add render icon property to ha-control-select-menu (#29881) 2026-02-27 16:23:58 +01:00
Petar PetrovandGitHub 0bdf4b8777 Fix monetary device class state display with non-ISO 4217 currency symbols (#29887) 2026-02-27 14:59:14 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
6337828ed8 Update dependency barcode-detector to v3.1.0 (#29886)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-27 15:46:29 +02:00
b8e5af652b Add audits and yaml mode to more info details (#29854)
* Add audits and yaml mode to more info details

* Reset yaml mode on back

* Use mapped array for state entries

* Typo

Co-authored-by: Bram Kragten <[email protected]>

* Memoize

* Rename

* Fix

* Format audits in normal mode

* Refactor, dont pass hass

---------

Co-authored-by: Bram Kragten <[email protected]>
2026-02-27 14:45:55 +01:00
Petar PetrovandGitHub e4ae29e8b5 Fix energy compare tooltip showing wrong year (#29885) 2026-02-27 14:37:52 +01:00
Aidan TimsonandGitHub 08231dbbb0 Use large width on system log dialogs (#29879) 2026-02-27 12:46:10 +01:00
Paul Bottein 0ca656933d Revert "Add render icon property to ha-control-select-menu"
This reverts commit b23cf8eba4.
2026-02-27 12:21:23 +01:00
Paul Bottein b23cf8eba4 Add render icon property to ha-control-select-menu 2026-02-27 12:20:52 +01:00
Robert ReschandGitHub 61b546415d Revert "Add vacuum mapping not configured issue" (#29876) 2026-02-27 11:18:49 +01:00
Brandon ChenandGitHub 4e1b709303 Fix YAML content invisible in dark mode for conversation debug result… (#29874) 2026-02-27 09:11:28 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
34e65b302d Update dependency typescript-eslint to v8.56.1 (#29868)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 18:08:04 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
336d0e1b9d Update dependency @html-eslint/eslint-plugin to v0.57.0 (#29863)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 17:25:10 +01:00
Paul BotteinandGitHub 58d4cf8d84 Fix scrollbar in 2026.3 (#29865) 2026-02-26 16:44:12 +01:00
Aidan TimsonandGitHub d3453aff37 Add missing theming variable support to dialog and bottom sheet (#29857) 2026-02-26 16:43:20 +01:00
Aidan TimsonandGitHub 64ff2e414c Add thread configuration my link (#29861) 2026-02-26 15:06:46 +00:00
WendelinandGitHub 2ca25c980f Fix quick search icon size (#29858) 2026-02-26 15:59:27 +01:00
Aidan TimsonandGitHub 73d93bc601 Add matter configuration my link (#29859) 2026-02-26 14:41:43 +00:00
WendelinandGitHub 5ca6a8aced Fix ha-icon-button-toggle selected style (#29856) 2026-02-26 13:02:12 +00:00
Aidan TimsonandGitHub 7ff4993e0b Fix esc closing dialogs with prevent scrim close (#29851) 2026-02-26 13:20:05 +02:00
Norbert RittelandGitHub 4e6fbacccc Remove trailing periods from "Learn more" etc. links / tooltips (#29835) 2026-02-26 10:38:54 +00:00
Petar PetrovandGitHub 2958d49e36 Convert Energy Now tiles to badges (#29845) 2026-02-26 10:38:01 +00:00
Norbert RittelandGitHub 92289dc7ea Improve "Create a new … helper" option in entity picker (#29853) 2026-02-26 10:34:42 +00:00
Petar PetrovandGitHub f6c1a890e4 Dynamically calculate the date range picker's vertical opening direction (#29850) 2026-02-26 09:33:34 +00:00
WendelinandGitHub d06321ed43 Fix protocols dashboards fab padding (#29847) 2026-02-26 10:31:50 +02:00
dependabot[bot]GitHubdependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
3c3d8d9974 Bump rollup from 2.79.2 to 2.80.0 (#29841)
Bumps [rollup](https://github.com/rollup/rollup) from 2.79.2 to 2.80.0.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/v2.80.0/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v2.79.2...v2.80.0)

---
updated-dependencies:
- dependency-name: rollup
  dependency-version: 2.80.0
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <[email protected]>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2026-02-26 08:18:15 +02:00
Paul BotteinandGitHub 4f39fa482d Only ask to refresh dashboard in edit mode or yaml mode (#29826) 2026-02-26 08:16:21 +02:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5d0fe3236c Update dependency @swc/helpers to v0.5.19 (#29836)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-26 07:07:37 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
b86142ae50 Update Node.js to v24.14.0 (#29831)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 19:25:42 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
5d2f3ee5e8 Update dependency tar to v7.5.9 (#29832)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 19:24:58 +00:00
AlCalzoneandGitHub e3f7c631a7 Rename "Z-Wave JS" to "Z-Wave" when not referring to the project/org (#29830) 2026-02-25 19:15:16 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
49f9d95853 Update dependency vite-tsconfig-paths to v6.1.1 (#29829)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:53:12 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
db3d7701b5 Update dependency typescript-eslint to v8.56.0 (#29828)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 17:35:36 +00:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
3e55acf531 Update dependency @home-assistant/webawesome to v3.2.1-ha.3 (#29810)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:26:47 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
f102618d9d Update dependency eslint-plugin-wc to v3.1.0 (#29824)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:25:06 +01:00
renovate[bot]GitHubrenovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
a3c02b511d Update dependency jsdom to v28.1.0 (#29825)
Co-authored-by: renovate[bot] <29139614+renovate[bot]@users.noreply.github.com>
2026-02-25 18:24:38 +01:00
Bram KragtenandGitHub 74111d248e Fix css minifying (#29827) 2026-02-25 17:53:50 +01:00
69 changed files with 1944 additions and 1460 deletions
+2 -2
View File
@@ -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/
+2 -2
View File
@@ -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
+1 -1
View File
@@ -1 +1 @@
24.13.1
24.14.0
+14 -14
View File
@@ -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
View File
@@ -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"
+10 -2
View File
@@ -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 };
+4
View File
@@ -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>
+27 -1
View File
@@ -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");
+5 -5
View File
@@ -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;
+12 -1
View File
@@ -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
+12
View File
@@ -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,
+2 -1
View File
@@ -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;
+8 -1
View File
@@ -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
View File
@@ -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;
}
}
+16 -5
View File
@@ -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">
+9 -2
View File
@@ -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;
+11 -8
View File
@@ -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 {
+21 -7
View File
@@ -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 {
+25 -10
View File
@@ -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 {
+10 -3
View File
@@ -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
+8 -5
View File
@@ -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 {
+55 -18
View File
@@ -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">
+1 -1
View File
@@ -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;
+9 -11
View File
@@ -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)) {
+11 -4
View File
@@ -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
+20 -2
View File
@@ -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() {
+2 -2
View File
@@ -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;
}
}
`,
+9 -3
View File
@@ -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);
+1 -1
View File
@@ -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 {
+3 -1
View File
@@ -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 {
+13
View File
@@ -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;
+10 -10
View File
@@ -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 />
+267 -46
View File
@@ -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);
+286 -182
View File
@@ -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);
}
}
`;
}
+146 -292
View File
@@ -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 = (
+10 -17
View File
@@ -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 {
+5 -20
View File
@@ -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);
}
}
+12 -2
View File
@@ -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
+11 -23
View File
@@ -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
View File
@@ -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);
+3 -3
View File
@@ -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
View File
@@ -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": {
+286 -286
View File
File diff suppressed because it is too large Load Diff