mirror of
https://github.com/home-assistant/frontend.git
synced 2026-11-13 16:33:02 +00:00
Compare commits
23
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e96fac4dc | ||
|
|
1fa476ab67 | ||
|
|
0a4a354e22 | ||
|
|
95a009641f | ||
|
|
36f59f8495 | ||
|
|
8a13bba521 | ||
|
|
cc7b1952c0 | ||
|
|
2edf6f91d0 | ||
|
|
9d52517ae9 | ||
|
|
27a7f6335f | ||
|
|
cd44bfcebc | ||
|
|
8819844740 | ||
|
|
008adf1533 | ||
|
|
8c3d0f2f1e | ||
|
|
3f4f89480f | ||
|
|
432762a2ea | ||
|
|
013cf8d7e6 | ||
|
|
1de489cca9 | ||
|
|
97af661779 | ||
|
|
f702e369fc | ||
|
|
78e0a92ddd | ||
|
|
d742259444 | ||
|
|
623ca14356 |
No files matched your search
@@ -28,10 +28,18 @@ AI can be a helpful tool for writing code and documentation. However, due to the
|
||||
|
||||
All contributions must be reviewed and understood by the contributor before submission. You should be able to explain every change in a pull request you submit. Pull requests that appear to be unreviewed AI output will be closed without review.
|
||||
|
||||
## Design contributions
|
||||
|
||||
AI can be a helpful tool to explore ideas and build mockups and prototypes. But AI tends to give the most common answer, not the one that fits Home Assistant and its users. So we require a human who brings the point of view and owns the design decisions.
|
||||
|
||||
You should be able to explain the problem your design solves and why you made your choices. If a mockup or prototype was mostly generated with AI, say so when you share it. Check your work against the [design guidelines](https://contributors.home-assistant.io/design/), as AI tools often make up components, colors, or spacing that don't exist in Home Assistant. Design proposals that look like unreviewed AI output may be closed or hidden.
|
||||
|
||||
## Our use of AI
|
||||
|
||||
Some of our projects use AI tools to assist with code reviews, issue triaging, reporting, and other project management tasks. These tools may leave comments on pull requests or issues. As with any automated tooling, these comments are not always correct.
|
||||
|
||||
We also use AI to help keep the design system, the Figma kit, and the code in sync. The design decisions behind them are made by people.
|
||||
|
||||
If an AI tool leaves a comment on your contribution, treat it as you would any other review comment. If you believe it is incorrect, say so; a brief explanation is sufficient. Maintainers always have the final say. If in doubt, ask a maintainer.
|
||||
|
||||
## Enforcement
|
||||
|
||||
+5
-5
@@ -77,11 +77,11 @@
|
||||
"@mdi/svg": "7.4.47",
|
||||
"@replit/codemirror-indentation-markers": "6.5.3",
|
||||
"@swc/helpers": "0.5.24",
|
||||
"@tsparticles/engine": "4.5.2",
|
||||
"@tsparticles/preset-links": "4.5.2",
|
||||
"@tsparticles/engine": "4.5.3",
|
||||
"@tsparticles/preset-links": "4.5.3",
|
||||
"@versatiles/style": "8.1.1",
|
||||
"@vibrant/color": "4.0.4",
|
||||
"@vvo/tzdb": "6.202.0",
|
||||
"@vvo/tzdb": "6.203.0",
|
||||
"@webcomponents/scoped-custom-element-registry": "0.0.10",
|
||||
"barcode-detector": "3.2.3",
|
||||
"cally": "0.9.2",
|
||||
@@ -180,7 +180,7 @@
|
||||
"eslint": "10.14.0",
|
||||
"eslint-config-prettier": "10.1.8",
|
||||
"eslint-import-resolver-webpack": "0.13.11",
|
||||
"eslint-plugin-import-x": "4.17.1",
|
||||
"eslint-plugin-import-x": "4.18.0",
|
||||
"eslint-plugin-lit": "2.3.1",
|
||||
"eslint-plugin-lit-a11y": "5.1.3",
|
||||
"eslint-plugin-unused-imports": "4.4.1",
|
||||
@@ -208,7 +208,7 @@
|
||||
"pinst": "3.0.0",
|
||||
"prettier": "3.9.10",
|
||||
"rspack-manifest-plugin": "5.2.2",
|
||||
"serve": "14.2.6",
|
||||
"serve": "14.2.7",
|
||||
"sinon": "22.1.0",
|
||||
"tar": "7.5.22",
|
||||
"terser-webpack-plugin": "5.6.1",
|
||||
|
||||
Generated
+138
-192
@@ -291,11 +291,11 @@ importers:
|
||||
specifier: 0.5.24
|
||||
version: 0.5.24
|
||||
'@tsparticles/engine':
|
||||
specifier: 4.5.2
|
||||
version: 4.5.2
|
||||
specifier: 4.5.3
|
||||
version: 4.5.3
|
||||
'@tsparticles/preset-links':
|
||||
specifier: 4.5.2
|
||||
version: 4.5.2
|
||||
specifier: 4.5.3
|
||||
version: 4.5.3
|
||||
'@versatiles/style':
|
||||
specifier: 8.1.1
|
||||
version: 8.1.1([email protected])
|
||||
@@ -303,8 +303,8 @@ importers:
|
||||
specifier: 4.0.4
|
||||
version: 4.0.4
|
||||
'@vvo/tzdb':
|
||||
specifier: 6.202.0
|
||||
version: 6.202.0
|
||||
specifier: 6.203.0
|
||||
version: 6.203.0
|
||||
'@webcomponents/scoped-custom-element-registry':
|
||||
specifier: 0.0.10
|
||||
version: 0.0.10
|
||||
@@ -595,8 +595,8 @@ importers:
|
||||
specifier: 0.13.11
|
||||
version: 0.13.11([email protected])
|
||||
eslint-plugin-import-x:
|
||||
specifier: 4.17.1
|
||||
version: 4.17.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])
|
||||
specifier: 4.18.0
|
||||
version: 4.18.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected])
|
||||
eslint-plugin-lit:
|
||||
specifier: 2.3.1
|
||||
version: 2.3.1([email protected]([email protected]))
|
||||
@@ -679,8 +679,8 @@ importers:
|
||||
specifier: 5.2.2
|
||||
version: 5.2.2(@rspack/[email protected](@swc/[email protected]))
|
||||
serve:
|
||||
specifier: 14.2.6
|
||||
version: 14.2.6([email protected])
|
||||
specifier: 14.2.7
|
||||
version: 14.2.7([email protected])
|
||||
sinon:
|
||||
specifier: 22.1.0
|
||||
version: 22.1.0
|
||||
@@ -3270,85 +3270,85 @@ packages:
|
||||
resolution: {integrity: sha512-/67zpWDBLV+oYAEL682s1ktXL0HgqX76f6gaVGkGnVZlBbm1zd0v4Bz8MFF2GGhoX9rvfq3KSQHubFHwa6w6/Q==}
|
||||
engines: {node: '>=12'}
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-bwvu84lMX5Sp79pyKu5yW2SFAqY52B7OlaxDjeeP3TiO7hJLFWxd8VPmSfOarcYK/z/uxp/dZfbcvp/5ylXyiQ==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-Fz2bTDc6Folav8isOk2scXM+LCSPhQ9hD8xCSmsEGuGH7JkE+XTsS4j6E9H6b+RFlZ1yb2TxdVBJnfXy8XM9Bw==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-lA+GP2EN5uN3TnOMEFqYBS0yLuQyZ2UlHy72TedgREGeEsrUoXB/FtZKseVt0g9FId4ZI6jXizxEavs65PbUsA==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-f0gHWnqChK4ksE9Bq3YN0iuJVMi2B81YSkCL5BrohuykoaKCLSVVSqjhefmELTMrdiFVsdt6WPJ+HjVQgjt7CA==}
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-NaxB1X/90lSu+uI0ayaaNC4okLIwJ93nBWvrdYHQ5/xsyan01qU+rnDjdg8Y9fPewncXlHkOaQezHeG4RjDubg==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-kbZoUA2uqR2043FHfH2+KFz7JhYTZjyTeDqVL/Z3z38mfdHEVrOJ4q+mwYjr+hX4tq2YoLf1D4DA6w/er1AlKA==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-JmaqBMZW/OzXGeQBz1g67EwTKeOYccIFrQMnBmiZgavO7LLhglABpyTWJAT9Cb7M/kgx3GIecZGkPxFzQ44pxQ==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-ak8AAPQKinJlwztRAo+OA5ItwjLtymmjayefQCF6K1FIWs5SaWiHWbzMB5vdOvuGUE6OOchqj8NLcu3HfHmdjw==}
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-kNwh59Oyi8lvFQYK9z4YgAhS0GcOfFLkIhafTo0qlklqSUl3CvZq0aS14t368oz4Qk/l29pvnLJQg8pmRxIMaw==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-NAm9yL8o3oaeYgB7WLzuoD0yfLw9kvEwu0N+UHTwoe+kYRLJvmoUyMlED7hPQHW74VTlSmuZ6gqWTnaOv1mFLA==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/plugin-interactivity': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
'@tsparticles/plugin-interactivity': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-7mO9tpJvXkbRIn4OjjUNoiPKAEJkEeazLl8eQB9W9lYpxuNCyZZ+sXJpS2LazyBNKTvGy1P0ekoX/rHqc84vKA==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-fJJp0xVEeGQDpkjGzvftMbCrLWtwqo+ees49gCnr9SZYVMRbxi3vQYukvi4ULOzYgEW5bTseVprDfiqOwfQJcw==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-gzcPgOzb3vP3p1AF/op5045buNYjsUOHoeTx9WHABrbDDVrQYUo1KsThg6Uo6Uahd8yMIuWUDlzjgF722KBn0w==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-k+AkTG8r2UTE3n2u4kmb9dODRVtBc3Yttk7/UQ5HPlcBMr8AnKbx9gyxzzCx2MaEFZG2My0cghJPvQvibrAW7Q==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-/gIqWofLBYLcYcEGt1jxZPD+kgCJOoajS22NxxbVi80MrUxvT586qqbhYiiAesBxRUgCZOJnK39BF2S4A0Ik7Q==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-jfyHjPvrc8ORwaiIJ3xgzIveRhlImn21TiblpakeF2Ft5HgxJ+Lb8eIN/2VUJFjdqLpQ3H1nGSJspHYzeU5RtQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-uzZGjqxFhz/LUAKnbBYApgelXBrXSJt25fad8TxL5RoDVGgd22pxPErwEtCTAbhJyL6Z5Clj9p5vk9Vve+sodg==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-4w1m8WFL5UJElwTO978P9fOub2vtLCYRLD0YpmXY+AumZw02wLRH6Al7xY2Sgchbgm/uLHkCDzeUTYUi9GzURg==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-Oh2FPN26M+JlSFvLr7rOJEyTO7n0AUu5x3gHCx4mX9Dqm0JQsL0T17nUMc8pra/nHOemiK3W+dCIm0Y7/8GQiQ==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-kyriGuLkOu5uVv3cHRacJRk8XMm9gga7OzCuf3oxbgfqqIaazva4Qw8N19RCpVCW1EfwyWFGq3h5VD/QplkA0w==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-y6s2yant8vBdvIw6chTlPVxf9T5lzdmpSyEsI6gX3obTSaJyhw6nbfl6iWcQNx1jjgp4dYVhEf8PneNMqC8VQw==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-lKlI9BaHXy+L30d2CKWWn3B9Ye7LOqRHav8rzXcxAQMfZxj4iK1UuGRT+27/KzQMZBYMPd9G4R7x7lV8RngFdQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-WH8dfjSMWa5uCzheemqy+UzQXIpIymWVrnBOK3L3eBusYFhAg3L0ChBhjKBDjPDlKOquoeRrdt5D8ebgBjlY8g==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-U21Aj8pPjxW7wEH1Itm7Ik1rBO5rTXyxKWLjZXEDlT1agXxcwCVbL/5rffUQzbeRFAk/t4KiUGi4rTvAS62wTQ==}
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-D6sKXKLPZZ45csOfj3mUQ1bzBgO5mJw7qfpAIYLlk7H+slrHlIoOwxwWgwcuBTY7JJ0jLgiq2062yLHxjEfdeA==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-jtJAqqnltG12YCy7VpTtIIEOlRNmVDz11SQrZlxFUHWgIYa0UcTEUDeLpP4h9IW7KtnxJKBo/xu1JLRq4tY6fw==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-/mJkwVoFZZl4TtkPmAhdZYuiSjaegJ4zs5l7YJdwKPnN7/6xmvJwrQhj9UGQHwusyUBZ5/V+6cXQFEG7Em154Q==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-VaWHjSacuvcDpUPKriQMeG4xzNTKRuK7tZJL0toDQdB4uei2LX5qUBUqTrI0oBWW5Rk0QdUZR83LkhCLVJWtew==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-vpSXG73F0lFMsb1uwgut7SiuFNF0Hk5QRD7i8DJO5oyyTrSkrHUjQHFFL7ONb+zmPzLC1J8SlfHXkldJZlpKww==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-gx9cV9LutgY0f25AQtFqW285jGCsHpAxYKbmoN4MOUEYOVX9WiDfrf59Ta9c/SelLvOanVsu5V1hVzBsJyX37w==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-6EXelSVOv0LQrAretlDMIxMV/e1+GfJftBfPsHuCTPrTbmcoEIHQRC9VsGQDS/k4ija6oA2GTqTvoni8Ztbk8A==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-I+tPJgq7M5Qcu4pX97J1Hfo0s+bnCmj32IiZ3AJTFFA/+xScaL0SF7T7tW5YtXR45ZwYjaSdnmQ80CwrLxJjdQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
resolution: {integrity: sha512-XwcyTnRssZ+ssuL500ojFaMdR5w+eb9XVs7MHd2G8q3A5N5a2UAKIhExiTEbWyAGXSgfHo/LQxUTX2Lq1cxSRg==}
|
||||
'@tsparticles/[email protected].3':
|
||||
resolution: {integrity: sha512-hh62wuc4HZ+BydUC62bA74hgr+qV888iWm23V222d6rqIkOpYzi5tcoPAh48Mgjl2gMSigTE6sySJez7reffNQ==}
|
||||
peerDependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tufjs/[email protected]':
|
||||
resolution: {integrity: sha512-yVtV8zsdo8qFHe+/3kw81dSLyF7D576A5cCFCi4X7B39tWT7SekaEFUnvnWJHz+9qO7qJTah1JbrDjWKqFtdWA==}
|
||||
@@ -3864,8 +3864,8 @@ packages:
|
||||
'@vscode/[email protected]':
|
||||
resolution: {integrity: sha512-2ZUIRfhofZ/npLlf872EBnPmn27Kt4M2UssmQIfnJvgGgMYZJ5fvtHEDnttBBf2hnVtBgNCqZMVHJA+wsFVqTA==}
|
||||
|
||||
'@vvo/[email protected]2.0':
|
||||
resolution: {integrity: sha512-MGsedU65ypk/Y7yrp+oOLBzhQj/124xvyoUzx7csPiBSf1b3lgTp5imatuWU4SF6s1Ue3I6GxC+GxE8R5O1l0w==}
|
||||
'@vvo/[email protected]3.0':
|
||||
resolution: {integrity: sha512-VdAiMSmHFzdL2csQU4QtNEV0l4JOgMPdFvk7cEm4a5LRVZsM3tQ2Kxyk5O6plB55DjOwIyoTt29mFi+vA9GYSw==}
|
||||
|
||||
'@web/[email protected]':
|
||||
resolution: {integrity: sha512-XVKH79pk4d3EHRhofete8eAnqto1e8mCRAqPV00KLNFzCWSe8sWmLnqKCqkPNARC6nksMaGrATnA5sPDRllMpQ==}
|
||||
@@ -4050,9 +4050,6 @@ packages:
|
||||
resolution: {integrity: sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==}
|
||||
engines: {node: '>= 8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Of/R0wqp83cgHozfIYLbBMnej79U/SVGOOyuB3VVFv1NRM/PSFMK12x9KVtiYzJqmnU5WR2qp0Z5rHb7sWGnFQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==}
|
||||
|
||||
@@ -4400,6 +4397,9 @@ packages:
|
||||
resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==}
|
||||
engines: {node: '>=18'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-lmNyBzi6iYiqyrExTctfmjd8tInx+dHu9PBomTfH5RXS53c27JMFJgzNv5TyAnm3PgGf+MLy0+Ws41/HlVxdKQ==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
|
||||
engines: {node: '>= 10.0'}
|
||||
@@ -4420,10 +4420,6 @@ packages:
|
||||
resolution: {integrity: sha512-bXfOC4QcT1tKXGorxL3wbJm6XJPDqEnij2gQ2m7ESQuE+/z9YFIWnl/5RpTiKWbMq3EVKR4fRLJGn6DVfu0mpw==}
|
||||
engines: {node: '>=18.20'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Su+uU5sr1jkUy1sGRpLKjKrvEOVXgSgiSInwa/qeID6aJ07yh+5NWc3h2QfjHjBnfX4LhtFcuAWKUsJ3r+fjbg==}
|
||||
engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
|
||||
|
||||
@@ -4909,6 +4905,9 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-poHGpORABojJJucnV9KbOavETW8lBVnphkW77ER5/BQ5Fz7oXSoCNek7IH3vR5nRjdsEz926ibFYX8KtLQmdyw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-BldNY5mwkVu+cQ5Ymn3e6hB9Sx8XHx4ZevWbiUyZbfKxJxYO27Hf0wfzkxIaD5CAJMaGl3YspKELj0ff/7nCMA==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -4962,8 +4961,8 @@ packages:
|
||||
eslint-plugin-import: '>=1.4.0'
|
||||
webpack: '>=1.11.0'
|
||||
|
||||
[email protected]7.1:
|
||||
resolution: {integrity: sha512-4cdstYkKCyjumM2Q9NSI03K8D2a9F4Ssz33K2lv2hQa4KmR9jPLwk3uWGtNvclfqBrPGfGuMBwsGMbe6dMRbfg==}
|
||||
[email protected]8.0:
|
||||
resolution: {integrity: sha512-UfxHFmV+f9vwybrNn6LQ1rPzfeDMPGi/dRbqQnCFO/jBzYrujSOyX7jxqBSYr/HwXs/V4XJdEGPvC3Re46nzKg==}
|
||||
engines: {node: ^18.18.0 || ^20.9.0 || >=21.1.0}
|
||||
peerDependencies:
|
||||
'@typescript-eslint/utils': ^8.56.0
|
||||
@@ -5067,10 +5066,6 @@ packages:
|
||||
resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==}
|
||||
engines: {node: '>=0.8.x'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==}
|
||||
|
||||
@@ -5288,10 +5283,6 @@ packages:
|
||||
resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-w9UMqWwJxHNOvoNzSJ2oPF5wvYcvP7jUvYzhp67yEhTi17ZDBBC1z9pTdGuzjD+EFIqLSYRweZjqfiPzQ06Ebg==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5498,10 +5489,6 @@ packages:
|
||||
resolution: {integrity: sha512-ag87y7cJJ9/3+GxFr8Oy4O5faDsGRGnBGsJj/YjOSsSx/5eadKLYTMPlzuR6obgoCDDm0abAAZitXXQkMOPSpA==}
|
||||
engines: {node: '>= 20'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==}
|
||||
engines: {node: '>=10.17.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5gs5ytaNjBrh5Ow3zrvdUUY+0VxIuWVL4i9irt6friV+BqdCfmV11CQTWMiBYWHbXhco+J1kHfTOUkePhCDvMA==}
|
||||
engines: {node: '>=18'}
|
||||
@@ -5638,11 +5625,6 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==}
|
||||
engines: {node: '>=8'}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-+XSoyS05OdBbhFuELhgTCpFNHkpBOJqtsZfUFFpe5QTw+9Sjbh8zitxhQkYAo6wV7e1Vb8cAPvpCk9jGam/82g==}
|
||||
engines: {node: '>= 0.4'}
|
||||
@@ -5792,10 +5774,6 @@ packages:
|
||||
resolution: {integrity: sha512-eXK1UInq2bPmjyX6e3VHIzMLobc4J94i4AWn+Hpq3OU5KkrRC96OAcR3PRJ/pGu6m8TRnBHP9dkXQVsT/COVIA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==}
|
||||
|
||||
@@ -6435,10 +6413,6 @@ packages:
|
||||
resolution: {integrity: sha512-vzc1svxw/kw1IRjFsLi6gaxe1Olqm88V0tIfu2u5raL0b1gChe6ZEXNkyUlKxUC7s/egt5NxZHkbY18tMKKLfQ==}
|
||||
engines: {node: ^22.22.2 || ^24.15.0 || >=26.0.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==}
|
||||
engines: {node: '>=8'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==}
|
||||
|
||||
@@ -7040,8 +7014,8 @@ packages:
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-CinAq1xWb0vR3twAv9evEU8cNWkXCb9kd5ePAHUKJBkOsUpR1wt/CvGdeca7vqumL1U5cSaeVQ6zZMxiJ3yWsg==}
|
||||
|
||||
[email protected].6:
|
||||
resolution: {integrity: sha512-QEjUSA+sD4Rotm1znR8s50YqA3kYpRGPmtd5GlFxbaL9n/FdUNbqMhxClqdditSk0LlZyA/dhud6XNRTOC9x2Q==}
|
||||
[email protected].7:
|
||||
resolution: {integrity: sha512-mTJ9LAyBwHhO2T/PNaI0oWxmKl7FFuJ+ZEHKL1rRO69fWV1Y4qcsFB3gpuD2HfHvJCwvJq5ChibjjAGe0wc0vw==}
|
||||
engines: {node: '>= 14'}
|
||||
hasBin: true
|
||||
|
||||
@@ -7295,10 +7269,6 @@ packages:
|
||||
resolution: {integrity: sha512-ZprKx+bBLXv067WTCALv8SSz5l2+XhpYCsVtSqlMnkAXMWDq+/ekVbl1ghqP9rUHTzv6sm/DwCOiYutU/yp1fw==}
|
||||
engines: {node: '>=10'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
@@ -7439,6 +7409,10 @@ packages:
|
||||
resolution: {integrity: sha512-nIZ2N2dFpjBsg2hgitKu7mA+FvsIJuGJOwysRngmVRRmWgR/UyPjwJj8c/fT/dhm8EkHnF5EOogRf+0Z2qrimA==}
|
||||
engines: {node: '>=20.0.0'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-uo33abH+Ays0xYaDysoBt494Hb3hsEczMpcC0MwFl773pazORx4fmvKhclhR1wonUbB6vvpRsvVMwnhfqeMc+A==}
|
||||
engines: {node: ^16.14.0 || >= 17.3.0}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==}
|
||||
|
||||
@@ -11118,88 +11092,88 @@ snapshots:
|
||||
magic-string: 0.30.21
|
||||
string.prototype.matchall: 4.0.12
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
'@tsparticles/[email protected].3':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/plugin-blend': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/plugin-hex-color': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/plugin-hsl-color': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/plugin-move': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/plugin-rgb-color': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/shape-circle': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/updater-opacity': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/updater-out-modes': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/updater-paint': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/updater-size': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/engine': 4.5.3
|
||||
'@tsparticles/plugin-blend': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/plugin-hex-color': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/plugin-hsl-color': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/plugin-move': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/plugin-rgb-color': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/shape-circle': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/updater-opacity': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/updater-out-modes': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/updater-paint': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/updater-size': 4.5.3(@tsparticles/[email protected].3)
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2': {}
|
||||
'@tsparticles/[email protected].3': {}
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)(@tsparticles/[email protected].2(@tsparticles/[email protected].2))':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)(@tsparticles/[email protected].3(@tsparticles/[email protected].3))':
|
||||
dependencies:
|
||||
'@tsparticles/canvas-utils': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/plugin-interactivity': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/canvas-utils': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/engine': 4.5.3
|
||||
'@tsparticles/plugin-interactivity': 4.5.3(@tsparticles/[email protected].3)
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2':
|
||||
'@tsparticles/[email protected].3':
|
||||
dependencies:
|
||||
'@tsparticles/basic': 4.5.2
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/interaction-particles-links': 4.5.2(@tsparticles/[email protected].2)(@tsparticles/[email protected].2(@tsparticles/[email protected].2))
|
||||
'@tsparticles/plugin-interactivity': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/basic': 4.5.3
|
||||
'@tsparticles/engine': 4.5.3
|
||||
'@tsparticles/interaction-particles-links': 4.5.3(@tsparticles/[email protected].3)(@tsparticles/[email protected].3(@tsparticles/[email protected].3))
|
||||
'@tsparticles/plugin-interactivity': 4.5.3(@tsparticles/[email protected].3)
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/animation-utils': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/animation-utils': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tsparticles/[email protected].2(@tsparticles/[email protected].2)':
|
||||
'@tsparticles/[email protected].3(@tsparticles/[email protected].3)':
|
||||
dependencies:
|
||||
'@tsparticles/animation-utils': 4.5.2(@tsparticles/[email protected].2)
|
||||
'@tsparticles/engine': 4.5.2
|
||||
'@tsparticles/animation-utils': 4.5.3(@tsparticles/[email protected].3)
|
||||
'@tsparticles/engine': 4.5.3
|
||||
|
||||
'@tufjs/[email protected]': {}
|
||||
|
||||
@@ -11647,7 +11621,7 @@ snapshots:
|
||||
|
||||
'@vscode/[email protected]': {}
|
||||
|
||||
'@vvo/[email protected]2.0': {}
|
||||
'@vvo/[email protected]3.0': {}
|
||||
|
||||
'@web/[email protected]':
|
||||
dependencies:
|
||||
@@ -11786,8 +11760,6 @@ snapshots:
|
||||
normalize-path: 3.0.0
|
||||
picomatch: 2.3.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -12165,6 +12137,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
source-map: 0.6.1
|
||||
@@ -12179,12 +12153,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
arch: 2.2.0
|
||||
execa: 5.1.1
|
||||
is-wsl: 2.2.0
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
string-width: 4.2.3
|
||||
@@ -12693,6 +12661,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
es-errors: 1.3.0
|
||||
@@ -12751,15 +12721,17 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
[email protected]7.1(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected]):
|
||||
[email protected]8.0(@typescript-eslint/[email protected]([email protected]([email protected]))([email protected])([email protected]))([email protected]([email protected]))([email protected]):
|
||||
dependencies:
|
||||
'@typescript-eslint/types': 8.72.1
|
||||
cjs-module-lexer: 2.3.0
|
||||
comment-parser: 1.4.8
|
||||
debug: 4.4.3([email protected])
|
||||
es-module-lexer: 3.0.4
|
||||
eslint: 10.14.0([email protected])
|
||||
eslint-import-context: 0.1.9([email protected])
|
||||
is-glob: 4.0.3
|
||||
minimatch: 10.2.6
|
||||
minimatch: 11.0.5
|
||||
semver: 7.8.5
|
||||
stable-hash-x: 0.2.0
|
||||
unrs-resolver: 1.12.2
|
||||
@@ -12884,18 +12856,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
cross-spawn: 7.0.6
|
||||
get-stream: 6.0.1
|
||||
human-signals: 2.1.0
|
||||
is-stream: 2.0.1
|
||||
merge-stream: 2.0.0
|
||||
npm-run-path: 4.0.1
|
||||
onetime: 5.1.2
|
||||
signal-exit: 3.0.7
|
||||
strip-final-newline: 2.0.0
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -13145,8 +13105,6 @@ snapshots:
|
||||
dunder-proto: 1.0.1
|
||||
es-object-atoms: 1.1.2
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -13419,8 +13377,6 @@ snapshots:
|
||||
- kerberos
|
||||
- supports-color
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -13550,8 +13506,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
call-bound: 1.0.4
|
||||
@@ -13674,10 +13628,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
is-docker: 2.2.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -14368,10 +14318,6 @@ snapshots:
|
||||
- kerberos
|
||||
- supports-color
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
path-key: 3.1.1
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
boolbase: 1.0.0
|
||||
@@ -15042,7 +14988,7 @@ snapshots:
|
||||
path-to-regexp: 3.3.0
|
||||
range-parser: 1.2.0
|
||||
|
||||
[email protected].6([email protected]):
|
||||
[email protected].7([email protected]):
|
||||
dependencies:
|
||||
'@zeit/schemas': 2.36.0
|
||||
ajv: 8.18.0
|
||||
@@ -15050,10 +14996,10 @@ snapshots:
|
||||
boxen: 7.0.0
|
||||
chalk: 5.0.1
|
||||
chalk-template: 0.4.0
|
||||
clipboardy: 3.0.0
|
||||
compression: 1.8.1([email protected])
|
||||
is-port-reachable: 4.0.0
|
||||
serve-handler: 6.1.7
|
||||
tinyclip: 0.1.15
|
||||
update-check: 1.5.4
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
@@ -15397,8 +15343,6 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
@@ -15514,6 +15458,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
/** Where a value is in a config, e.g. `["actions", 0, "then", 1]`. */
|
||||
export type ConfigPath = (string | number)[];
|
||||
|
||||
export const stringifyPath = (path: ConfigPath): string => path.join("/");
|
||||
|
||||
export const pathEquals = (a: ConfigPath, b: ConfigPath): boolean =>
|
||||
a.length === b.length && a.every((segment, index) => segment === b[index]);
|
||||
|
||||
export const pathStartsWith = (path: ConfigPath, prefix: ConfigPath): boolean =>
|
||||
prefix.length <= path.length &&
|
||||
prefix.every((segment, index) => segment === path[index]);
|
||||
|
||||
/** Formats a path the way it reads in YAML, e.g. `actions[1].then[0].delay`. */
|
||||
export const formatPath = (path: ConfigPath): string =>
|
||||
path
|
||||
.map((segment, index) =>
|
||||
typeof segment === "number"
|
||||
? `[${segment}]`
|
||||
: index === 0
|
||||
? segment
|
||||
: `.${segment}`
|
||||
)
|
||||
.join("");
|
||||
@@ -30,6 +30,9 @@ export class HaAutomationRow extends LitElement {
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public dim = false;
|
||||
|
||||
@property({ type: Boolean, reflect: true })
|
||||
public error = false;
|
||||
|
||||
@query(".row")
|
||||
private _rowElement?: HTMLDivElement;
|
||||
|
||||
@@ -232,6 +235,15 @@ export class HaAutomationRow extends LitElement {
|
||||
:host([highlight]) .row:hover {
|
||||
background-color: rgba(var(--rgb-primary-color), 0.16);
|
||||
}
|
||||
:host([error]) .row {
|
||||
background-color: var(--ha-color-fill-danger-quiet-resting);
|
||||
}
|
||||
:host([error][selected]) .row,
|
||||
:host([error][selected]) .row:focus {
|
||||
outline: 2px solid var(--ha-color-border-danger-loud);
|
||||
outline-offset: -2px;
|
||||
background-color: var(--ha-color-fill-danger-normal-resting);
|
||||
}
|
||||
|
||||
.icons,
|
||||
.leading-icon-wrapper,
|
||||
|
||||
@@ -97,6 +97,8 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
|
||||
@state() private _dateValue?: string;
|
||||
|
||||
@state() private _rangeIncomplete = false;
|
||||
|
||||
@state() private _timeValue = {
|
||||
from: { hours: 0, minutes: 0 },
|
||||
to: { hours: 23, minutes: 59 },
|
||||
@@ -195,6 +197,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
.locale=${this._i18n.locale.language}
|
||||
.focusedDate=${this._focusDate}
|
||||
@focusday=${this._focusChanged}
|
||||
@rangestart=${this._handleRangeStart}
|
||||
@change=${this._handleChange}
|
||||
show-outside-days
|
||||
.firstDayOfWeek=${firstWeekdayIndex(this._i18n.locale)}
|
||||
@@ -253,7 +256,9 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
<ha-button appearance="plain" @click=${this._cancel}
|
||||
>${this._i18n.localize("ui.common.cancel")}</ha-button
|
||||
>
|
||||
<ha-button .disabled=${!this._dateValue} @click=${this._save}
|
||||
<ha-button
|
||||
.disabled=${!this._dateValue || this._rangeIncomplete}
|
||||
@click=${this._save}
|
||||
>${this._i18n.localize(
|
||||
"ui.components.date-range-picker.select"
|
||||
)}</ha-button
|
||||
@@ -280,7 +285,7 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
}
|
||||
|
||||
private _save() {
|
||||
if (!this._dateValue) {
|
||||
if (!this._dateValue || this._rangeIncomplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -331,9 +336,21 @@ export class DateRangePicker extends MobileAwareMixin(LitElement) {
|
||||
this._focusDate = ev.target.focusedDate;
|
||||
}
|
||||
|
||||
private _handleRangeStart(
|
||||
ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
|
||||
) {
|
||||
this._rangeIncomplete = matchMedia("(hover: hover)").matches;
|
||||
if (this._rangeIncomplete) {
|
||||
return;
|
||||
}
|
||||
const day = ev.target.tentative;
|
||||
this._dateValue = `${day}/${day}`;
|
||||
}
|
||||
|
||||
private _handleChange(
|
||||
ev: HASSDomTargetEvent<HTMLElementTagNameMap["calendar-range"]>
|
||||
) {
|
||||
this._rangeIncomplete = false;
|
||||
const dateElement = ev.target as HTMLElementTagNameMap["calendar-range"];
|
||||
this._dateValue = dateElement.value;
|
||||
this._focusDate = dateElement.focusedDate;
|
||||
|
||||
@@ -14,6 +14,12 @@ class HaDeviceActionPicker extends HaDeviceAutomationPicker<DeviceAction> {
|
||||
);
|
||||
}
|
||||
|
||||
protected get NOT_SUPPORTED_TEXT() {
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.actions.not_supported"
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.actions.unknown_action"
|
||||
|
||||
@@ -49,6 +49,10 @@ export abstract class HaDeviceAutomationPicker<
|
||||
|
||||
@property({ type: Object }) public value?: T;
|
||||
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@state() private _automations?: T[];
|
||||
|
||||
// Trigger an empty render so we start with a clean DOM.
|
||||
@@ -106,6 +110,13 @@ export abstract class HaDeviceAutomationPicker<
|
||||
);
|
||||
}
|
||||
|
||||
/** Error shown when the picked device has nothing to choose from. */
|
||||
protected get NOT_SUPPORTED_TEXT() {
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.actions.not_supported"
|
||||
);
|
||||
}
|
||||
|
||||
private _localizeDeviceAutomation: (
|
||||
localize: LocalizeFunc,
|
||||
states: HassEntities,
|
||||
@@ -163,6 +174,12 @@ export abstract class HaDeviceAutomationPicker<
|
||||
.value=${value}
|
||||
.disabled=${this._automations?.length === 0}
|
||||
.loading=${this._automations === undefined}
|
||||
.invalid=${this.invalid}
|
||||
.errorMessage=${
|
||||
this._automations?.length === 0
|
||||
? this.NOT_SUPPORTED_TEXT
|
||||
: this.errorMessage
|
||||
}
|
||||
.getItems=${this._getItems(
|
||||
value,
|
||||
this._automations,
|
||||
|
||||
@@ -14,6 +14,12 @@ class HaDeviceConditionPicker extends HaDeviceAutomationPicker<DeviceCondition>
|
||||
);
|
||||
}
|
||||
|
||||
protected get NOT_SUPPORTED_TEXT() {
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.conditions.not_supported"
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.conditions.unknown_condition"
|
||||
|
||||
@@ -107,6 +107,10 @@ export class HaDevicePicker extends LitElement {
|
||||
|
||||
@property() public helper?: string;
|
||||
|
||||
@property({ type: Boolean }) public invalid = false;
|
||||
|
||||
@property({ attribute: "error-message" }) public errorMessage?: string;
|
||||
|
||||
@property() public placeholder?: string;
|
||||
|
||||
@property({ type: String, attribute: "search-label" })
|
||||
@@ -521,6 +525,8 @@ export class HaDevicePicker extends LitElement {
|
||||
.autofocus=${this.autofocus}
|
||||
.disabled=${this.disabled}
|
||||
.helper=${this.helper}
|
||||
.invalid=${this.invalid}
|
||||
.errorMessage=${this.errorMessage}
|
||||
.label=${this.label}
|
||||
.searchLabel=${this.searchLabel}
|
||||
.notFoundLabel=${this._notFoundLabel}
|
||||
|
||||
@@ -14,6 +14,12 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
|
||||
);
|
||||
}
|
||||
|
||||
protected get NOT_SUPPORTED_TEXT() {
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.triggers.not_supported"
|
||||
);
|
||||
}
|
||||
|
||||
protected get UNKNOWN_AUTOMATION_TEXT() {
|
||||
return this._i18n.localize(
|
||||
"ui.panel.config.devices.automation.triggers.unknown_trigger"
|
||||
|
||||
@@ -62,10 +62,6 @@ export class HaControlSelect extends LitElement {
|
||||
});
|
||||
}
|
||||
|
||||
public override focus(): void {
|
||||
this._focusOption(this._tabbableIndex);
|
||||
}
|
||||
|
||||
private _handleBlur(ev: FocusEvent) {
|
||||
// Only reset if focus is leaving the entire component
|
||||
if (!this.contains(ev.relatedTarget as Node)) {
|
||||
|
||||
@@ -219,6 +219,7 @@ export class HaControlSlider extends LitElement {
|
||||
this.pressed = false;
|
||||
this._hideTooltip();
|
||||
this.value = savedValue;
|
||||
fireEvent(this, "slider-moved", { value: undefined });
|
||||
});
|
||||
this._mc.on("panmove", (e) => {
|
||||
if (this.disabled) return;
|
||||
|
||||
@@ -214,15 +214,19 @@ class HaHsColorPicker extends LitElement {
|
||||
this._mc.add(new Tap({ event: "singletap" }));
|
||||
|
||||
let savedPosition;
|
||||
let savedValue: [number, number] | undefined;
|
||||
this._mc.on("panstart", (e) => {
|
||||
if (this.disabled) return;
|
||||
this._pressed = e.pointerType;
|
||||
savedPosition = this._cursorPosition;
|
||||
savedValue = this._localValue;
|
||||
});
|
||||
this._mc.on("pancancel", () => {
|
||||
if (this.disabled) return;
|
||||
this._pressed = undefined;
|
||||
this._cursorPosition = savedPosition;
|
||||
this._localValue = savedValue;
|
||||
fireEvent(this, "cursor-moved", { value: undefined });
|
||||
});
|
||||
this._mc.on("panmove", (e) => {
|
||||
if (this.disabled) return;
|
||||
|
||||
@@ -21,6 +21,8 @@ export class HaActionSelector extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public value?: Action;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
@@ -51,6 +53,7 @@ export class HaActionSelector extends LitElement {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-action
|
||||
.pathKey=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
.actions=${this._actions(this.value)}
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -16,6 +16,8 @@ export class HaConditionSelector extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public value?: Condition;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
@@ -27,6 +29,7 @@ export class HaConditionSelector extends LitElement {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-condition
|
||||
.pathKey=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
.conditions=${this.value || []}
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -17,6 +17,8 @@ export class HaTriggerSelector extends LitElement {
|
||||
|
||||
@property({ attribute: false }) public value?: Trigger;
|
||||
|
||||
@property() public name?: string;
|
||||
|
||||
@property() public label?: string;
|
||||
|
||||
@property({ type: Boolean, reflect: true }) public disabled = false;
|
||||
@@ -32,6 +34,7 @@ export class HaTriggerSelector extends LitElement {
|
||||
return html`
|
||||
${this.label ? html`<label>${this.label}</label>` : nothing}
|
||||
<ha-automation-trigger
|
||||
.pathKey=${this.name}
|
||||
.disabled=${this.disabled}
|
||||
.triggers=${this._triggers(this.value)}
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -41,6 +41,7 @@ import "./ha-selector/ha-selector";
|
||||
import type { HaSelector } from "./ha-selector/ha-selector";
|
||||
import "./ha-service-picker";
|
||||
import "./ha-service-section-icon";
|
||||
import "./ha-input-helper-text";
|
||||
import "./ha-settings-row";
|
||||
import "./ha-yaml-editor";
|
||||
import type { HaYamlEditor } from "./ha-yaml-editor";
|
||||
@@ -119,6 +120,15 @@ export class HaServiceControl extends LitElement {
|
||||
@property({ attribute: "hide-description", type: Boolean })
|
||||
public hideDescription = false;
|
||||
|
||||
/** Error shown below the target picker. */
|
||||
@property({ attribute: false }) public targetError?: string;
|
||||
|
||||
/** Errors shown below the data fields, keyed by field. */
|
||||
@property({ attribute: false }) public dataErrors?: Record<string, string>;
|
||||
|
||||
/** Error of the data as a whole. */
|
||||
@property({ attribute: false }) public dataError?: string;
|
||||
|
||||
@state() private _value!: this["value"];
|
||||
|
||||
@state() private _checkedKeys = new Set();
|
||||
@@ -542,13 +552,14 @@ export class HaServiceControl extends LitElement {
|
||||
${
|
||||
serviceData && "target" in serviceData
|
||||
? html`<ha-selector
|
||||
class="target-selector"
|
||||
.hass=${this.hass}
|
||||
.selector=${targetSelector}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this._value?.target}
|
||||
></ha-selector>`
|
||||
class="target-selector"
|
||||
.hass=${this.hass}
|
||||
.selector=${targetSelector}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._targetChanged}
|
||||
.value=${this._value?.target}
|
||||
></ha-selector>
|
||||
${this._renderError(this.targetError)}`
|
||||
: entityId
|
||||
? html`<ha-entity-picker
|
||||
.disabled=${this.disabled}
|
||||
@@ -594,7 +605,10 @@ export class HaServiceControl extends LitElement {
|
||||
this._hasFilteredFields(fields, targetEntities)
|
||||
? html`<ha-expansion-panel
|
||||
left-chevron
|
||||
.expanded=${!dataField.collapsed}
|
||||
.expanded=${
|
||||
!dataField.collapsed ||
|
||||
fields.some((field) => this.dataErrors?.[field.key])
|
||||
}
|
||||
.header=${
|
||||
this.hass.localize(
|
||||
`component.${domain}.services.${serviceName}.sections.${dataField.key}.name`,
|
||||
@@ -626,7 +640,12 @@ export class HaServiceControl extends LitElement {
|
||||
</ha-expansion-panel>`
|
||||
: nothing;
|
||||
})
|
||||
} `;
|
||||
}
|
||||
${this._unshownErrors(
|
||||
serviceData,
|
||||
targetEntities,
|
||||
Boolean(shouldRenderServiceDataYaml)
|
||||
).map((error) => this._renderError(error))}`;
|
||||
}
|
||||
|
||||
private _getSectionDescription(
|
||||
@@ -690,6 +709,25 @@ export class HaServiceControl extends LitElement {
|
||||
? this.hass.services[domain][serviceName].description_placeholders
|
||||
: undefined;
|
||||
|
||||
const fieldError = this.dataErrors?.[dataField.key];
|
||||
const selectorTemplate = html`<ha-selector
|
||||
.context=${this._selectorContext(targetEntities)}
|
||||
.disabled=${
|
||||
this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this._value?.data || this._value.data[dataField.key] === undefined))
|
||||
}
|
||||
.hass=${this.hass}
|
||||
.selector=${selector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${this._value?.data ? this._value.data[dataField.key] : undefined}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.required=${dataField.required}
|
||||
></ha-selector>`;
|
||||
|
||||
return dataField.selector
|
||||
? html`<ha-settings-row
|
||||
.narrow=${
|
||||
@@ -742,30 +780,50 @@ export class HaServiceControl extends LitElement {
|
||||
}
|
||||
></ha-markdown>
|
||||
</span>
|
||||
<ha-selector
|
||||
.context=${this._selectorContext(targetEntities)}
|
||||
.disabled=${
|
||||
this.disabled ||
|
||||
(showOptional &&
|
||||
!this._checkedKeys.has(dataField.key) &&
|
||||
(!this._value?.data ||
|
||||
this._value.data[dataField.key] === undefined))
|
||||
}
|
||||
.hass=${this.hass}
|
||||
.selector=${selector}
|
||||
.key=${dataField.key}
|
||||
@value-changed=${this._serviceDataChanged}
|
||||
.value=${
|
||||
this._value?.data ? this._value.data[dataField.key] : undefined
|
||||
}
|
||||
.placeholder=${dataField.default}
|
||||
.localizeValue=${this._localizeValueCallback}
|
||||
.required=${dataField.required}
|
||||
></ha-selector>
|
||||
<div class="field">
|
||||
${selectorTemplate}${this._renderError(fieldError)}
|
||||
</div>
|
||||
</ha-settings-row>`
|
||||
: "";
|
||||
};
|
||||
|
||||
// Errors without a field to show them on, e.g. of a field no target
|
||||
// supports or of a target the action doesn't have a picker for
|
||||
private _unshownErrors(
|
||||
serviceData: ExtHassService | undefined,
|
||||
targetEntities: string[] | null,
|
||||
dataYaml: boolean
|
||||
): string[] {
|
||||
const targetShown = Boolean(serviceData && "target" in serviceData);
|
||||
const shownFields = new Set(
|
||||
dataYaml
|
||||
? []
|
||||
: (serviceData?.flatFields ?? [])
|
||||
.filter(
|
||||
(field) =>
|
||||
field.selector &&
|
||||
(!field.filter ||
|
||||
this._filterField(field.filter, targetEntities))
|
||||
)
|
||||
.map((field) => field.key)
|
||||
);
|
||||
return [
|
||||
...(this.targetError && !targetShown ? [this.targetError] : []),
|
||||
...(this.dataError ? [this.dataError] : []),
|
||||
...Object.entries(this.dataErrors ?? {})
|
||||
.filter(([key]) => !shownFields.has(key))
|
||||
.map(([key, error]) => `${key}: ${error}`),
|
||||
];
|
||||
}
|
||||
|
||||
private _renderError(error: string | undefined) {
|
||||
return error
|
||||
? html`<ha-input-helper-text class="error"
|
||||
>${error}</ha-input-helper-text
|
||||
>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _selectorContext = memoizeOne((targetEntities: string[] | null) => ({
|
||||
filter_entity: targetEntities || undefined,
|
||||
}));
|
||||
@@ -991,6 +1049,17 @@ export class HaServiceControl extends LitElement {
|
||||
ha-settings-row {
|
||||
padding: var(--service-control-padding, 0 var(--ha-space-4));
|
||||
}
|
||||
/* Every field has this wrapper, so the selector isn't rebuilt when its
|
||||
error comes or goes */
|
||||
.field {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
ha-input-helper-text.error {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
padding-top: var(--ha-space-1);
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-settings-row[narrow] {
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,13 @@
|
||||
import type { ConfigPath } from "../common/util/config-path";
|
||||
import type { HomeAssistant } from "../types";
|
||||
|
||||
export interface ConfigValidationError {
|
||||
message: string;
|
||||
/** Where the problem is, `[]` when core does not know. */
|
||||
path: ConfigPath;
|
||||
code?: string | null;
|
||||
}
|
||||
|
||||
export interface ValidConfig {
|
||||
valid: true;
|
||||
error: null;
|
||||
@@ -8,10 +16,14 @@ export interface ValidConfig {
|
||||
export interface InvalidConfig {
|
||||
valid: false;
|
||||
error: string;
|
||||
/** Paths start at the item index of the section. */
|
||||
errors: ConfigValidationError[];
|
||||
}
|
||||
|
||||
type ValidKeys = "triggers" | "actions" | "conditions";
|
||||
|
||||
export type ValidationSectionKeys = Partial<Record<ValidKeys, string>>;
|
||||
|
||||
export const validateConfig = <T extends Partial<Record<ValidKeys, unknown>>>(
|
||||
hass: HomeAssistant,
|
||||
config: T
|
||||
@@ -20,3 +32,61 @@ export const validateConfig = <T extends Partial<Record<ValidKeys, unknown>>>(
|
||||
type: "validate_config",
|
||||
...config,
|
||||
});
|
||||
|
||||
/**
|
||||
* Flattens a `validate_config` result into errors with paths from the config
|
||||
* root. `sectionKeys` names the config key of a section when it differs from
|
||||
* the key it is validated under, e.g. `{ actions: "sequence" }` for scripts.
|
||||
*/
|
||||
export const validationResultErrors = (
|
||||
result: Partial<Record<ValidKeys, ValidConfig | InvalidConfig>>,
|
||||
sectionKeys: ValidationSectionKeys = {}
|
||||
): ConfigValidationError[] =>
|
||||
(Object.keys(result) as ValidKeys[]).flatMap((key) => {
|
||||
const section = result[key];
|
||||
if (!section || section.valid) {
|
||||
return [];
|
||||
}
|
||||
const sectionKey = sectionKeys[key] ?? key;
|
||||
return section.errors.map((error) => ({
|
||||
...error,
|
||||
path: [sectionKey, ...error.path],
|
||||
}));
|
||||
});
|
||||
|
||||
export const validateConfigErrors = async (
|
||||
hass: HomeAssistant,
|
||||
config: Partial<Record<ValidKeys, unknown>>,
|
||||
sectionKeys?: ValidationSectionKeys
|
||||
): Promise<ConfigValidationError[]> =>
|
||||
validationResultErrors(await validateConfig(hass, config), sectionKeys);
|
||||
|
||||
export interface ConfigSaveError {
|
||||
message: string;
|
||||
errors: ConfigValidationError[];
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads the errors of a failed config save. Errors that are not about the
|
||||
* config, like a network error, only have a message.
|
||||
*/
|
||||
export const parseConfigSaveError = (err: unknown): ConfigSaveError => {
|
||||
const { body, error } = (err ?? {}) as { body?: unknown; error?: unknown };
|
||||
const bodyObject =
|
||||
body && typeof body === "object"
|
||||
? (body as { message?: unknown; errors?: unknown })
|
||||
: undefined;
|
||||
const message = String(
|
||||
bodyObject?.message ||
|
||||
error ||
|
||||
(typeof body === "string" && body) ||
|
||||
(err instanceof Error && err.message) ||
|
||||
err
|
||||
);
|
||||
return {
|
||||
message,
|
||||
errors: Array.isArray(bodyObject?.errors)
|
||||
? (bodyObject.errors as ConfigValidationError[])
|
||||
: [],
|
||||
};
|
||||
};
|
||||
@@ -0,0 +1,128 @@
|
||||
import type { HassEntities } from "home-assistant-js-websocket";
|
||||
import type { ConfigEntry } from "./config_entries";
|
||||
import type { DataEntryFlowProgress } from "./data_entry_flow";
|
||||
import type { EntityRegistryEntry } from "./entity/entity_registry";
|
||||
import type { IntegrationSetup } from "./integration";
|
||||
import type { RepairsIssue } from "./repairs";
|
||||
import type { LoggedError } from "./system_log";
|
||||
import { getLoggedErrorIntegration } from "./system_log";
|
||||
|
||||
export type HealthStatus = "ok" | "warning" | "error" | "info";
|
||||
|
||||
export type OverallHealthStatus = Exclude<HealthStatus, "info">;
|
||||
|
||||
export interface ConfigEntryHealth {
|
||||
status: OverallHealthStatus;
|
||||
state: HealthStatus;
|
||||
entities: {
|
||||
status: HealthStatus;
|
||||
total: number;
|
||||
unavailable: EntityRegistryEntry[];
|
||||
};
|
||||
repairs: { status: HealthStatus; issues: RepairsIssue[] };
|
||||
logs: { status: HealthStatus; errors: number; warnings: number };
|
||||
flows: { status: HealthStatus; flows: DataEntryFlowProgress[] };
|
||||
setupSeconds?: number;
|
||||
}
|
||||
|
||||
const ATTENTION_SOURCES = ["reauth", "reconfigure"];
|
||||
|
||||
const STATE_STATUS: Record<ConfigEntry["state"], HealthStatus> = {
|
||||
loaded: "ok",
|
||||
setup_in_progress: "info",
|
||||
unload_in_progress: "info",
|
||||
not_loaded: "info",
|
||||
setup_retry: "warning",
|
||||
setup_error: "error",
|
||||
migration_error: "error",
|
||||
failed_unload: "error",
|
||||
};
|
||||
|
||||
const worst = (statuses: HealthStatus[]): OverallHealthStatus =>
|
||||
statuses.includes("error")
|
||||
? "error"
|
||||
: statuses.includes("warning")
|
||||
? "warning"
|
||||
: "ok";
|
||||
|
||||
export const computeConfigEntryHealth = (
|
||||
entry: ConfigEntry,
|
||||
entities: EntityRegistryEntry[],
|
||||
states: HassEntities,
|
||||
issues: RepairsIssue[],
|
||||
log: LoggedError[],
|
||||
flows: DataEntryFlowProgress[],
|
||||
setups: IntegrationSetup[],
|
||||
// Library loggers from the manifest, so dependency errors count too.
|
||||
loggers: string[] = []
|
||||
): ConfigEntryHealth => {
|
||||
const enabled = entities.filter((entity) => !entity.disabled_by);
|
||||
// Entities without a state yet (e.g. while the entry is not loaded) count as unavailable.
|
||||
const unavailable = enabled.filter(
|
||||
(entity) =>
|
||||
!states[entity.entity_id] ||
|
||||
states[entity.entity_id].state === "unavailable"
|
||||
);
|
||||
const entryIssues = issues.filter(
|
||||
(issue) =>
|
||||
!issue.ignored && (issue.issue_domain ?? issue.domain) === entry.domain
|
||||
);
|
||||
const entryLog = log.filter(
|
||||
(item) =>
|
||||
getLoggedErrorIntegration(item) === entry.domain ||
|
||||
loggers.some(
|
||||
(logger) => item.name === logger || item.name.startsWith(`${logger}.`)
|
||||
)
|
||||
);
|
||||
const sumCount = (levels: LoggedError["level"][]) =>
|
||||
entryLog
|
||||
.filter((item) => levels.includes(item.level))
|
||||
.reduce((total, item) => total + item.count, 0);
|
||||
const errors = sumCount(["error", "critical"]);
|
||||
const warnings = sumCount(["warning"]);
|
||||
const entryFlows = flows.filter(
|
||||
(flow) =>
|
||||
flow.handler === entry.domain &&
|
||||
flow.context.entry_id === entry.entry_id &&
|
||||
ATTENTION_SOURCES.includes(flow.context.source)
|
||||
);
|
||||
|
||||
// A disabled entry that failed to unload still needs a restart.
|
||||
const state: HealthStatus =
|
||||
entry.disabled_by && entry.state !== "failed_unload"
|
||||
? "info"
|
||||
: STATE_STATUS[entry.state];
|
||||
const health: ConfigEntryHealth = {
|
||||
status: "ok",
|
||||
state,
|
||||
entities: {
|
||||
status: entry.disabled_by || !unavailable.length ? "ok" : "warning",
|
||||
total: enabled.length,
|
||||
unavailable,
|
||||
},
|
||||
repairs: {
|
||||
status: entryIssues.some((issue) => issue.severity !== "warning")
|
||||
? "error"
|
||||
: entryIssues.length
|
||||
? "warning"
|
||||
: "ok",
|
||||
issues: entryIssues,
|
||||
},
|
||||
logs: {
|
||||
status: errors ? "error" : warnings ? "warning" : "ok",
|
||||
errors,
|
||||
warnings,
|
||||
},
|
||||
flows: { status: entryFlows.length ? "error" : "ok", flows: entryFlows },
|
||||
setupSeconds: setups.find((setup) => setup.domain === entry.domain)
|
||||
?.seconds,
|
||||
};
|
||||
health.status = worst([
|
||||
state,
|
||||
health.entities.status,
|
||||
health.repairs.status,
|
||||
health.logs.status,
|
||||
health.flows.status,
|
||||
]);
|
||||
return health;
|
||||
};
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import type { LocalizeFunc } from "../common/translations/localize";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
import type {
|
||||
DataEntryFlowProgress,
|
||||
DataEntryFlowStep,
|
||||
@@ -78,10 +78,10 @@ export const deleteConfigFlow = (
|
||||
|
||||
// The backend indexes a single key, and only these two exist.
|
||||
export const getConfigFlowHandlers = (
|
||||
hass: HomeAssistant,
|
||||
callApi: HomeAssistantApi["callApi"],
|
||||
type?: "helper" | "integration"
|
||||
) =>
|
||||
hass.callApi<string[]>(
|
||||
callApi<string[]>(
|
||||
"GET",
|
||||
`config/config_entries/flow_handlers${type ? `?type=${type}` : ""}`
|
||||
);
|
||||
|
||||
+3
-3
@@ -1,4 +1,4 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
export interface Counter {
|
||||
id: string;
|
||||
@@ -25,10 +25,10 @@ export const fetchCounter = (hass: HomeAssistant) =>
|
||||
hass.callWS<Counter[]>({ type: "counter/list" });
|
||||
|
||||
export const createCounter = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: CounterMutableParams
|
||||
) =>
|
||||
hass.callWS<Counter>({
|
||||
callWS<Counter>({
|
||||
type: "counter/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { Connection } from "home-assistant-js-websocket";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../../types";
|
||||
import type { ConfigEntry } from "../config_entries";
|
||||
import type {
|
||||
EntityRegistryDisplayEntry,
|
||||
@@ -231,11 +231,11 @@ export const devicesInArea = (devices: DeviceRegistryEntry[], areaId: string) =>
|
||||
devices.filter((device) => device.area_id === areaId);
|
||||
|
||||
export const updateDeviceRegistryEntry = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
deviceId: string,
|
||||
updates: Partial<DeviceRegistryEntryMutableParams>
|
||||
) =>
|
||||
hass.callWS<DeviceRegistryEntry>({
|
||||
callWS<DeviceRegistryEntry>({
|
||||
type: "config/device_registry/update",
|
||||
device_id: deviceId,
|
||||
...updates,
|
||||
|
||||
@@ -5,7 +5,7 @@ import memoizeOne from "memoize-one";
|
||||
import { computeDomain } from "../../common/entity/compute_domain";
|
||||
import { caseInsensitiveStringCompare } from "../../common/string/compare";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../../types";
|
||||
import type { LightColor } from "../light";
|
||||
import type { NextNamePart, RegistryEntry } from "../registry";
|
||||
import type { Segment } from "../vacuum";
|
||||
@@ -284,11 +284,11 @@ export const getExtendedEntityRegistryEntries = (
|
||||
});
|
||||
|
||||
export const updateEntityRegistryEntry = (
|
||||
hass: Pick<HomeAssistant, "callWS">,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
entityId: string,
|
||||
updates: Partial<EntityRegistryEntryUpdateParams>
|
||||
): Promise<UpdateEntityRegistryEntryResult> =>
|
||||
hass.callWS({
|
||||
callWS({
|
||||
type: "config/entity_registry/update",
|
||||
entity_id: entityId,
|
||||
...updates,
|
||||
@@ -388,11 +388,11 @@ export interface AutomaticEntityIdOverrides {
|
||||
}
|
||||
|
||||
export const getAutomaticEntityIds = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
entity_ids: string[],
|
||||
overrides?: Record<string, AutomaticEntityIdOverrides>
|
||||
) =>
|
||||
hass.callWS<Record<string, string | null>>({
|
||||
callWS<Record<string, string | null>>({
|
||||
type: "config/entity_registry/get_automatic_entity_ids",
|
||||
entity_ids,
|
||||
...(overrides && { overrides }),
|
||||
@@ -403,22 +403,22 @@ export const getAutomaticEntityIds = (
|
||||
* the area. Returns the entity ID of each entity after the change.
|
||||
*/
|
||||
export const updateEntitiesArea = async (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
entityIds: string[],
|
||||
areaId: string
|
||||
): Promise<Record<string, string>> => {
|
||||
await Promise.all(
|
||||
entityIds.map((entityId) =>
|
||||
updateEntityRegistryEntry(hass, entityId, { area_id: areaId })
|
||||
updateEntityRegistryEntry(callWS, entityId, { area_id: areaId })
|
||||
)
|
||||
);
|
||||
const newEntityIds = await getAutomaticEntityIds(hass, entityIds);
|
||||
const newEntityIds = await getAutomaticEntityIds(callWS, entityIds);
|
||||
const result: Record<string, string> = {};
|
||||
for (const entityId of entityIds) {
|
||||
const newEntityId = newEntityIds[entityId];
|
||||
if (newEntityId) {
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await updateEntityRegistryEntry(hass, entityId, {
|
||||
await updateEntityRegistryEntry(callWS, entityId, {
|
||||
new_entity_id: newEntityId,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -49,17 +49,12 @@ export interface SecurityAlertEntityConfig {
|
||||
export interface SecurityFrontendSystemData {
|
||||
alert_entities?: SecurityAlertEntityConfig[];
|
||||
favorite_entities?: string[];
|
||||
hidden_entities?: string[];
|
||||
}
|
||||
|
||||
export interface MaintenanceFrontendSystemData {
|
||||
hidden_entities?: string[];
|
||||
}
|
||||
|
||||
export interface ClimateFrontendSystemData {
|
||||
hidden_entities?: string[];
|
||||
}
|
||||
|
||||
export interface EnergyFrontendSystemData {
|
||||
// Stable "<view>.<card-type>" keys of energy dashboard cards the user has
|
||||
// hidden. An absent key or array means nothing is hidden (all cards visible),
|
||||
@@ -79,7 +74,6 @@ declare global {
|
||||
energy: EnergyFrontendSystemData;
|
||||
security: SecurityFrontendSystemData;
|
||||
maintenance: MaintenanceFrontendSystemData;
|
||||
climate: ClimateFrontendSystemData;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
export interface InputBoolean {
|
||||
id: string;
|
||||
@@ -17,10 +17,10 @@ export const fetchInputBoolean = (hass: HomeAssistant) =>
|
||||
hass.callWS<InputBoolean[]>({ type: "input_boolean/list" });
|
||||
|
||||
export const createInputBoolean = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: InputBooleanMutableParams
|
||||
) =>
|
||||
hass.callWS<InputBoolean>({
|
||||
callWS<InputBoolean>({
|
||||
type: "input_boolean/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
export interface InputButton {
|
||||
id: string;
|
||||
@@ -15,10 +15,10 @@ export const fetchInputButton = (hass: HomeAssistant) =>
|
||||
hass.callWS<InputButton[]>({ type: "input_button/list" });
|
||||
|
||||
export const createInputButton = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: InputButtonMutableParams
|
||||
) =>
|
||||
hass.callWS<InputButton>({
|
||||
callWS<InputButton>({
|
||||
type: "input_button/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -45,10 +45,10 @@ export const fetchInputDateTime = (hass: HomeAssistant) =>
|
||||
hass.callWS<InputDateTime[]>({ type: "input_datetime/list" });
|
||||
|
||||
export const createInputDateTime = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: InputDateTimeMutableParams
|
||||
) =>
|
||||
hass.callWS<InputDateTime>({
|
||||
callWS<InputDateTime>({
|
||||
type: "input_datetime/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
export interface InputNumber {
|
||||
id: string;
|
||||
@@ -29,10 +29,10 @@ export const fetchInputNumber = (hass: HomeAssistant) =>
|
||||
hass.callWS<InputNumber[]>({ type: "input_number/list" });
|
||||
|
||||
export const createInputNumber = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: InputNumberMutableParams
|
||||
) =>
|
||||
hass.callWS<InputNumber>({
|
||||
callWS<InputNumber>({
|
||||
type: "input_number/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {
|
||||
HassEntityAttributeBase,
|
||||
HassEntityBase,
|
||||
} from "home-assistant-js-websocket";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
interface InputSelectEntityAttributes extends HassEntityAttributeBase {
|
||||
options: string[];
|
||||
@@ -41,10 +41,10 @@ export const fetchInputSelect = (hass: HomeAssistant) =>
|
||||
hass.callWS<InputSelect[]>({ type: "input_select/list" });
|
||||
|
||||
export const createInputSelect = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: InputSelectMutableParams
|
||||
) =>
|
||||
hass.callWS<InputSelect>({
|
||||
callWS<InputSelect>({
|
||||
type: "input_select/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
export interface InputText {
|
||||
id: string;
|
||||
@@ -36,10 +36,10 @@ export const fetchInputText = (hass: HomeAssistant) =>
|
||||
hass.callWS<InputText[]>({ type: "input_text/list" });
|
||||
|
||||
export const createInputText = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: InputTextMutableParams
|
||||
) =>
|
||||
hass.callWS<InputText>({
|
||||
callWS<InputText>({
|
||||
type: "input_text/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -196,8 +196,8 @@ export const fetchIntegrationManifest = (
|
||||
.callWS<IntegrationManifest>({ type: "manifest/get", integration })
|
||||
.then(sanitizeManifest);
|
||||
|
||||
export const fetchIntegrationSetups = (hass: HomeAssistant) =>
|
||||
hass.callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
|
||||
export const fetchIntegrationSetups = (callWS: HomeAssistant["callWS"]) =>
|
||||
callWS<IntegrationSetup[]>({ type: "integration/setup_info" });
|
||||
|
||||
export const fetchIntegrationLogInfo = (conn) =>
|
||||
conn.sendMessagePromise({
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import type { Connection, HassEventBase } from "home-assistant-js-websocket";
|
||||
import { getCollection } from "home-assistant-js-websocket";
|
||||
import type { ConfigPath } from "../common/util/config-path";
|
||||
import type { HuiBadge } from "../panels/lovelace/badges/hui-badge";
|
||||
import type { HuiCard } from "../panels/lovelace/cards/hui-card";
|
||||
import type { HuiSection } from "../panels/lovelace/sections/hui-section";
|
||||
import type { LovelacePath } from "../panels/lovelace/editor/lovelace-path";
|
||||
import type { Lovelace } from "../panels/lovelace/types";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { LovelaceSectionConfig } from "./lovelace/config/section";
|
||||
@@ -21,7 +21,7 @@ export interface LovelaceViewElement extends HTMLElement {
|
||||
narrow?: boolean;
|
||||
// Temporary compatibility: custom view layouts still read the view index
|
||||
index?: number;
|
||||
path?: LovelacePath;
|
||||
path?: ConfigPath;
|
||||
cards?: HuiCard[];
|
||||
badges?: HuiBadge[];
|
||||
sections?: HuiSection[];
|
||||
@@ -34,7 +34,7 @@ export interface LovelaceSectionElement extends HTMLElement {
|
||||
hass?: HomeAssistant;
|
||||
lovelace?: Lovelace;
|
||||
preview?: boolean;
|
||||
path?: LovelacePath;
|
||||
path?: ConfigPath;
|
||||
cards?: HuiCard[];
|
||||
badges?: HuiBadge[];
|
||||
sections?: HuiSection[];
|
||||
|
||||
+3
-2
@@ -21,8 +21,9 @@ export const findOTBRInfoForDataset = (
|
||||
}
|
||||
|
||||
const preferredAddress = dataset.preferred_extended_address;
|
||||
if (preferredAddress && otbrInfo[preferredAddress]) {
|
||||
return otbrInfo[preferredAddress];
|
||||
const preferred = preferredAddress ? otbrInfo[preferredAddress] : undefined;
|
||||
if (preferred?.extended_pan_id === dataset.extended_pan_id) {
|
||||
return preferred;
|
||||
}
|
||||
|
||||
return Object.values(otbrInfo).find(
|
||||
|
||||
@@ -15,7 +15,7 @@ export const regenerateEntityIds = async (
|
||||
hass: HomeAssistant,
|
||||
entities: string[]
|
||||
): Promise<void> => {
|
||||
const entityIdsMapping = await getAutomaticEntityIds(hass, entities);
|
||||
const entityIdsMapping = await getAutomaticEntityIds(hass.callWS, entities);
|
||||
|
||||
const entityIdsEntries = Object.entries(entityIdsMapping);
|
||||
|
||||
@@ -95,7 +95,7 @@ export const regenerateEntityIds = async (
|
||||
entityIdsEntries
|
||||
.filter(([oldId, newId]) => newId && oldId !== newId)
|
||||
.forEach(([oldEntityId, newEntityId]) =>
|
||||
updateEntityRegistryEntry(hass, oldEntityId, {
|
||||
updateEntityRegistryEntry(hass.callWS, oldEntityId, {
|
||||
new_entity_id: newEntityId!,
|
||||
}).catch((err: any) => {
|
||||
showAlertDialog(element, {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
|
||||
export const weekdays = [
|
||||
"sunday",
|
||||
@@ -33,10 +33,10 @@ export const fetchSchedule = (hass: HomeAssistant) =>
|
||||
hass.callWS<Schedule[]>({ type: "schedule/list" });
|
||||
|
||||
export const createSchedule = (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: ScheduleMutableParams
|
||||
) =>
|
||||
hass.callWS<Schedule>({
|
||||
callWS<Schedule>({
|
||||
type: "schedule/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -23,8 +23,8 @@ export interface LoggedError {
|
||||
first_occurred: number;
|
||||
}
|
||||
|
||||
export const fetchSystemLog = async (hass: HomeAssistant) => {
|
||||
const log = await hass.callWS<LoggedError[]>({ type: "system_log/list" });
|
||||
export const fetchSystemLog = async (callWS: HomeAssistant["callWS"]) => {
|
||||
const log = await callWS<LoggedError[]>({ type: "system_log/list" });
|
||||
for (const error of log) {
|
||||
error.level = error.level.toLowerCase() as LoggedError["level"];
|
||||
}
|
||||
|
||||
+6
-3
@@ -11,7 +11,7 @@ import secondsToDuration from "../common/datetime/seconds_to_duration";
|
||||
import { formatNumericDuration } from "../common/datetime/format_duration";
|
||||
import type { FormatEntityStateFunc } from "../common/translations/entity-state";
|
||||
import type { HaDurationData } from "../components/ha-duration-input";
|
||||
import type { HomeAssistant } from "../types";
|
||||
import type { HomeAssistant, HomeAssistantApi } from "../types";
|
||||
import type { FrontendLocaleData } from "./translation";
|
||||
|
||||
export type TimerEntity = HassEntityBase & {
|
||||
@@ -48,8 +48,11 @@ export interface TimerMutableParams {
|
||||
export const fetchTimer = (hass: HomeAssistant) =>
|
||||
hass.callWS<Timer[]>({ type: "timer/list" });
|
||||
|
||||
export const createTimer = (hass: HomeAssistant, values: TimerMutableParams) =>
|
||||
hass.callWS<Timer>({
|
||||
export const createTimer = (
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
values: TimerMutableParams
|
||||
) =>
|
||||
callWS<Timer>({
|
||||
type: "timer/create",
|
||||
...values,
|
||||
});
|
||||
|
||||
@@ -303,7 +303,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
if (Object.keys(this._deviceUpdate).length) {
|
||||
const deviceUpdates = Object.entries(this._deviceUpdate).map(
|
||||
([deviceId, update]) =>
|
||||
updateDeviceRegistryEntry(this.hass, deviceId, {
|
||||
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
|
||||
name_by_user: update.name,
|
||||
area_id: update.area,
|
||||
}).catch((err: unknown) => {
|
||||
@@ -333,14 +333,14 @@ class StepFlowCreateEntry extends LitElement {
|
||||
});
|
||||
|
||||
const entityIdsMapping = await getAutomaticEntityIds(
|
||||
this.hass,
|
||||
this.hass.callWS,
|
||||
entityIds
|
||||
);
|
||||
|
||||
Object.entries(entityIdsMapping).forEach(([oldEntityId, newEntityId]) => {
|
||||
if (newEntityId) {
|
||||
entityUpdates.push(
|
||||
updateEntityRegistryEntry(this.hass, oldEntityId, {
|
||||
updateEntityRegistryEntry(this.hass.callWS, oldEntityId, {
|
||||
new_entity_id: newEntityId,
|
||||
}).catch((err) =>
|
||||
showAlertDialog(this, {
|
||||
@@ -423,7 +423,7 @@ class StepFlowCreateEntry extends LitElement {
|
||||
): Promise<ExtEntityRegistryEntry[]> {
|
||||
try {
|
||||
const newEntityIds = await updateEntitiesArea(
|
||||
this.hass,
|
||||
this.hass.callWS,
|
||||
this._entitiesWithoutDevice(entities).map((entity) => entity.entity_id),
|
||||
areaId
|
||||
);
|
||||
|
||||
@@ -146,7 +146,7 @@ export class HaMoreInfoCoverFavoritePositions extends LitElement {
|
||||
}
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this._api,
|
||||
this._api.callWS,
|
||||
this.entry.entity_id,
|
||||
{
|
||||
options_domain: "cover",
|
||||
|
||||
@@ -75,7 +75,7 @@ export class HaMoreInfoLightFavoriteColors extends LitElement {
|
||||
|
||||
private async _save(newFavoriteColors: LightColor[]): Promise<void> {
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this._api,
|
||||
this._api.callWS,
|
||||
this.entry!.entity_id,
|
||||
{
|
||||
options_domain: "light",
|
||||
|
||||
@@ -15,6 +15,7 @@ import {
|
||||
import { consumeLocalize } from "../../../../common/decorators/consume-context-entry";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import type { LocalizeFunc } from "../../../../common/translations/localize";
|
||||
import { PendingValueSync } from "../../../../common/util/pending-value-sync";
|
||||
import { throttle } from "../../../../common/util/throttle";
|
||||
import "../../../../components/ha-hs-color-picker";
|
||||
import "../../../../components/ha-icon";
|
||||
@@ -59,6 +60,8 @@ class LightRgbColorPicker extends LitElement {
|
||||
|
||||
@state() private _isInteracting?: boolean;
|
||||
|
||||
private _pendingSync = new PendingValueSync();
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -181,23 +184,29 @@ class LightRgbColorPicker extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _updateBrightnessAdjusted() {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
this._brightnessAdjusted = undefined;
|
||||
if (
|
||||
stateObj.attributes.color_mode === LightColorMode.RGB &&
|
||||
stateObj.attributes.rgb_color &&
|
||||
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
|
||||
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
|
||||
) {
|
||||
const maxVal = Math.max(...stateObj.attributes.rgb_color);
|
||||
|
||||
if (maxVal < 255) {
|
||||
this._brightnessAdjusted = maxVal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private _updateSliderValues() {
|
||||
const stateObj = this.stateObj;
|
||||
|
||||
if (stateObj.state === "on") {
|
||||
this._brightnessAdjusted = undefined;
|
||||
if (
|
||||
stateObj.attributes.color_mode === LightColorMode.RGB &&
|
||||
stateObj.attributes.rgb_color &&
|
||||
!lightSupportsColorMode(stateObj, LightColorMode.RGBWW) &&
|
||||
!lightSupportsColorMode(stateObj, LightColorMode.RGBW)
|
||||
) {
|
||||
const maxVal = Math.max(...stateObj.attributes.rgb_color);
|
||||
|
||||
if (maxVal < 255) {
|
||||
this._brightnessAdjusted = maxVal;
|
||||
}
|
||||
}
|
||||
this._updateBrightnessAdjusted();
|
||||
|
||||
this._wvSliderValue =
|
||||
stateObj.attributes.color_mode === LightColorMode.RGBW &&
|
||||
@@ -235,11 +244,23 @@ class LightRgbColorPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (this._isInteracting || !changedProps.has("stateObj")) {
|
||||
if (!changedProps.has("stateObj")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateSliderValues();
|
||||
const oldStateObj = changedProps.get("stateObj") as LightEntity | undefined;
|
||||
if (oldStateObj?.entity_id !== this.stateObj?.entity_id) {
|
||||
this._throttleUpdateColor.cancel();
|
||||
this._pendingSync.reset();
|
||||
this._isInteracting = false;
|
||||
this._updateSliderValues();
|
||||
} else if (!this._isInteracting && this._pendingSync.shouldSync()) {
|
||||
this._updateSliderValues();
|
||||
} else if (this.stateObj.state === "on") {
|
||||
// Keep the picker values, but the brightness normalization has to
|
||||
// follow the reported state, as it is applied to the next color sent.
|
||||
this._updateBrightnessAdjusted();
|
||||
}
|
||||
}
|
||||
|
||||
private _hsColorCursorMoved(ev: CustomEvent) {
|
||||
@@ -250,6 +271,7 @@ class LightRgbColorPicker extends LitElement {
|
||||
return;
|
||||
}
|
||||
this._hsPickerValue = color;
|
||||
this._pendingSync.schedule();
|
||||
|
||||
this._throttleUpdateColor();
|
||||
}
|
||||
@@ -312,13 +334,23 @@ class LightRgbColorPicker extends LitElement {
|
||||
this._colorBrightnessSliderValue = hsv[2] / 2.55;
|
||||
}
|
||||
|
||||
this._pendingSync.schedule();
|
||||
this._throttleUpdateColor();
|
||||
}
|
||||
|
||||
private _hsColorChanged(ev: CustomEvent) {
|
||||
this._isInteracting = false;
|
||||
if (!ev.detail.value) {
|
||||
// Nothing is sent, so stop blocking state updates.
|
||||
this._throttleUpdateColor.cancel();
|
||||
this._pendingSync.reset();
|
||||
this._updateSliderValues();
|
||||
return;
|
||||
}
|
||||
|
||||
// A pending throttled call would otherwise send an older color after the
|
||||
// final one.
|
||||
this._throttleUpdateColor.cancel();
|
||||
this._hsPickerValue = ev.detail.value;
|
||||
|
||||
this._updateColor();
|
||||
@@ -366,11 +398,15 @@ class LightRgbColorPicker extends LitElement {
|
||||
|
||||
private _applyColor(color: LightColor, params?: Record<string, any>) {
|
||||
fireEvent(this, "color-changed", color);
|
||||
this._api.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
...color,
|
||||
...params,
|
||||
});
|
||||
return this._pendingSync.run(
|
||||
() =>
|
||||
this._api.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
...color,
|
||||
...params,
|
||||
}),
|
||||
() => this._updateSliderValues()
|
||||
);
|
||||
}
|
||||
|
||||
private _colorBrightnessSliderChanged(ev: CustomEvent) {
|
||||
|
||||
@@ -13,6 +13,7 @@ import {
|
||||
} from "../../../../common/color/convert-light-color";
|
||||
import { fireEvent } from "../../../../common/dom/fire_event";
|
||||
import { stateColorCss } from "../../../../common/entity/state_color";
|
||||
import { PendingValueSync } from "../../../../common/util/pending-value-sync";
|
||||
import { throttle } from "../../../../common/util/throttle";
|
||||
import "../../../../components/ha-control-slider";
|
||||
import {
|
||||
@@ -66,6 +67,8 @@ class LightColorTempPicker extends LitElement {
|
||||
|
||||
@state() private _isInteracting?: boolean;
|
||||
|
||||
private _pendingSync = new PendingValueSync();
|
||||
|
||||
protected render() {
|
||||
if (!this.stateObj) {
|
||||
return nothing;
|
||||
@@ -125,11 +128,19 @@ class LightColorTempPicker extends LitElement {
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
|
||||
if (this._isInteracting || !changedProps.has("stateObj")) {
|
||||
if (!changedProps.has("stateObj")) {
|
||||
return;
|
||||
}
|
||||
|
||||
this._updateSliderValues();
|
||||
const oldStateObj = changedProps.get("stateObj");
|
||||
if (oldStateObj?.entity_id !== this.stateObj?.entity_id) {
|
||||
this._throttleUpdateColorTemp.cancel();
|
||||
this._pendingSync.reset();
|
||||
this._isInteracting = false;
|
||||
this._updateSliderValues();
|
||||
} else if (!this._isInteracting && this._pendingSync.shouldSync()) {
|
||||
this._updateSliderValues();
|
||||
}
|
||||
}
|
||||
|
||||
private _ctColorCursorMoved(ev: CustomEvent) {
|
||||
@@ -142,6 +153,7 @@ class LightColorTempPicker extends LitElement {
|
||||
}
|
||||
|
||||
this._ctPickerValue = ct;
|
||||
this._pendingSync.schedule();
|
||||
|
||||
this._throttleUpdateColorTemp();
|
||||
}
|
||||
@@ -153,7 +165,16 @@ class LightColorTempPicker extends LitElement {
|
||||
private _ctColorChanged(ev: CustomEvent) {
|
||||
const ct = ev.detail.value;
|
||||
|
||||
if (isNaN(ct) || this._ctPickerValue === ct) {
|
||||
this._isInteracting = false;
|
||||
|
||||
// A pending throttled call would otherwise send an older value after the
|
||||
// final one.
|
||||
this._throttleUpdateColorTemp.cancel();
|
||||
|
||||
if (isNaN(ct)) {
|
||||
// Nothing is sent, so stop blocking state updates.
|
||||
this._pendingSync.reset();
|
||||
this._updateSliderValues();
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -170,11 +191,15 @@ class LightColorTempPicker extends LitElement {
|
||||
|
||||
private _applyColor(color: LightColor, params?: Record<string, any>) {
|
||||
fireEvent(this, "color-changed", color);
|
||||
this._api.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
...color,
|
||||
...params,
|
||||
});
|
||||
return this._pendingSync.run(
|
||||
() =>
|
||||
this._api.callService("light", "turn_on", {
|
||||
entity_id: this.stateObj!.entity_id,
|
||||
...color,
|
||||
...params,
|
||||
}),
|
||||
() => this._updateSliderValues()
|
||||
);
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
|
||||
@@ -112,7 +112,7 @@ export class HaMoreInfoTimerPresets extends LitElement {
|
||||
};
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this._api,
|
||||
this._api.callWS,
|
||||
this.entry.entity_id,
|
||||
{
|
||||
options_domain: "timer",
|
||||
|
||||
@@ -82,7 +82,7 @@ export class HaMoreInfoViewVacuumSegmentMapping extends LitElement {
|
||||
};
|
||||
|
||||
try {
|
||||
await updateEntityRegistryEntry(this.hass, this.params.entityId, {
|
||||
await updateEntityRegistryEntry(this.hass.callWS, this.params.entityId, {
|
||||
options_domain: "vacuum",
|
||||
options: options,
|
||||
});
|
||||
|
||||
@@ -110,7 +110,7 @@ export class HaMoreInfoValveFavoritePositions extends LitElement {
|
||||
currentOptions.favorite_positions = this._favoritePositions;
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this._api,
|
||||
this._api.callWS,
|
||||
this.entry.entity_id,
|
||||
{
|
||||
options_domain: "valve",
|
||||
|
||||
@@ -132,7 +132,7 @@ const copyFavoriteOptionsToEntities = async (
|
||||
if (selected?.entity) {
|
||||
const result = await Promise.allSettled(
|
||||
selected.entity.map((entityId: string) =>
|
||||
updateEntityRegistryEntry(hass, entityId, {
|
||||
updateEntityRegistryEntry(hass.callWS, entityId, {
|
||||
options_domain: domain,
|
||||
options,
|
||||
})
|
||||
|
||||
@@ -531,7 +531,7 @@ export class MoreInfoDialog extends DirtyStateProviderMixin<
|
||||
}
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this.hass.callWS,
|
||||
favoritesContext.entry.entity_id,
|
||||
{
|
||||
options_domain: favoritesHandler.domain,
|
||||
|
||||
@@ -50,7 +50,7 @@ export class HaVoiceAssistantSetupStepArea extends LitElement {
|
||||
});
|
||||
return;
|
||||
}
|
||||
await updateDeviceRegistryEntry(this.hass, this.deviceId, {
|
||||
await updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
|
||||
area_id: area,
|
||||
});
|
||||
this._nextStep();
|
||||
|
||||
@@ -351,7 +351,7 @@ export class HaVoiceAssistantSetupStepSuccess extends LitElement {
|
||||
private async _done() {
|
||||
if (this._deviceName) {
|
||||
try {
|
||||
updateDeviceRegistryEntry(this.hass, this.deviceId, {
|
||||
updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
|
||||
name_by_user: this._deviceName,
|
||||
});
|
||||
} catch (error: any) {
|
||||
|
||||
@@ -1,28 +1,18 @@
|
||||
import { mdiPencil } from "@mdi/js";
|
||||
import type { CSSResultGroup, PropertyValues } from "lit";
|
||||
import { LitElement, css, html, nothing } from "lit";
|
||||
import { customElement, property, state } from "lit/decorators";
|
||||
import type { HASSDomEvent } from "../../common/dom/fire_event";
|
||||
import { debounce } from "../../common/util/debounce";
|
||||
import { deepEqual } from "../../common/util/deep-equal";
|
||||
import "../../components/ha-button";
|
||||
import "../../components/ha-icon-button";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
import {
|
||||
fetchFrontendSystemDataAfterSaves,
|
||||
queueFrontendSystemDataSave,
|
||||
type ClimateFrontendSystemData,
|
||||
} from "../../data/frontend";
|
||||
import type { LovelaceStrategyViewConfig } from "../../data/lovelace/config/view";
|
||||
import { ChildPanelReady } from "../../layouts/panel-ready";
|
||||
import { haStyle } from "../../resources/styles";
|
||||
import type { HomeAssistant } from "../../types";
|
||||
import { showToast } from "../../util/toast";
|
||||
import { generateLovelaceViewStrategy } from "../lovelace/strategies/get-strategy";
|
||||
import type { Lovelace } from "../lovelace/types";
|
||||
import "../lovelace/views/hui-view";
|
||||
import "../lovelace/views/hui-view-background";
|
||||
import "../lovelace/views/hui-view-container";
|
||||
import "../../components/ha-top-app-bar-fixed";
|
||||
|
||||
const CLIMATE_LOVELACE_VIEW_CONFIG: LovelaceStrategyViewConfig = {
|
||||
strategy: {
|
||||
@@ -40,20 +30,10 @@ class PanelClimate extends LitElement {
|
||||
|
||||
@state() private _lovelace?: Lovelace;
|
||||
|
||||
@state() private _config?: ClimateFrontendSystemData;
|
||||
|
||||
@state() private _editVisibility = false;
|
||||
|
||||
@state() private _searchParams = new URLSearchParams(window.location.search);
|
||||
|
||||
private _childPanelReady?: ChildPanelReady;
|
||||
|
||||
private _loadConfigPromise?: Promise<void>;
|
||||
|
||||
private _loadConfigRevision = 0;
|
||||
|
||||
private _setLovelaceRevision = 0;
|
||||
|
||||
public willUpdate(changedProps: PropertyValues<this>) {
|
||||
super.willUpdate(changedProps);
|
||||
// Initial setup
|
||||
@@ -67,22 +47,13 @@ class PanelClimate extends LitElement {
|
||||
}
|
||||
|
||||
const oldHass = changedProps.get("hass") as this["hass"];
|
||||
if (
|
||||
oldHass &&
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setup();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && oldHass.localize !== this.hass.localize) {
|
||||
this._setLovelace();
|
||||
return;
|
||||
}
|
||||
|
||||
if (oldHass && this.hass) {
|
||||
// Refresh the generated view when registries or panels change.
|
||||
// If the entity registry changed, ask the user if they want to refresh the config
|
||||
if (
|
||||
oldHass.entities !== this.hass.entities ||
|
||||
oldHass.devices !== this.hass.devices ||
|
||||
@@ -92,46 +63,24 @@ class PanelClimate extends LitElement {
|
||||
) {
|
||||
if (this.hass.config.state === "RUNNING") {
|
||||
this._debounceRegistriesChanged();
|
||||
return;
|
||||
}
|
||||
}
|
||||
// If ha started, refresh the config
|
||||
if (
|
||||
this.hass.config.state === "RUNNING" &&
|
||||
oldHass.config.state !== "RUNNING"
|
||||
) {
|
||||
this._setLovelace();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async _setup() {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
await Promise.all([
|
||||
this.hass.loadFragmentTranslation("lovelace"),
|
||||
this._loadConfigPromise,
|
||||
]);
|
||||
await this.hass.loadFragmentTranslation("lovelace");
|
||||
this._setLovelace();
|
||||
}
|
||||
|
||||
private async _loadConfig() {
|
||||
const revision = ++this._loadConfigRevision;
|
||||
this._config = undefined;
|
||||
try {
|
||||
const data = await fetchFrontendSystemDataAfterSaves(
|
||||
this.hass.connection,
|
||||
"climate"
|
||||
);
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
this._config = data || {};
|
||||
} catch (err) {
|
||||
if (revision !== this._loadConfigRevision) {
|
||||
return;
|
||||
}
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to load climate configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.climate.editor.load_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private _debounceRegistriesChanged = debounce(
|
||||
() => this._registriesChanged(),
|
||||
200
|
||||
@@ -148,27 +97,6 @@ class PanelClimate extends LitElement {
|
||||
.backButton=${this._searchParams.has("historyBack")}
|
||||
>
|
||||
<div slot="title">${this.hass.localize("panel.climate")}</div>
|
||||
${
|
||||
!this.hass.user?.is_admin
|
||||
? nothing
|
||||
: this._editVisibility
|
||||
? html`<ha-button
|
||||
slot="actionItems"
|
||||
appearance="filled"
|
||||
size="s"
|
||||
@click=${this._stopEditVisibility}
|
||||
>
|
||||
${this.hass.localize("ui.panel.lovelace.menu.exit_edit_mode")}
|
||||
</ha-button>`
|
||||
: this._config
|
||||
? html`<ha-icon-button
|
||||
slot="actionItems"
|
||||
.path=${mdiPencil}
|
||||
.label=${this.hass.localize("ui.panel.climate.editor.title")}
|
||||
@click=${this._startEditVisibility}
|
||||
></ha-icon-button>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
this._lovelace
|
||||
? html`
|
||||
@@ -180,7 +108,6 @@ class PanelClimate extends LitElement {
|
||||
.narrow=${this.narrow}
|
||||
.lovelace=${this._lovelace}
|
||||
.index=${this._viewIndex}
|
||||
@entity-visibility-changed=${this._visibilityChanged}
|
||||
></hui-view
|
||||
></hui-view-container>
|
||||
`
|
||||
@@ -190,31 +117,16 @@ class PanelClimate extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private async _setLovelace(force = false) {
|
||||
const revision = ++this._setLovelaceRevision;
|
||||
if (this._loadConfigPromise) {
|
||||
await this._loadConfigPromise;
|
||||
}
|
||||
|
||||
private async _setLovelace() {
|
||||
const viewConfig = await generateLovelaceViewStrategy(
|
||||
{
|
||||
strategy: {
|
||||
...CLIMATE_LOVELACE_VIEW_CONFIG.strategy,
|
||||
hidden_entities: this._config?.hidden_entities,
|
||||
edit_visibility: this._editVisibility || undefined,
|
||||
},
|
||||
},
|
||||
CLIMATE_LOVELACE_VIEW_CONFIG,
|
||||
this.hass
|
||||
);
|
||||
|
||||
if (revision !== this._setLovelaceRevision) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = { views: [viewConfig] };
|
||||
const rawConfig = { views: [CLIMATE_LOVELACE_VIEW_CONFIG] };
|
||||
|
||||
if (!force && deepEqual(config, this._lovelace?.config)) {
|
||||
if (deepEqual(config, this._lovelace?.config)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -234,60 +146,6 @@ class PanelClimate extends LitElement {
|
||||
};
|
||||
}
|
||||
|
||||
private _startEditVisibility = () => {
|
||||
this._editVisibility = true;
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _stopEditVisibility = () => {
|
||||
this._editVisibility = false;
|
||||
if (!this._config) {
|
||||
this._loadConfigPromise = this._loadConfig();
|
||||
}
|
||||
this._setLovelace();
|
||||
};
|
||||
|
||||
private _visibilityChanged = (
|
||||
ev: HASSDomEvent<HASSDomEvents["entity-visibility-changed"]>
|
||||
) => {
|
||||
if (!this._config) {
|
||||
return;
|
||||
}
|
||||
const { entity, hidden } = ev.detail;
|
||||
const hiddenEntities = (this._config.hidden_entities ?? []).filter(
|
||||
(entityId) => entityId !== entity
|
||||
);
|
||||
if (hidden) {
|
||||
hiddenEntities.push(entity);
|
||||
}
|
||||
this._saveConfig({
|
||||
...this._config,
|
||||
hidden_entities: hiddenEntities.length ? hiddenEntities : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
private _saveConfig(config: ClimateFrontendSystemData): Promise<void> {
|
||||
this._config = config;
|
||||
return queueFrontendSystemDataSave(
|
||||
this.hass.connection,
|
||||
"climate",
|
||||
config
|
||||
).catch(async (err) => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error("Failed to save climate configuration:", err);
|
||||
showToast(this, {
|
||||
message: this.hass.localize("ui.panel.climate.editor.save_failed"),
|
||||
duration: 0,
|
||||
dismissable: true,
|
||||
});
|
||||
if (config !== this._config) {
|
||||
return;
|
||||
}
|
||||
await this._loadConfig();
|
||||
await this._setLovelace(true);
|
||||
});
|
||||
}
|
||||
|
||||
static get styles(): CSSResultGroup {
|
||||
return [
|
||||
haStyle,
|
||||
|
||||
@@ -2,10 +2,9 @@ import { ReactiveElement } from "lit";
|
||||
import { customElement } from "lit/decorators";
|
||||
import { getAreasFloorHierarchy } from "../../../common/areas/areas-floor-hierarchy";
|
||||
import {
|
||||
findFilteredEntities,
|
||||
findEntities,
|
||||
generateEntityFilter,
|
||||
type EntityFilter,
|
||||
type EntityFilterSources,
|
||||
} from "../../../common/entity/entity_filter";
|
||||
import { floorDefaultIcon } from "../../../components/ha-floor-icon";
|
||||
import type { LovelaceCardConfig } from "../../../data/lovelace/config/card";
|
||||
@@ -13,15 +12,9 @@ import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/sec
|
||||
import type { LovelaceViewConfig } from "../../../data/lovelace/config/view";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import { computeAreaTileCardConfig } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper";
|
||||
import {
|
||||
wrapEntityVisibility,
|
||||
type ComputeEntityCard,
|
||||
} from "../../lovelace/strategies/helpers/entity-visibility";
|
||||
|
||||
export interface ClimateViewStrategyConfig {
|
||||
type: "climate";
|
||||
hidden_entities?: string[];
|
||||
edit_visibility?: boolean;
|
||||
}
|
||||
|
||||
export const climateEntityFilters: EntityFilter[] = [
|
||||
@@ -49,53 +42,40 @@ export const climateEntityFilters: EntityFilter[] = [
|
||||
},
|
||||
];
|
||||
|
||||
export const findClimateEntities = (
|
||||
sources: EntityFilterSources,
|
||||
hiddenEntities?: string[]
|
||||
): string[] =>
|
||||
findFilteredEntities(sources, climateEntityFilters, hiddenEntities);
|
||||
export const hasClimateEntities = (hass: HomeAssistant): boolean => {
|
||||
const hasAreaSensor = Object.values(hass.areas).some(
|
||||
(area) =>
|
||||
(area.temperature_entity_id && hass.states[area.temperature_entity_id]) ||
|
||||
(area.humidity_entity_id && hass.states[area.humidity_entity_id])
|
||||
);
|
||||
|
||||
type ClimateAreaSensorKind = "temperature" | "humidity";
|
||||
|
||||
export const findClimateAreaSensors = (
|
||||
sources: EntityFilterSources,
|
||||
hiddenEntities?: string[]
|
||||
): Map<string, ClimateAreaSensorKind> => {
|
||||
const hidden = new Set(hiddenEntities);
|
||||
const sensors = new Map<string, ClimateAreaSensorKind>();
|
||||
for (const area of Object.values(sources.areas)) {
|
||||
const slots = [
|
||||
[area.temperature_entity_id, "temperature"],
|
||||
[area.humidity_entity_id, "humidity"],
|
||||
] as const;
|
||||
for (const [entityId, kind] of slots) {
|
||||
if (entityId && entityId in sources.states && !hidden.has(entityId)) {
|
||||
sensors.set(entityId, kind);
|
||||
}
|
||||
}
|
||||
if (hasAreaSensor) {
|
||||
return true;
|
||||
}
|
||||
return sensors;
|
||||
|
||||
const entityIds = Object.keys(hass.states);
|
||||
|
||||
return climateEntityFilters.some((filter) =>
|
||||
entityIds.some(
|
||||
generateEntityFilter(
|
||||
hass.states,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors,
|
||||
filter
|
||||
)
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const hasClimateEntities = (
|
||||
hass: HomeAssistant,
|
||||
hiddenEntities?: string[]
|
||||
): boolean =>
|
||||
findClimateAreaSensors(hass, hiddenEntities).size > 0 ||
|
||||
findClimateEntities(hass, hiddenEntities).length > 0;
|
||||
|
||||
interface ClimateCardOptions {
|
||||
entities: string[];
|
||||
areaSensors: Map<string, ClimateAreaSensorKind>;
|
||||
computeCard: ComputeEntityCard;
|
||||
}
|
||||
|
||||
const processAreasForClimate = (
|
||||
areaIds: string[],
|
||||
hass: HomeAssistant,
|
||||
{ entities, areaSensors, computeCard }: ClimateCardOptions
|
||||
entities: string[]
|
||||
): LovelaceCardConfig[] => {
|
||||
const cards: LovelaceCardConfig[] = [];
|
||||
const computeTileCard = computeAreaTileCardConfig(hass, true);
|
||||
|
||||
for (const areaId of areaIds) {
|
||||
const area = hass.areas[areaId];
|
||||
@@ -116,13 +96,25 @@ const processAreasForClimate = (
|
||||
|
||||
// Add temperature and humidity sensors with trend graphs for areas
|
||||
const temperatureEntityId = area.temperature_entity_id;
|
||||
if (temperatureEntityId && areaSensors.has(temperatureEntityId)) {
|
||||
areaCards.push(computeCard(temperatureEntityId));
|
||||
if (temperatureEntityId && hass.states[temperatureEntityId]) {
|
||||
areaCards.push({
|
||||
...computeTileCard(temperatureEntityId),
|
||||
name:
|
||||
hass.localize("component.sensor.entity_component.temperature.name") ||
|
||||
"Temperature",
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
}
|
||||
|
||||
const humidityEntityId = area.humidity_entity_id;
|
||||
if (humidityEntityId && areaSensors.has(humidityEntityId)) {
|
||||
areaCards.push(computeCard(humidityEntityId));
|
||||
if (humidityEntityId && hass.states[humidityEntityId]) {
|
||||
areaCards.push({
|
||||
...computeTileCard(humidityEntityId),
|
||||
name:
|
||||
hass.localize("component.sensor.entity_component.humidity.name") ||
|
||||
"Humidity",
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
}
|
||||
|
||||
// Add other climate entities
|
||||
@@ -132,7 +124,18 @@ const processAreasForClimate = (
|
||||
continue;
|
||||
}
|
||||
|
||||
areaCards.push(computeCard(entityId));
|
||||
const state = hass.states[entityId];
|
||||
if (
|
||||
state?.attributes.device_class === "temperature" ||
|
||||
state?.attributes.device_class === "humidity"
|
||||
) {
|
||||
areaCards.push({
|
||||
...computeTileCard(entityId),
|
||||
features: [{ type: "trend-graph" }],
|
||||
});
|
||||
} else {
|
||||
areaCards.push(computeTileCard(entityId));
|
||||
}
|
||||
}
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
@@ -156,7 +159,7 @@ const processAreasForClimate = (
|
||||
|
||||
const processUnassignedEntities = (
|
||||
hass: HomeAssistant,
|
||||
{ entities, computeCard }: ClimateCardOptions
|
||||
entities: string[]
|
||||
): LovelaceCardConfig[] => {
|
||||
const unassignedFilter = generateEntityFilter(
|
||||
hass.states,
|
||||
@@ -170,9 +173,10 @@ const processUnassignedEntities = (
|
||||
);
|
||||
const unassignedEntities = entities.filter(unassignedFilter);
|
||||
const areaCards: LovelaceCardConfig[] = [];
|
||||
const computeTileCard = computeAreaTileCardConfig(hass, true);
|
||||
|
||||
for (const entityId of unassignedEntities) {
|
||||
areaCards.push(computeCard(entityId));
|
||||
areaCards.push(computeTileCard(entityId));
|
||||
}
|
||||
|
||||
return areaCards;
|
||||
@@ -181,7 +185,7 @@ const processUnassignedEntities = (
|
||||
@customElement("climate-view-strategy")
|
||||
export class ClimateViewStrategy extends ReactiveElement {
|
||||
static async generate(
|
||||
config: ClimateViewStrategyConfig,
|
||||
_config: ClimateViewStrategyConfig,
|
||||
hass: HomeAssistant
|
||||
): Promise<LovelaceViewConfig> {
|
||||
const areas = Object.values(hass.areas);
|
||||
@@ -190,40 +194,20 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
|
||||
const sections: LovelaceSectionRawConfig[] = [];
|
||||
|
||||
const hiddenEntities = new Set(config.hidden_entities);
|
||||
const editVisibility = Boolean(config.edit_visibility);
|
||||
const entities = findClimateEntities(
|
||||
hass,
|
||||
editVisibility ? undefined : config.hidden_entities
|
||||
const allEntities = Object.keys(hass.states);
|
||||
|
||||
const climateFilters = climateEntityFilters.map((filter) =>
|
||||
generateEntityFilter(
|
||||
hass.states,
|
||||
hass.entities,
|
||||
hass.devices,
|
||||
hass.areas,
|
||||
hass.floors,
|
||||
filter
|
||||
)
|
||||
);
|
||||
|
||||
const areaSensors = findClimateAreaSensors(
|
||||
hass,
|
||||
editVisibility ? undefined : config.hidden_entities
|
||||
);
|
||||
const computeTileCard = computeAreaTileCardConfig(hass, true);
|
||||
const computeClimateCard = (entityId: string): LovelaceCardConfig => {
|
||||
const tileCard = computeTileCard(entityId);
|
||||
const areaSensor = areaSensors.get(entityId);
|
||||
if (!areaSensor) {
|
||||
return tileCard;
|
||||
}
|
||||
return {
|
||||
...tileCard,
|
||||
name:
|
||||
hass.localize(
|
||||
`component.sensor.entity_component.${areaSensor}.name`
|
||||
) || undefined,
|
||||
features: [{ type: "trend-graph" }],
|
||||
};
|
||||
};
|
||||
const cardOptions: ClimateCardOptions = {
|
||||
entities,
|
||||
areaSensors,
|
||||
computeCard: editVisibility
|
||||
? wrapEntityVisibility(hiddenEntities, computeClimateCard)
|
||||
: computeClimateCard,
|
||||
};
|
||||
const entities = findEntities(allEntities, climateFilters);
|
||||
|
||||
const floorCount =
|
||||
hierarchy.floors.length + (hierarchy.areas.length ? 1 : 0);
|
||||
@@ -249,7 +233,7 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForClimate(areaIds, hass, cardOptions);
|
||||
const areaCards = processAreasForClimate(areaIds, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
@@ -273,11 +257,7 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
],
|
||||
};
|
||||
|
||||
const areaCards = processAreasForClimate(
|
||||
hierarchy.areas,
|
||||
hass,
|
||||
cardOptions
|
||||
);
|
||||
const areaCards = processAreasForClimate(hierarchy.areas, hass, entities);
|
||||
|
||||
if (areaCards.length > 0) {
|
||||
section.cards!.push(...areaCards);
|
||||
@@ -286,7 +266,7 @@ export class ClimateViewStrategy extends ReactiveElement {
|
||||
}
|
||||
|
||||
// Process unassigned entities
|
||||
const unassignedCards = processUnassignedEntities(hass, cardOptions);
|
||||
const unassignedCards = processUnassignedEntities(hass, entities);
|
||||
|
||||
if (unassignedCards.length > 0) {
|
||||
const section: LovelaceSectionRawConfig = {
|
||||
|
||||
@@ -857,6 +857,8 @@ class HaConfigAreaPage extends LitElement {
|
||||
},
|
||||
this.hass.auth.data.hassUrl
|
||||
)}
|
||||
width="24"
|
||||
height="24"
|
||||
@error=${this._onImageError}
|
||||
@load=${this._onImageLoad}
|
||||
/>`;
|
||||
@@ -1061,7 +1063,7 @@ class HaConfigAreaPage extends LitElement {
|
||||
justify-content: space-around;
|
||||
}
|
||||
|
||||
img {
|
||||
.img-container img {
|
||||
border-radius: var(
|
||||
--ha-card-border-radius,
|
||||
var(--ha-border-radius-lg)
|
||||
@@ -1115,11 +1117,6 @@ class HaConfigAreaPage extends LitElement {
|
||||
ha-list-item-button ha-icon-next {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
img[slot="start"] {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
object-fit: contain;
|
||||
}
|
||||
ha-card > *:first-child {
|
||||
margin-top: -16px;
|
||||
}
|
||||
|
||||
@@ -128,6 +128,8 @@ import "./types/ha-automation-action-wait_for_trigger";
|
||||
import "./types/ha-automation-action-wait_template";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
import { scrollRowIntoView } from "../scroll-row-into-view";
|
||||
import { AutomationPathController } from "../automation-path";
|
||||
import { renderRowErrorIcon } from "../automation-row-error-icon";
|
||||
|
||||
export interface ActionElement extends LitElement {
|
||||
action: Action;
|
||||
@@ -182,6 +184,8 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public root = false;
|
||||
|
||||
@property({ attribute: false }) public index!: number;
|
||||
|
||||
@property({ type: Boolean }) public first?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
@@ -194,6 +198,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
@property({ type: Boolean, attribute: "sort-selected" })
|
||||
public sortSelected = false;
|
||||
|
||||
protected readonly pathController = new AutomationPathController(this, {
|
||||
segment: () => [this.index],
|
||||
row: () => (this.optionsInSidebar ? this : undefined),
|
||||
});
|
||||
|
||||
@storage({
|
||||
key: "automationClipboard",
|
||||
state: false,
|
||||
@@ -490,6 +499,11 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
${this._runResult?.title}
|
||||
</ha-automation-row-event-chip>
|
||||
|
||||
${renderRowErrorIcon(
|
||||
this.pathController.errors,
|
||||
this.pathController.path,
|
||||
this.hass.localize("ui.panel.config.automation.editor.errors.label")
|
||||
)}
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-dropdown
|
||||
@@ -790,6 +804,7 @@ export default class HaAutomationActionRow extends LitElement {
|
||||
...ACTION_COMBINED_BLOCKS,
|
||||
].includes(blockType!)}
|
||||
.sortSelected=${this.sortSelected}
|
||||
.error=${this.pathController.errors.length > 0}
|
||||
.dim=${this._running}
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
|
||||
@@ -43,6 +43,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
|
||||
|
||||
return html`
|
||||
<ha-automation-option
|
||||
path-key="choose"
|
||||
.options=${options}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._optionsChanged}
|
||||
|
||||
@@ -27,9 +27,18 @@ import {
|
||||
} from "../../../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import {
|
||||
deviceAutomationErrorText,
|
||||
deviceErrorText,
|
||||
} from "../../automation-errors";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
|
||||
@customElement("ha-automation-action-device_id")
|
||||
export class HaDeviceAction extends LitElement {
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["device_id", "domain"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Boolean }) public disabled = false;
|
||||
@@ -131,12 +140,20 @@ export class HaDeviceAction extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const deviceError = this._fieldErrors.fieldError("device_id");
|
||||
const automationError = this._fieldErrors.fieldError("domain");
|
||||
const deviceId = this._deviceId || this.action.device_id;
|
||||
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
.invalid=${!!deviceError}
|
||||
.errorMessage=${
|
||||
deviceError
|
||||
? deviceErrorText(this.hass.localize, deviceError)
|
||||
: undefined
|
||||
}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._devicePicked}
|
||||
label=${this.hass.localize(
|
||||
@@ -144,6 +161,16 @@ export class HaDeviceAction extends LitElement {
|
||||
)}
|
||||
></ha-device-picker>
|
||||
<ha-device-action-picker
|
||||
.invalid=${!!automationError}
|
||||
.errorMessage=${
|
||||
automationError
|
||||
? deviceAutomationErrorText(
|
||||
this.hass.localize,
|
||||
"actions",
|
||||
automationError
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.value=${this.action}
|
||||
.deviceId=${deviceId}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -46,6 +46,7 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-condition
|
||||
path-key="if"
|
||||
.conditions=${action.if ?? []}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._ifChanged}
|
||||
@@ -60,6 +61,7 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-action
|
||||
path-key="then"
|
||||
.actions=${action.then ?? []}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._thenChanged}
|
||||
@@ -73,6 +75,7 @@ export class HaIfAction extends LitElement implements ActionElement {
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-action
|
||||
path-key="else"
|
||||
.actions=${action.else || []}
|
||||
.disabled=${this.disabled}
|
||||
@value-changed=${this._elseChanged}
|
||||
|
||||
@@ -35,6 +35,7 @@ export class HaParallelAction extends LitElement implements ActionElement {
|
||||
|
||||
return html`
|
||||
<ha-automation-action
|
||||
path-key="parallel"
|
||||
.actions=${action.parallel}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -8,6 +8,7 @@ import { haStyle } from "../../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../ha-automation-action";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
import { AutomationPathController } from "../../automation-path";
|
||||
|
||||
import { isTemplate } from "../../../../../common/string/has-template";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
@@ -40,6 +41,11 @@ export class HaRepeatAction extends LitElement implements ActionElement {
|
||||
|
||||
@property({ type: Boolean, attribute: "indent" }) public indent = false;
|
||||
|
||||
// The form edits the repeat options, so its fields are keyed below "repeat"
|
||||
protected readonly pathController = new AutomationPathController(this, {
|
||||
segment: () => ["repeat"],
|
||||
});
|
||||
|
||||
@query("ha-form")
|
||||
private _formElement?: HaForm;
|
||||
|
||||
|
||||
@@ -35,6 +35,7 @@ export class HaSequenceAction extends LitElement implements ActionElement {
|
||||
|
||||
return html`
|
||||
<ha-automation-action
|
||||
path-key="sequence"
|
||||
.actions=${action.sequence ?? []}
|
||||
.narrow=${this.narrow}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -12,6 +12,8 @@ import type { ServiceAction } from "../../../../../data/script";
|
||||
import { serviceActionStruct } from "../../../../../data/script";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { targetErrorText } from "../../automation-errors";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
import type { ActionElement } from "../ha-automation-action-row";
|
||||
|
||||
@customElement("ha-automation-action-service")
|
||||
@@ -28,6 +30,10 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
|
||||
@state() private _responseChecked = false;
|
||||
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["target", "data"],
|
||||
});
|
||||
|
||||
public static get defaultConfig(): ServiceAction {
|
||||
return { action: "", data: {} };
|
||||
}
|
||||
@@ -80,6 +86,8 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
? this._action.action.split(".", 2)
|
||||
: [undefined, undefined];
|
||||
|
||||
const targetError = this._fieldErrors.fieldError("target");
|
||||
|
||||
const optionalResponse =
|
||||
domain && service
|
||||
? !!this.hass.services[domain]?.[service]?.response?.optional
|
||||
@@ -92,6 +100,13 @@ export class HaServiceAction extends LitElement implements ActionElement {
|
||||
.value=${this._action}
|
||||
.disabled=${this.disabled}
|
||||
.hidePicker=${!!this._action.metadata}
|
||||
.targetError=${
|
||||
targetError
|
||||
? targetErrorText(this.hass.localize, targetError)
|
||||
: undefined
|
||||
}
|
||||
.dataErrors=${this._fieldErrors.nestedFieldErrors("data")}
|
||||
.dataError=${this._fieldErrors.ownFieldError("data")?.message}
|
||||
@value-changed=${this._actionChanged}
|
||||
></ha-service-control>
|
||||
${
|
||||
|
||||
@@ -90,6 +90,7 @@ export class HaWaitForTriggerAction
|
||||
${
|
||||
this.indent || (!this.inSidebar && !this.indent)
|
||||
? html`<ha-automation-trigger
|
||||
path-key="wait_for_trigger"
|
||||
class=${!this.inSidebar && !this.indent ? "expansion-panel" : ""}
|
||||
.triggers=${ensureArray(this.action.wait_for_trigger)}
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -7,6 +7,7 @@ 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 { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
@@ -15,6 +16,10 @@ import type { TimeoutType } from "../../types";
|
||||
|
||||
@customElement("ha-automation-action-wait_template")
|
||||
export class HaWaitAction extends LitElement implements ActionElement {
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["wait_template", "timeout", "continue_on_timeout"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public action!: WaitAction;
|
||||
@@ -65,6 +70,7 @@ export class HaWaitAction extends LitElement implements ActionElement {
|
||||
: "duration";
|
||||
return html`
|
||||
<ha-form
|
||||
.error=${this._fieldErrors.fields}
|
||||
.hass=${this.hass}
|
||||
.data=${this.action}
|
||||
.schema=${this._schema(timeoutType, timeout !== undefined)}
|
||||
|
||||
@@ -0,0 +1,350 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import { ensureArray } from "../../../common/array/ensure-array";
|
||||
import type { LocalizeFunc } from "../../../common/translations/localize";
|
||||
import {
|
||||
formatPath,
|
||||
pathEquals,
|
||||
pathStartsWith,
|
||||
type ConfigPath,
|
||||
} from "../../../common/util/config-path";
|
||||
import type { ConfigValidationError } from "../../../data/config";
|
||||
import type {
|
||||
AutomationPathRow,
|
||||
AutomationRowRegistry,
|
||||
} from "./automation-path";
|
||||
|
||||
/** The validation errors of the config being edited, paths from its root. */
|
||||
export const automationErrorsContext =
|
||||
createContext<ConfigValidationError[]>("automation-errors");
|
||||
|
||||
export const NO_ERRORS: ConfigValidationError[] = [];
|
||||
|
||||
/**
|
||||
* The row an error belongs to: the deepest rendered row on its path. Rows
|
||||
* above it only contain the error, they are expanded to show it.
|
||||
*/
|
||||
export const errorOwner = (
|
||||
registry: AutomationRowRegistry,
|
||||
error: ConfigValidationError
|
||||
): AutomationPathRow | undefined => {
|
||||
const rows = registry.rowsAlong(error.path);
|
||||
return rows[rows.length - 1];
|
||||
};
|
||||
|
||||
export const ownedErrors = (
|
||||
registry: AutomationRowRegistry,
|
||||
errors: ConfigValidationError[],
|
||||
row: AutomationPathRow
|
||||
): ConfigValidationError[] => {
|
||||
const owned = errors.filter((error) => errorOwner(registry, error) === row);
|
||||
return owned.length ? owned : NO_ERRORS;
|
||||
};
|
||||
|
||||
export const fieldErrors = (
|
||||
errors: ConfigValidationError[],
|
||||
path: ConfigPath
|
||||
): Record<string, string> => {
|
||||
const fields: Record<string, string> = {};
|
||||
errors.forEach((error) => {
|
||||
const field = errorField(error, path);
|
||||
if (field !== undefined && !(field in fields)) {
|
||||
fields[field] = error.message;
|
||||
}
|
||||
});
|
||||
return fields;
|
||||
};
|
||||
|
||||
/** The field of the config at `path` an error is about, if any. */
|
||||
export const errorField = (
|
||||
error: ConfigValidationError,
|
||||
path: ConfigPath
|
||||
): string | undefined =>
|
||||
error.path.length > path.length && pathStartsWith(error.path, path)
|
||||
? String(error.path[path.length])
|
||||
: undefined;
|
||||
|
||||
export const errorText = (
|
||||
error: ConfigValidationError,
|
||||
path: ConfigPath = []
|
||||
): string => {
|
||||
const relative = pathStartsWith(error.path, path)
|
||||
? error.path.slice(path.length)
|
||||
: error.path;
|
||||
return relative.length
|
||||
? `${formatPath(relative)}: ${error.message}`
|
||||
: error.message;
|
||||
};
|
||||
|
||||
export const errorsEqual = (
|
||||
a: ConfigValidationError[],
|
||||
b: ConfigValidationError[]
|
||||
): boolean =>
|
||||
a.length === b.length &&
|
||||
a.every(
|
||||
(error, index) =>
|
||||
error.message === b[index].message &&
|
||||
error.code === b[index].code &&
|
||||
pathEquals(error.path, b[index].path)
|
||||
);
|
||||
|
||||
const TRIGGER_LIST_KEYS = ["triggers", "wait_for_trigger"];
|
||||
|
||||
const isTriggerSublist = (value: unknown): value is { triggers: unknown } =>
|
||||
typeof value === "object" &&
|
||||
value !== null &&
|
||||
Object.keys(value).length === 1 &&
|
||||
"triggers" in value;
|
||||
|
||||
// Core validates triggers after flattening `triggers:` sublists one level
|
||||
// deep, so its trigger indexes count the triggers of the flat list.
|
||||
const flatTriggerPaths = (triggers: unknown[]): ConfigPath[] =>
|
||||
triggers.flatMap((trigger, index) =>
|
||||
isTriggerSublist(trigger)
|
||||
? (ensureArray(trigger.triggers) ?? []).map((_, subIndex) => [
|
||||
index,
|
||||
"triggers",
|
||||
subIndex,
|
||||
])
|
||||
: [[index]]
|
||||
);
|
||||
|
||||
const isConfigObject = (value: unknown): value is Record<string, unknown> =>
|
||||
typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
|
||||
const childAt = (value: unknown, segment: string | number): unknown =>
|
||||
typeof value === "object" && value !== null
|
||||
? (value as Record<string | number, unknown>)[segment]
|
||||
: undefined;
|
||||
|
||||
const valueAt = (config: unknown, path: ConfigPath): unknown =>
|
||||
path.reduce<unknown>(childAt, config);
|
||||
|
||||
/** Maps an error path of core's validated config onto the edited config. */
|
||||
export const configErrorPath = (
|
||||
config: unknown,
|
||||
path: ConfigPath
|
||||
): ConfigPath => {
|
||||
const result: ConfigPath = [];
|
||||
let value = config;
|
||||
for (let index = 0; index < path.length; index++) {
|
||||
const segment = path[index];
|
||||
const parentKey = result[result.length - 1];
|
||||
// Core wraps each action of the `parallel` shorthand in a sequence
|
||||
if (
|
||||
segment === "sequence" &&
|
||||
path[index + 1] === 0 &&
|
||||
result[result.length - 2] === "parallel" &&
|
||||
isConfigObject(value) &&
|
||||
!("sequence" in value)
|
||||
) {
|
||||
index++;
|
||||
continue;
|
||||
}
|
||||
const mapped =
|
||||
typeof segment === "number" &&
|
||||
Array.isArray(value) &&
|
||||
TRIGGER_LIST_KEYS.includes(parentKey as string)
|
||||
? flatTriggerPaths(value)[segment]
|
||||
: undefined;
|
||||
(mapped ?? [segment]).forEach((part) => {
|
||||
result.push(part);
|
||||
value = childAt(value, part);
|
||||
});
|
||||
}
|
||||
return result;
|
||||
};
|
||||
|
||||
export const mapErrorsToConfig = (
|
||||
config: unknown,
|
||||
errors: ConfigValidationError[]
|
||||
): ConfigValidationError[] =>
|
||||
errors.map((error) => ({
|
||||
...error,
|
||||
path: configErrorPath(config, error.path),
|
||||
}));
|
||||
|
||||
// Rows sit at list items that are objects, not at entries of value lists
|
||||
// like `entity_id`
|
||||
const errorItemPath = (config: unknown, path: ConfigPath): ConfigPath => {
|
||||
let value = config;
|
||||
let end = 0;
|
||||
path.forEach((segment, index) => {
|
||||
value = childAt(value, segment);
|
||||
if (typeof segment === "number" && isConfigObject(value)) {
|
||||
end = index + 1;
|
||||
}
|
||||
});
|
||||
return path.slice(0, end);
|
||||
};
|
||||
|
||||
/** A config item with an error, to recognize it after edits. */
|
||||
export interface KnownErrorItem {
|
||||
path: ConfigPath;
|
||||
item?: object;
|
||||
content?: string;
|
||||
}
|
||||
|
||||
const knownItemAt = (config: unknown, path: ConfigPath): KnownErrorItem => {
|
||||
const item = valueAt(config, path);
|
||||
return isConfigObject(item)
|
||||
? { path, item, content: JSON.stringify(item) }
|
||||
: { path };
|
||||
};
|
||||
|
||||
export const knownErrorItems = (
|
||||
config: unknown,
|
||||
errors: ConfigValidationError[]
|
||||
): KnownErrorItem[] => {
|
||||
const known: KnownErrorItem[] = [];
|
||||
errors.forEach((error) => {
|
||||
const path = errorItemPath(config, error.path);
|
||||
if (!known.some((entry) => pathEquals(entry.path, path))) {
|
||||
known.push(knownItemAt(config, path));
|
||||
}
|
||||
});
|
||||
return known;
|
||||
};
|
||||
|
||||
interface ConfigItems {
|
||||
paths: Map<object, ConfigPath>;
|
||||
contents: Map<string, ConfigPath[]>;
|
||||
}
|
||||
|
||||
// The list entries that are objects, the items rows show
|
||||
const configItems = (config: unknown): ConfigItems => {
|
||||
const items: ConfigItems = { paths: new Map(), contents: new Map() };
|
||||
const walk = (value: unknown, path: ConfigPath) => {
|
||||
if (typeof value !== "object" || value === null) {
|
||||
return;
|
||||
}
|
||||
if (typeof path[path.length - 1] === "number" && isConfigObject(value)) {
|
||||
items.paths.set(value, path);
|
||||
const content = JSON.stringify(value);
|
||||
items.contents.set(content, [
|
||||
...(items.contents.get(content) ?? []),
|
||||
path,
|
||||
]);
|
||||
}
|
||||
Object.entries(value).forEach(([key, child]) => {
|
||||
walk(child, [...path, Array.isArray(value) ? Number(key) : key]);
|
||||
});
|
||||
};
|
||||
walk(config, []);
|
||||
return items;
|
||||
};
|
||||
|
||||
const followItem = (
|
||||
entry: KnownErrorItem,
|
||||
previous: ConfigItems,
|
||||
current: ConfigItems,
|
||||
config: unknown
|
||||
): KnownErrorItem | undefined => {
|
||||
// Unchanged or moved
|
||||
const path = entry.item && current.paths.get(entry.item);
|
||||
if (path) {
|
||||
return { ...entry, path };
|
||||
}
|
||||
// Moved after the YAML was parsed again
|
||||
const samePaths = entry.content
|
||||
? current.contents.get(entry.content)
|
||||
: undefined;
|
||||
if (samePaths?.length === 1) {
|
||||
return knownItemAt(config, samePaths[0]);
|
||||
}
|
||||
// Identical items, e.g. two copies of a row, keep their place
|
||||
if (samePaths?.some((other) => pathEquals(other, entry.path))) {
|
||||
return knownItemAt(config, entry.path);
|
||||
}
|
||||
const now = valueAt(config, entry.path);
|
||||
if (!isConfigObject(now)) {
|
||||
return undefined;
|
||||
}
|
||||
// An item that was already there took the place, e.g. after a delete
|
||||
const beforePaths = previous.paths.has(now)
|
||||
? [previous.paths.get(now)!]
|
||||
: (previous.contents.get(JSON.stringify(now)) ?? []);
|
||||
if (
|
||||
beforePaths.length &&
|
||||
!beforePaths.some((other) => pathEquals(other, entry.path))
|
||||
) {
|
||||
return undefined;
|
||||
}
|
||||
// Edited in place
|
||||
return knownItemAt(config, entry.path);
|
||||
};
|
||||
|
||||
/** Follows known items from `previous` to `config` through moves and edits. */
|
||||
export const followKnownErrorItems = (
|
||||
known: KnownErrorItem[],
|
||||
previous: unknown,
|
||||
config: unknown
|
||||
): KnownErrorItem[] => {
|
||||
const previousItems = configItems(previous);
|
||||
const currentItems = configItems(config);
|
||||
return known
|
||||
.map((entry) => followItem(entry, previousItems, currentItems, config))
|
||||
.filter((entry): entry is KnownErrorItem => entry !== undefined);
|
||||
};
|
||||
|
||||
/**
|
||||
* Moves errors of `previous` to where their items are in `config`. Errors of
|
||||
* items that are gone are dropped.
|
||||
*/
|
||||
export const followErrors = (
|
||||
errors: ConfigValidationError[],
|
||||
previous: unknown,
|
||||
config: unknown
|
||||
): ConfigValidationError[] => {
|
||||
const previousItems = configItems(previous);
|
||||
const currentItems = configItems(config);
|
||||
return errors.flatMap((error) => {
|
||||
const path = errorItemPath(previous, error.path);
|
||||
const moved = followItem(
|
||||
knownItemAt(previous, path),
|
||||
previousItems,
|
||||
currentItems,
|
||||
config
|
||||
);
|
||||
return moved
|
||||
? [{ ...error, path: [...moved.path, ...error.path.slice(path.length)] }]
|
||||
: [];
|
||||
});
|
||||
};
|
||||
|
||||
/** The errors on items that already had one. */
|
||||
export const errorsOnKnownItems = (
|
||||
known: KnownErrorItem[],
|
||||
config: unknown,
|
||||
errors: ConfigValidationError[]
|
||||
): ConfigValidationError[] =>
|
||||
errors.filter((error) => {
|
||||
const path = errorItemPath(config, error.path);
|
||||
return known.some((entry) => pathEquals(entry.path, path));
|
||||
});
|
||||
|
||||
export const targetErrorText = (
|
||||
localize: LocalizeFunc,
|
||||
error: ConfigValidationError
|
||||
): string =>
|
||||
localize(
|
||||
error.code === "required"
|
||||
? "ui.panel.config.automation.editor.errors.target_required"
|
||||
: "ui.panel.config.automation.editor.errors.target_invalid"
|
||||
);
|
||||
|
||||
export const deviceErrorText = (
|
||||
localize: LocalizeFunc,
|
||||
error: ConfigValidationError
|
||||
): string =>
|
||||
error.code === "required"
|
||||
? localize("ui.panel.config.automation.editor.errors.device_required")
|
||||
: error.message;
|
||||
|
||||
export const deviceAutomationErrorText = (
|
||||
localize: LocalizeFunc,
|
||||
kind: "triggers" | "conditions" | "actions",
|
||||
error: ConfigValidationError
|
||||
): string =>
|
||||
error.code === "required"
|
||||
? localize(`ui.panel.config.devices.automation.${kind}.required`)
|
||||
: error.message;
|
||||
@@ -0,0 +1,186 @@
|
||||
import { createContext } from "@lit/context";
|
||||
import {
|
||||
consumeContext,
|
||||
ContextController,
|
||||
} from "../../../common/decorators/consume";
|
||||
import { pathEquals, type ConfigPath } from "../../../common/util/config-path";
|
||||
import type { ConfigValidationError } from "../../../data/config";
|
||||
import {
|
||||
automationErrorsContext,
|
||||
errorField,
|
||||
errorsEqual,
|
||||
fieldErrors,
|
||||
NO_ERRORS,
|
||||
ownedErrors,
|
||||
} from "./automation-errors";
|
||||
import {
|
||||
automationPathContext,
|
||||
automationRowRegistryContext,
|
||||
type AutomationRowRegistry,
|
||||
} from "./automation-path";
|
||||
|
||||
const fieldsEqual = (a: Record<string, string>, b: Record<string, string>) => {
|
||||
const keys = Object.keys(a);
|
||||
return (
|
||||
keys.length === Object.keys(b).length &&
|
||||
keys.every((key) => a[key] === b[key])
|
||||
);
|
||||
};
|
||||
|
||||
/**
|
||||
* @summary The fields whose errors an editor shows next to the field, so
|
||||
* the sidebar doesn't list them again.
|
||||
*/
|
||||
export class InlineErrorFields {
|
||||
private _fields = new Map<object, string[]>();
|
||||
|
||||
constructor(private readonly _onChange: () => void) {}
|
||||
|
||||
public set(owner: object, fields: string[]) {
|
||||
this._fields.set(owner, fields);
|
||||
this._onChange();
|
||||
}
|
||||
|
||||
public delete(owner: object) {
|
||||
if (this._fields.delete(owner)) {
|
||||
this._onChange();
|
||||
}
|
||||
}
|
||||
|
||||
public has(field: string): boolean {
|
||||
return [...this._fields.values()].some((fields) => fields.includes(field));
|
||||
}
|
||||
}
|
||||
|
||||
export const automationInlineErrorFieldsContext =
|
||||
createContext<InlineErrorFields>("automation-inline-error-fields");
|
||||
|
||||
interface AutomationFieldErrorsOptions {
|
||||
/** Fields the host shows the error of, see `InlineErrorFields`. */
|
||||
inlineFields?: string[];
|
||||
}
|
||||
|
||||
/**
|
||||
* @summary Validation errors of the config at the host's path.
|
||||
*
|
||||
* Consumes the errors and the path of the closest ancestor and updates the
|
||||
* host when the errors it exposes change.
|
||||
*/
|
||||
export class AutomationFieldErrorsController extends ContextController {
|
||||
public fields: Record<string, string> = {};
|
||||
|
||||
/** Errors that belong to the row at exactly the path, see `errorOwner`. */
|
||||
public rowErrors: ConfigValidationError[] = NO_ERRORS;
|
||||
|
||||
public path: ConfigPath = [];
|
||||
|
||||
@consumeContext({ context: automationErrorsContext, subscribe: true })
|
||||
private _errors: ConfigValidationError[] = NO_ERRORS;
|
||||
|
||||
@consumeContext({ context: automationPathContext, subscribe: true })
|
||||
private _path: ConfigPath = [];
|
||||
|
||||
@consumeContext({ context: automationRowRegistryContext })
|
||||
private _registry?: AutomationRowRegistry;
|
||||
|
||||
@consumeContext({ context: automationInlineErrorFieldsContext })
|
||||
private _inlineErrorFields?: InlineErrorFields;
|
||||
|
||||
private readonly _inlineFields?: string[];
|
||||
|
||||
private _claimedIn?: InlineErrorFields;
|
||||
|
||||
constructor(
|
||||
host: ConstructorParameters<typeof ContextController>[0],
|
||||
options: AutomationFieldErrorsOptions = {}
|
||||
) {
|
||||
super(host);
|
||||
this._inlineFields = options.inlineFields;
|
||||
}
|
||||
|
||||
private _nestedFieldErrors = new Map<string, Record<string, string>>();
|
||||
|
||||
/** First error message of each key inside a field, keyed by the key. */
|
||||
public nestedFieldErrors(field: string): Record<string, string> {
|
||||
const errors = fieldErrors(this._errors, [...this._path, field]);
|
||||
const previous = this._nestedFieldErrors.get(field);
|
||||
// The same object while nothing changed, so the host's child doesn't update
|
||||
if (previous && fieldsEqual(previous, errors)) {
|
||||
return previous;
|
||||
}
|
||||
this._nestedFieldErrors.set(field, errors);
|
||||
return errors;
|
||||
}
|
||||
|
||||
/** The error of a field as a whole, not of something inside it. */
|
||||
public ownFieldError(field: string): ConfigValidationError | undefined {
|
||||
return this._errors.find((error) =>
|
||||
pathEquals(error.path, [...this._path, field])
|
||||
);
|
||||
}
|
||||
|
||||
/** The first error of a field of the config at the path. */
|
||||
public fieldError(field: string): ConfigValidationError | undefined {
|
||||
return this._errors.find(
|
||||
(error) => errorField(error, this._path) === field
|
||||
);
|
||||
}
|
||||
|
||||
private _unsubscribeRegistry?: () => void;
|
||||
|
||||
public hostConnected() {
|
||||
this._unsubscribeRegistry ??= this._registry?.subscribe(() => this._sync());
|
||||
this._claimInlineFields();
|
||||
this._sync();
|
||||
}
|
||||
|
||||
public hostDisconnected() {
|
||||
this._unsubscribeRegistry?.();
|
||||
this._unsubscribeRegistry = undefined;
|
||||
this._claimedIn?.delete(this);
|
||||
this._claimedIn = undefined;
|
||||
}
|
||||
|
||||
protected contextUpdated() {
|
||||
this._claimInlineFields();
|
||||
this._sync();
|
||||
}
|
||||
|
||||
private _claimInlineFields() {
|
||||
if (
|
||||
!this._inlineFields ||
|
||||
!this._inlineErrorFields ||
|
||||
this._claimedIn === this._inlineErrorFields
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this._claimedIn?.delete(this);
|
||||
this._inlineErrorFields.set(this, this._inlineFields);
|
||||
this._claimedIn = this._inlineErrorFields;
|
||||
}
|
||||
|
||||
private _sync() {
|
||||
const fields = this._errors.length
|
||||
? fieldErrors(this._errors, this._path)
|
||||
: {};
|
||||
const row =
|
||||
this._errors.length && this._path.length
|
||||
? this._registry?.rowAt(this._path)
|
||||
: undefined;
|
||||
const rowErrors =
|
||||
row && this._registry
|
||||
? ownedErrors(this._registry, this._errors, row)
|
||||
: NO_ERRORS;
|
||||
if (
|
||||
this.path === this._path &&
|
||||
fieldsEqual(fields, this.fields) &&
|
||||
errorsEqual(rowErrors, this.rowErrors)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
this.path = this._path;
|
||||
this.fields = fields;
|
||||
this.rowErrors = rowErrors;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
import { ContextProvider, createContext } from "@lit/context";
|
||||
import type { ReactiveElement } from "lit";
|
||||
import {
|
||||
consumeContext,
|
||||
ContextController,
|
||||
} from "../../../common/decorators/consume";
|
||||
import {
|
||||
pathEquals,
|
||||
stringifyPath,
|
||||
type ConfigPath,
|
||||
} from "../../../common/util/config-path";
|
||||
import type { ConfigValidationError } from "../../../data/config";
|
||||
import {
|
||||
automationErrorsContext,
|
||||
errorsEqual,
|
||||
NO_ERRORS,
|
||||
ownedErrors,
|
||||
} from "./automation-errors";
|
||||
|
||||
export const automationPathContext =
|
||||
createContext<ConfigPath>("automation-path");
|
||||
|
||||
export interface AutomationPathRow extends HTMLElement {
|
||||
readonly selected: boolean;
|
||||
openSidebar(): void;
|
||||
expand(): void;
|
||||
}
|
||||
|
||||
const owner = (rows?: AutomationPathRow[]) => rows?.[rows.length - 1];
|
||||
|
||||
/**
|
||||
* Rows that open in the sidebar, by path. Lets the editor find the row a
|
||||
* config path belongs to, e.g. to show it or point at an error in it.
|
||||
*/
|
||||
export class AutomationRowRegistry {
|
||||
// Rows claiming each path, the latest one owns it. While a row is dragged
|
||||
// over another list it claims the path of a row there, which takes the path
|
||||
// back once the dragged row leaves.
|
||||
private _rows = new Map<
|
||||
string,
|
||||
{ path: ConfigPath; rows: AutomationPathRow[] }
|
||||
>();
|
||||
|
||||
private _listeners = new Set<() => void>();
|
||||
|
||||
private _notifyScheduled = false;
|
||||
|
||||
public register(path: ConfigPath, row: AutomationPathRow) {
|
||||
const key = stringifyPath(path);
|
||||
const entry = this._rows.get(key);
|
||||
if (!entry) {
|
||||
this._rows.set(key, { path, rows: [row] });
|
||||
} else {
|
||||
entry.rows = [...entry.rows.filter((claimed) => claimed !== row), row];
|
||||
}
|
||||
this._changed();
|
||||
}
|
||||
|
||||
public unregister(path: ConfigPath, row: AutomationPathRow) {
|
||||
const key = stringifyPath(path);
|
||||
const entry = this._rows.get(key);
|
||||
if (!entry) {
|
||||
return;
|
||||
}
|
||||
entry.rows = entry.rows.filter((claimed) => claimed !== row);
|
||||
if (!entry.rows.length) {
|
||||
this._rows.delete(key);
|
||||
}
|
||||
this._changed();
|
||||
}
|
||||
|
||||
/**
|
||||
* Calls `listener` after rows were registered or unregistered, once for all
|
||||
* changes made in the same task. Returns the function to unsubscribe.
|
||||
*/
|
||||
public subscribe(listener: () => void): () => void {
|
||||
this._listeners.add(listener);
|
||||
return () => {
|
||||
this._listeners.delete(listener);
|
||||
};
|
||||
}
|
||||
|
||||
private _changed() {
|
||||
if (this._notifyScheduled || !this._listeners.size) {
|
||||
return;
|
||||
}
|
||||
this._notifyScheduled = true;
|
||||
queueMicrotask(() => {
|
||||
this._notifyScheduled = false;
|
||||
this._listeners.forEach((listener) => listener());
|
||||
});
|
||||
}
|
||||
|
||||
public rowAt(path: ConfigPath): AutomationPathRow | undefined {
|
||||
return owner(this._rows.get(stringifyPath(path))?.rows);
|
||||
}
|
||||
|
||||
public pathOf(row: AutomationPathRow): ConfigPath | undefined {
|
||||
return [...this._rows.values()].find((entry) => owner(entry.rows) === row)
|
||||
?.path;
|
||||
}
|
||||
|
||||
public selectedPath(): ConfigPath | undefined {
|
||||
return [...this._rows.values()].find((entry) => owner(entry.rows)?.selected)
|
||||
?.path;
|
||||
}
|
||||
|
||||
/** The rows containing `path`, outermost first. */
|
||||
public rowsAlong(path: ConfigPath): AutomationPathRow[] {
|
||||
return path
|
||||
.map((_segment, index) =>
|
||||
owner(this._rows.get(stringifyPath(path.slice(0, index + 1)))?.rows)
|
||||
)
|
||||
.filter((row): row is AutomationPathRow => row !== undefined);
|
||||
}
|
||||
}
|
||||
|
||||
export const automationRowRegistryContext =
|
||||
createContext<AutomationRowRegistry>("automation-row-registry");
|
||||
|
||||
interface AutomationPathControllerOptions {
|
||||
/** Path segments this element adds to the path of its ancestor. */
|
||||
segment: () => ConfigPath;
|
||||
/** The host while it is a row that opens in the sidebar. */
|
||||
row?: () => AutomationPathRow | undefined;
|
||||
}
|
||||
|
||||
/**
|
||||
* Consumes the path of the closest ancestor and provides it extended by the
|
||||
* host's own segment to its descendants. For a row, also finds the validation
|
||||
* errors the row owns and updates the host when they change.
|
||||
*/
|
||||
export class AutomationPathController extends ContextController {
|
||||
public path: ConfigPath = [];
|
||||
|
||||
public errors: ConfigValidationError[] = NO_ERRORS;
|
||||
|
||||
@consumeContext({ context: automationErrorsContext, subscribe: true })
|
||||
private _configErrors: ConfigValidationError[] = NO_ERRORS;
|
||||
|
||||
private _unsubscribeRegistry?: () => void;
|
||||
|
||||
@consumeContext({ context: automationPathContext, subscribe: true })
|
||||
private _parentPath: ConfigPath = [];
|
||||
|
||||
@consumeContext({ context: automationRowRegistryContext })
|
||||
private _registry?: AutomationRowRegistry;
|
||||
|
||||
private _registeredPath?: ConfigPath;
|
||||
|
||||
private _registeredRow?: AutomationPathRow;
|
||||
|
||||
private readonly _provider: ContextProvider<typeof automationPathContext>;
|
||||
|
||||
constructor(
|
||||
host: ReactiveElement,
|
||||
private readonly _options: AutomationPathControllerOptions
|
||||
) {
|
||||
super(host);
|
||||
this._provider = new ContextProvider(host, {
|
||||
context: automationPathContext,
|
||||
initialValue: this.path,
|
||||
});
|
||||
}
|
||||
|
||||
// The path is not rendered, so a changed ancestor path is passed on without
|
||||
// updating the host. Only the host's own segment changes go through an update.
|
||||
protected contextUpdated() {
|
||||
this._sync();
|
||||
}
|
||||
|
||||
public hostUpdate() {
|
||||
this._sync();
|
||||
}
|
||||
|
||||
public hostConnected() {
|
||||
// A row moved by a drag reconnects without an update
|
||||
this._sync();
|
||||
if (this._options.row && this._registry && !this._unsubscribeRegistry) {
|
||||
// Another row on the path of an error can take it over or give it back
|
||||
this._unsubscribeRegistry = this._registry.subscribe(() =>
|
||||
this._syncErrors()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private _sync() {
|
||||
const path = [...this._parentPath, ...this._options.segment()];
|
||||
if (!pathEquals(path, this.path)) {
|
||||
this.path = path;
|
||||
this._provider.setValue(path);
|
||||
}
|
||||
this._syncRegistration();
|
||||
this._syncErrors();
|
||||
}
|
||||
|
||||
public hostDisconnected() {
|
||||
this._unsubscribeRegistry?.();
|
||||
this._unsubscribeRegistry = undefined;
|
||||
this._unregister();
|
||||
}
|
||||
|
||||
private _syncErrors() {
|
||||
const errors =
|
||||
this._registeredRow && this._registry && this._configErrors.length
|
||||
? ownedErrors(this._registry, this._configErrors, this._registeredRow)
|
||||
: NO_ERRORS;
|
||||
if (!errorsEqual(errors, this.errors)) {
|
||||
this.errors = errors;
|
||||
this.host.requestUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
private _syncRegistration() {
|
||||
if (!this._registry || !this._options.row) {
|
||||
return;
|
||||
}
|
||||
const row = this.host.isConnected ? this._options.row() : undefined;
|
||||
if (
|
||||
this._registeredPath &&
|
||||
(!row || !pathEquals(this._registeredPath, this.path))
|
||||
) {
|
||||
this._unregister();
|
||||
}
|
||||
if (row && !this._registeredPath) {
|
||||
this._registry.register(this.path, row);
|
||||
this._registeredPath = this.path;
|
||||
this._registeredRow = row;
|
||||
}
|
||||
}
|
||||
|
||||
private _unregister() {
|
||||
if (this._registeredPath && this._registeredRow) {
|
||||
this._registry?.unregister(this._registeredPath, this._registeredRow);
|
||||
}
|
||||
this._registeredPath = undefined;
|
||||
this._registeredRow = undefined;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
import { mdiAlertCircle } from "@mdi/js";
|
||||
import { html, nothing } from "lit";
|
||||
import type { ConfigPath } from "../../../common/util/config-path";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import type { ConfigValidationError } from "../../../data/config";
|
||||
import { errorText } from "./automation-errors";
|
||||
|
||||
export const renderRowErrorIcon = (
|
||||
errors: ConfigValidationError[],
|
||||
path: ConfigPath,
|
||||
label: string
|
||||
) =>
|
||||
errors.length
|
||||
? html`<ha-svg-icon
|
||||
id="error-icon"
|
||||
slot="icons"
|
||||
tabindex="0"
|
||||
class="error-indicator"
|
||||
.path=${mdiAlertCircle}
|
||||
role="img"
|
||||
aria-label=${label}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip for="error-icon" slot="icons">
|
||||
${errors.map((error) => html`<p>${errorText(error, path)}</p>`)}
|
||||
</ha-tooltip>`
|
||||
: nothing;
|
||||
@@ -96,6 +96,8 @@ import "./types/ha-automation-condition-trigger";
|
||||
import "./types/ha-automation-condition-zone";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
import { scrollRowIntoView } from "../scroll-row-into-view";
|
||||
import { AutomationPathController } from "../automation-path";
|
||||
import { renderRowErrorIcon } from "../automation-row-error-icon";
|
||||
|
||||
export interface ConditionElement extends LitElement {
|
||||
condition: Condition;
|
||||
@@ -113,6 +115,8 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public root = false;
|
||||
|
||||
@property({ attribute: false }) public index!: number;
|
||||
|
||||
@property({ type: Boolean }) public first?: boolean;
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
@@ -124,6 +128,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
@property({ type: Boolean, attribute: "sort-selected" })
|
||||
public sortSelected = false;
|
||||
|
||||
protected readonly pathController = new AutomationPathController(this, {
|
||||
segment: () => [this.index],
|
||||
row: () => (this.optionsInSidebar ? this : undefined),
|
||||
});
|
||||
|
||||
@state() private _collapsed = true;
|
||||
|
||||
@state() private _isNew = false;
|
||||
@@ -254,6 +263,11 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
)}
|
||||
</ha-automation-row-event-chip>
|
||||
|
||||
${renderRowErrorIcon(
|
||||
this.pathController.errors,
|
||||
this.pathController.path,
|
||||
this.hass.localize("ui.panel.config.automation.editor.errors.label")
|
||||
)}
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-dropdown
|
||||
@@ -521,6 +535,7 @@ export default class HaAutomationConditionRow extends LitElement {
|
||||
this.condition.condition
|
||||
)}
|
||||
.sortSelected=${this.sortSelected}
|
||||
.error=${this.pathController.errors.length > 0}
|
||||
.dim=${this._testing}
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
|
||||
@@ -27,9 +27,18 @@ import {
|
||||
} from "../../../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import {
|
||||
deviceAutomationErrorText,
|
||||
deviceErrorText,
|
||||
} from "../../automation-errors";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
|
||||
@customElement("ha-automation-condition-device")
|
||||
export class HaDeviceCondition extends LitElement {
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["device_id", "domain"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Object }) public condition!: DeviceCondition;
|
||||
@@ -132,12 +141,20 @@ export class HaDeviceCondition extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const deviceError = this._fieldErrors.fieldError("device_id");
|
||||
const automationError = this._fieldErrors.fieldError("domain");
|
||||
const deviceId = this._deviceId || this.condition.device_id;
|
||||
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
.invalid=${!!deviceError}
|
||||
.errorMessage=${
|
||||
deviceError
|
||||
? deviceErrorText(this.hass.localize, deviceError)
|
||||
: undefined
|
||||
}
|
||||
@value-changed=${this._devicePicked}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.hass.localize(
|
||||
@@ -145,6 +162,16 @@ export class HaDeviceCondition extends LitElement {
|
||||
)}
|
||||
></ha-device-picker>
|
||||
<ha-device-condition-picker
|
||||
.invalid=${!!automationError}
|
||||
.errorMessage=${
|
||||
automationError
|
||||
? deviceAutomationErrorText(
|
||||
this.hass.localize,
|
||||
"conditions",
|
||||
automationError
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.value=${this.condition}
|
||||
.deviceId=${deviceId}
|
||||
@value-changed=${this._deviceConditionPicked}
|
||||
|
||||
@@ -29,6 +29,7 @@ export abstract class HaLogicalCondition
|
||||
protected render() {
|
||||
return html`
|
||||
<ha-automation-condition
|
||||
path-key="conditions"
|
||||
.conditions=${this.condition.conditions || []}
|
||||
@value-changed=${this._valueChanged}
|
||||
.hass=${this.hass}
|
||||
|
||||
@@ -34,6 +34,9 @@ import {
|
||||
} from "../../../../../data/target";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
import "../../../../../components/ha-input-helper-text";
|
||||
import { targetErrorText } from "../../automation-errors";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
import { targetExpandGroupContext } from "../../../../../components/target-picker/target-expand-group-context";
|
||||
|
||||
// Mirrors `MAX_HISTORY_PRIMING_LOOKBACK` in homeassistant/helpers/condition.py:
|
||||
@@ -53,6 +56,10 @@ export class HaPlatformCondition extends LitElement {
|
||||
@provide({ context: targetExpandGroupContext })
|
||||
protected _targetExpandGroup = false;
|
||||
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["target"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: PlatformCondition;
|
||||
@@ -234,6 +241,7 @@ export class HaPlatformCondition extends LitElement {
|
||||
></ha-selector>`
|
||||
: nothing
|
||||
}
|
||||
${this._renderTargetError()}
|
||||
${
|
||||
shouldRenderDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
@@ -258,6 +266,15 @@ export class HaPlatformCondition extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTargetError() {
|
||||
const error = this._fieldErrors.fieldError("target");
|
||||
return error
|
||||
? html`<ha-input-helper-text class="target-error">
|
||||
${targetErrorText(this.hass.localize, error)}
|
||||
</ha-input-helper-text>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector["target"] | null | undefined) =>
|
||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
||||
@@ -680,6 +697,10 @@ export class HaPlatformCondition extends LitElement {
|
||||
1px solid var(--divider-color)
|
||||
);
|
||||
}
|
||||
.target-error {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-yaml-editor {
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { createDurationData } from "../../../../../common/datetime/create_duration_data";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import type { StateCondition } from "../../../../../data/automation";
|
||||
import { STATE_CONDITION_HIDDEN_ATTRIBUTES } from "../../../../../data/entity/entity_attributes";
|
||||
@@ -71,6 +72,10 @@ const SCHEMA = memoizeOne(
|
||||
|
||||
@customElement("ha-automation-condition-state")
|
||||
export class HaStateCondition extends LitElement implements ConditionElement {
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["entity_id", "attribute", "state", "for"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public condition!: StateCondition;
|
||||
@@ -118,6 +123,7 @@ export class HaStateCondition extends LitElement implements ConditionElement {
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.error=${this._fieldErrors.fields}
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${SCHEMA(hasAttribute)}
|
||||
|
||||
@@ -56,7 +56,6 @@ import {
|
||||
triggerAutomationActions,
|
||||
} from "../../../data/automation";
|
||||
import { substituteBlueprint } from "../../../data/blueprint";
|
||||
import { validateConfig } from "../../../data/config";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import {
|
||||
type EntityRegistryEntry,
|
||||
@@ -71,7 +70,7 @@ 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, ValueChangedEvent } from "../../../types";
|
||||
import type { ValueChangedEvent } from "../../../types";
|
||||
import { isMac } from "../../../util/is_mac";
|
||||
import { showEditorToast } from "./editor-toast";
|
||||
import { showAssignCategoryDialog } from "../category/show-dialog-assign-category";
|
||||
@@ -159,7 +158,14 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
const { id, ...rest } = normalizeAutomationConfig(config, report);
|
||||
return { id, ...rest };
|
||||
},
|
||||
checkValidation: () => this._checkValidation(),
|
||||
validationSections: (config) =>
|
||||
"use_blueprint" in config
|
||||
? {}
|
||||
: {
|
||||
triggers: config.triggers,
|
||||
conditions: config.conditions,
|
||||
actions: config.actions,
|
||||
},
|
||||
};
|
||||
|
||||
private _undoRedoController = new UndoRedoController<AutomationConfig>(this, {
|
||||
@@ -501,16 +507,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
>
|
||||
${
|
||||
this.errors
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
slot="alerts"
|
||||
>
|
||||
${this.errors}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
${this.renderConfigErrorsAlert("alerts")}
|
||||
</blueprint-automation-editor>
|
||||
`
|
||||
: html`
|
||||
@@ -522,6 +519,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
.config=${this.config}
|
||||
.disabled=${this.readOnly}
|
||||
.saving=${this.saving}
|
||||
.configErrors=${this.configErrors}
|
||||
@value-changed=${this._valueChanged}
|
||||
@save-automation=${this._handleSaveAutomation}
|
||||
@editor-save=${this._handleSaveAutomation}
|
||||
@@ -530,26 +528,17 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
<div class="alert-wrapper" slot="alerts">
|
||||
${this._renderDeprecatedMigratedAlert()}
|
||||
${
|
||||
this.errors || stateObj?.state === UNAVAILABLE
|
||||
stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
.title=${
|
||||
stateObj?.state === UNAVAILABLE
|
||||
? this.hass.localize(
|
||||
"ui.panel.config.automation.editor.unavailable"
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.title=${this.hass.localize(
|
||||
"ui.panel.config.automation.editor.unavailable"
|
||||
)}
|
||||
>
|
||||
${this.errors || this.validationErrors}
|
||||
${
|
||||
stateObj?.state === UNAVAILABLE
|
||||
? html`<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiRobotConfused}
|
||||
></ha-svg-icon>`
|
||||
: nothing
|
||||
}
|
||||
<ha-svg-icon
|
||||
slot="icon"
|
||||
.path=${mdiRobotConfused}
|
||||
></ha-svg-icon>
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
@@ -628,6 +617,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
`
|
||||
: this.mode === "yaml"
|
||||
? html`${this._renderDeprecatedMigratedAlert()}
|
||||
${this.renderConfigErrorsAlert()}
|
||||
${
|
||||
stateObj?.state === "off"
|
||||
? html`
|
||||
@@ -747,7 +737,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
}
|
||||
);
|
||||
this._checkValidation();
|
||||
this.checkValidation();
|
||||
});
|
||||
this.currentEntityId = this.entityId;
|
||||
this.readOnly = true;
|
||||
@@ -775,32 +765,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
this.currentEntityId = automation?.entity_id;
|
||||
}
|
||||
|
||||
private async _checkValidation() {
|
||||
this.validationErrors = undefined;
|
||||
if (!this.currentEntityId || !this.config) {
|
||||
return;
|
||||
}
|
||||
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,
|
||||
});
|
||||
this.validationErrors = (
|
||||
Object.entries(validation) as Entries<typeof validation>
|
||||
).map(([key, value]) =>
|
||||
value.valid
|
||||
? ""
|
||||
: html`${this.hass.localize(
|
||||
`ui.panel.config.automation.editor.${key}.name`
|
||||
)}:
|
||||
${value.error}<br />`
|
||||
);
|
||||
}
|
||||
|
||||
private _valueChanged(ev: ValueChangedEvent<AutomationConfig>) {
|
||||
ev.stopPropagation();
|
||||
const config = ev.detail.value;
|
||||
@@ -830,7 +794,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
config: this.config,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
});
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
private _showInfo() {
|
||||
@@ -920,7 +883,6 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
config: this.config!,
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
});
|
||||
this.errors = undefined;
|
||||
}
|
||||
|
||||
protected async confirmUnsavedChanged(addHistory = true): Promise<boolean> {
|
||||
@@ -1001,9 +963,8 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
this._yamlEditor?.setValue(this.config);
|
||||
}
|
||||
this.readOnly = true;
|
||||
this.errors = undefined;
|
||||
} catch (err: any) {
|
||||
this.errors = err.message;
|
||||
showEditorToast(this, { message: err.message });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1116,7 +1077,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
|
||||
private async _saveAutomation(id): Promise<void> {
|
||||
this.saving = true;
|
||||
this.validationErrors = undefined;
|
||||
const config = this.config!;
|
||||
|
||||
let entityRegPromise: Promise<EntityRegistryEntry> | undefined;
|
||||
if (this.entityRegistryUpdate !== undefined && !this.currentEntityId) {
|
||||
@@ -1127,7 +1088,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
}
|
||||
|
||||
try {
|
||||
await saveAutomationConfig(this.hass, id, this.config!);
|
||||
await saveAutomationConfig(this.hass, id, config);
|
||||
|
||||
if (this.entityRegistryUpdate !== undefined) {
|
||||
let entityId = this.currentEntityId;
|
||||
@@ -1162,7 +1123,7 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
}
|
||||
|
||||
if (entityId) {
|
||||
await updateEntityRegistryEntry(this.hass, entityId, {
|
||||
await updateEntityRegistryEntry(this.hass.callWS, entityId, {
|
||||
categories: {
|
||||
automation: this.entityRegistryUpdate.category || null,
|
||||
},
|
||||
@@ -1174,11 +1135,9 @@ export class HaAutomationEditor extends AutomationScriptEditorMixin<AutomationCo
|
||||
|
||||
this._markDirtyStateClean();
|
||||
this.deprecatedConfigMigrated = false;
|
||||
} catch (errors: any) {
|
||||
this.errors = errors.body?.message || errors.error || errors.body;
|
||||
showEditorToast(this, {
|
||||
message: errors.body?.message || errors.error || errors.body,
|
||||
});
|
||||
this.clearConfigErrors();
|
||||
} catch (errors: unknown) {
|
||||
this.showSaveErrors(errors, config);
|
||||
throw errors;
|
||||
} finally {
|
||||
this.saving = false;
|
||||
|
||||
@@ -1267,7 +1267,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) {
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
updateEntityRegistryEntry(this.hass, entityId, {
|
||||
updateEntityRegistryEntry(this.hass.callWS, entityId, {
|
||||
categories: { automation: category },
|
||||
})
|
||||
);
|
||||
@@ -1306,7 +1306,7 @@ ${rejected
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
updateEntityRegistryEntry(this.hass, entityId, {
|
||||
updateEntityRegistryEntry(this.hass.callWS, entityId, {
|
||||
labels:
|
||||
action === "add"
|
||||
? this.hass.entities[entityId].labels.concat(label)
|
||||
@@ -1351,7 +1351,7 @@ ${rejected
|
||||
const promises: Promise<UpdateEntityRegistryEntryResult>[] = [];
|
||||
this._selected.forEach((entityId) => {
|
||||
promises.push(
|
||||
updateEntityRegistryEntry(this.hass, entityId, {
|
||||
updateEntityRegistryEntry(this.hass.callWS, entityId, {
|
||||
area_id: area,
|
||||
})
|
||||
);
|
||||
|
||||
@@ -4,16 +4,26 @@ import type {
|
||||
PropertyValues,
|
||||
TemplateResult,
|
||||
} from "lit";
|
||||
import { css, html } from "lit";
|
||||
import { property, state } from "lit/decorators";
|
||||
import { css, html, nothing } from "lit";
|
||||
import { ifDefined } from "lit/directives/if-defined";
|
||||
import { property, query, state } from "lit/decorators";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { goBack, navigate } from "../../../common/navigate";
|
||||
import { debounce } from "../../../common/util/debounce";
|
||||
import { afterNextRender } from "../../../common/util/render-status";
|
||||
import "../../../components/animation/ha-fade-in";
|
||||
import "../../../components/ha-alert";
|
||||
import "../../../components/ha-spinner"; // used by renderLoading() provided to both editors
|
||||
import type { AutomationMigrationReport } from "../../../data/automation";
|
||||
import {
|
||||
parseConfigSaveError,
|
||||
validateConfigErrors,
|
||||
type ConfigValidationError,
|
||||
type ValidationSectionKeys,
|
||||
} from "../../../data/config";
|
||||
import { fireRelatedContext, fullEntitiesContext } from "../../../data/context";
|
||||
import { UNAVAILABLE } from "../../../data/entity/entity";
|
||||
import type { EntityRegistryEntry } from "../../../data/entity/entity_registry";
|
||||
import {
|
||||
showAlertDialog,
|
||||
@@ -22,7 +32,28 @@ import {
|
||||
import { showMoreInfoDialog } from "../../../dialogs/more-info/show-ha-more-info-dialog";
|
||||
import { DirtyStateProviderMixin } from "../../../mixins/dirty-state-provider-mixin";
|
||||
import type { Constructor, HomeAssistant, Route } from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import {
|
||||
errorsEqual,
|
||||
errorsOnKnownItems,
|
||||
errorText,
|
||||
followErrors,
|
||||
followKnownErrorItems,
|
||||
knownErrorItems,
|
||||
mapErrorsToConfig,
|
||||
NO_ERRORS,
|
||||
type KnownErrorItem,
|
||||
} from "./automation-errors";
|
||||
import type { EntityRegistryUpdate } from "./automation-save-dialog/show-dialog-automation-save";
|
||||
import { showEditorToast } from "./editor-toast";
|
||||
|
||||
const CONFIG_ERRORS_TOAST_ID = "config-errors";
|
||||
|
||||
const REVALIDATE_DELAY = 500;
|
||||
|
||||
interface ErrorsEditorElement extends HTMLElement {
|
||||
revealErrors(): Promise<void>;
|
||||
}
|
||||
|
||||
/** Minimum config shape shared by both AutomationConfig and ScriptConfig. */
|
||||
interface BaseEditorConfig {
|
||||
@@ -57,6 +88,10 @@ export const automationScriptEditorStyles: CSSResult = css`
|
||||
p {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.config-errors p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
ha-button[slot="fab"] {
|
||||
position: fixed;
|
||||
right: calc(16px + var(--safe-area-inset-right, 0px));
|
||||
@@ -82,7 +117,11 @@ export const automationScriptEditorStyles: CSSResult = css`
|
||||
export interface EditorDomainHooks<TConfig> {
|
||||
fetchFileConfig(hass: HomeAssistant, id: string): Promise<TConfig>;
|
||||
normalizeConfig(raw: TConfig, report?: AutomationMigrationReport): TConfig;
|
||||
checkValidation(): Promise<void>;
|
||||
/** The parts of the config `validate_config` checks, none for blueprints. */
|
||||
validationSections(
|
||||
config: TConfig
|
||||
): Partial<Record<"triggers" | "conditions" | "actions", unknown>>;
|
||||
validationSectionKeys?: ValidationSectionKeys;
|
||||
domain: "automation" | "script";
|
||||
}
|
||||
|
||||
@@ -111,8 +150,6 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
@consume({ context: fullEntitiesContext, subscribe: true })
|
||||
entityRegistry?: EntityRegistryEntry[];
|
||||
|
||||
@state() protected errors?: string;
|
||||
|
||||
@state() protected yamlErrors?: string;
|
||||
|
||||
@state() protected currentEntityId?: string;
|
||||
@@ -125,7 +162,7 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
|
||||
@state() protected deprecatedConfigMigrated = false;
|
||||
|
||||
@state() protected validationErrors?: (string | TemplateResult)[];
|
||||
@state() protected configErrors: ConfigValidationError[] = NO_ERRORS;
|
||||
|
||||
@state() protected config?: TConfig;
|
||||
|
||||
@@ -157,6 +194,14 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
|
||||
private _relatedContextAreaId?: string;
|
||||
|
||||
@query("manual-automation-editor, manual-script-editor")
|
||||
private _errorsEditor?: ErrorsEditorElement;
|
||||
|
||||
// Validation responses of an older run are dropped
|
||||
private _validationRun = 0;
|
||||
|
||||
private _knownErrorItems: KnownErrorItem[] = [];
|
||||
|
||||
protected willUpdate(changedProps: PropertyValues): void {
|
||||
super.willUpdate(changedProps);
|
||||
if (
|
||||
@@ -167,6 +212,212 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
if (changedProps.has("config") && this.configErrors.length) {
|
||||
this._knownErrorItems = followKnownErrorItems(
|
||||
this._knownErrorItems,
|
||||
changedProps.get("config"),
|
||||
this.config
|
||||
);
|
||||
this._revalidate();
|
||||
}
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._revalidate.cancel();
|
||||
if (this.configErrors.length) {
|
||||
this._closeErrorsToast();
|
||||
}
|
||||
}
|
||||
|
||||
/** Shows the errors of saving `submitted`, the config sent to core. */
|
||||
protected async showSaveErrors(err: unknown, submitted: TConfig) {
|
||||
const { message, errors } = parseConfigSaveError(err);
|
||||
if (!errors.length) {
|
||||
this._showSaveFailedToast(message);
|
||||
return;
|
||||
}
|
||||
this._validationRun++;
|
||||
const submittedErrors = mapErrorsToConfig(submitted, errors);
|
||||
// Edits made while saving moved or removed some of the items
|
||||
const changed = submitted !== this.config;
|
||||
this._setConfigErrors(
|
||||
this.config,
|
||||
changed
|
||||
? followErrors(submittedErrors, submitted, this.config)
|
||||
: submittedErrors
|
||||
);
|
||||
if (changed) {
|
||||
// Followed errors miss items that were renamed and moved, so check
|
||||
// everything again
|
||||
await this._validateConfig(false);
|
||||
}
|
||||
this._showSaveFailedToast(
|
||||
errors.length === 1
|
||||
? errors[0].message
|
||||
: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.errors.problems_found",
|
||||
{ count: errors.length }
|
||||
)
|
||||
);
|
||||
await this._revealErrors();
|
||||
}
|
||||
|
||||
protected async checkValidation() {
|
||||
if (!this.currentEntityId || !this.config) {
|
||||
return;
|
||||
}
|
||||
if (this.hass.states[this.currentEntityId]?.state !== UNAVAILABLE) {
|
||||
return;
|
||||
}
|
||||
const config = this.config;
|
||||
const sections = this.domainHooks.validationSections(config);
|
||||
if (!Object.keys(sections).length) {
|
||||
return;
|
||||
}
|
||||
const run = ++this._validationRun;
|
||||
let errors: ConfigValidationError[];
|
||||
try {
|
||||
errors = await validateConfigErrors(
|
||||
this.hass,
|
||||
sections,
|
||||
this.domainHooks.validationSectionKeys
|
||||
);
|
||||
} catch (_err: unknown) {
|
||||
// The unavailable alert still shows, only the rows stay unmarked
|
||||
return;
|
||||
}
|
||||
if (run !== this._validationRun || config !== this.config) {
|
||||
return;
|
||||
}
|
||||
this._setConfigErrors(config, mapErrorsToConfig(config, errors));
|
||||
}
|
||||
|
||||
private _setConfigErrors(
|
||||
config: TConfig | undefined,
|
||||
errors: ConfigValidationError[]
|
||||
) {
|
||||
this.configErrors = errors;
|
||||
this._knownErrorItems = knownErrorItems(config, errors);
|
||||
}
|
||||
|
||||
private async _revealErrors() {
|
||||
await this.updateComplete;
|
||||
if (this.mode === "gui") {
|
||||
await this._errorsEditor?.revealErrors();
|
||||
}
|
||||
}
|
||||
|
||||
/** All errors, for the editors that have no rows to show them on. */
|
||||
protected renderConfigErrorsAlert(slot?: string) {
|
||||
return this.configErrors.length
|
||||
? html`<ha-alert
|
||||
alert-type="error"
|
||||
class="config-errors"
|
||||
slot=${ifDefined(slot)}
|
||||
>
|
||||
${this.configErrors.map((error) => html`<p>${errorText(error)}</p>`)}
|
||||
</ha-alert>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _showSaveFailedToast(problems: string) {
|
||||
showEditorToast(this, {
|
||||
id: CONFIG_ERRORS_TOAST_ID,
|
||||
message: this.hass.localize(
|
||||
"ui.panel.config.automation.editor.errors.save_failed",
|
||||
{ problems }
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
private _closeErrorsToast() {
|
||||
// A removed editor can't reach the toast manager by bubbling
|
||||
const host = this.isConnected
|
||||
? this
|
||||
: document.querySelector<HTMLElement>("home-assistant");
|
||||
if (!host) {
|
||||
return;
|
||||
}
|
||||
showToast(host, {
|
||||
id: CONFIG_ERRORS_TOAST_ID,
|
||||
message: "",
|
||||
duration: 0,
|
||||
});
|
||||
}
|
||||
|
||||
// Updates the errors of the items that had one, so fixed problems clear
|
||||
// while editing. New problems elsewhere, and errors outside the validated
|
||||
// sections like an invalid mode, wait for the next save.
|
||||
private _revalidate = debounce(() => {
|
||||
this._validateConfig(true);
|
||||
}, REVALIDATE_DELAY);
|
||||
|
||||
/**
|
||||
* Validates the sections of the current config. With `onlyKnown`, only
|
||||
* errors on items that already had one are kept.
|
||||
*/
|
||||
private async _validateConfig(onlyKnown: boolean) {
|
||||
this._revalidate.cancel();
|
||||
const config = this.config;
|
||||
if (!config || (onlyKnown && !this.configErrors.length)) {
|
||||
return;
|
||||
}
|
||||
const sections = this.domainHooks.validationSections(config);
|
||||
// Blueprint inputs can't be validated on their own, so their errors
|
||||
// only last until the next edit
|
||||
if (!Object.keys(sections).length) {
|
||||
this.clearConfigErrors();
|
||||
return;
|
||||
}
|
||||
const sectionKeys = this.domainHooks.validationSectionKeys ?? {};
|
||||
const run = ++this._validationRun;
|
||||
let sectionErrors: ConfigValidationError[];
|
||||
try {
|
||||
sectionErrors = await validateConfigErrors(
|
||||
this.hass,
|
||||
sections,
|
||||
sectionKeys
|
||||
);
|
||||
} catch (_err: unknown) {
|
||||
return;
|
||||
}
|
||||
if (run !== this._validationRun || config !== this.config) {
|
||||
return;
|
||||
}
|
||||
const mapped = mapErrorsToConfig(config, sectionErrors);
|
||||
const validated = Object.keys(sections).map(
|
||||
(key) => sectionKeys[key] ?? key
|
||||
);
|
||||
const errors = [
|
||||
...this.configErrors.filter(
|
||||
(error) => !validated.includes(error.path[0] as string)
|
||||
),
|
||||
...(onlyKnown
|
||||
? errorsOnKnownItems(this._knownErrorItems, config, mapped)
|
||||
: mapped),
|
||||
];
|
||||
if (!errors.length) {
|
||||
this.clearConfigErrors();
|
||||
return;
|
||||
}
|
||||
this._setConfigErrors(
|
||||
config,
|
||||
errorsEqual(errors, this.configErrors) ? this.configErrors : errors
|
||||
);
|
||||
}
|
||||
|
||||
protected clearConfigErrors() {
|
||||
this._validationRun++;
|
||||
this._revalidate.cancel();
|
||||
if (this.configErrors.length) {
|
||||
this.configErrors = NO_ERRORS;
|
||||
this._closeErrorsToast();
|
||||
}
|
||||
}
|
||||
|
||||
private _setRelatedContext(): void {
|
||||
const areaId = this.currentEntityId
|
||||
? this.entityRegistry?.find(
|
||||
@@ -277,6 +528,7 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
protected async loadConfig(id: string) {
|
||||
const hooks = this.domainHooks;
|
||||
const domain = hooks.domain;
|
||||
this.clearConfigErrors();
|
||||
try {
|
||||
const config = await hooks.fetchFileConfig(this.hass, id);
|
||||
if (!this.isConnected) {
|
||||
@@ -296,7 +548,7 @@ export const AutomationScriptEditorMixin = <TConfig extends BaseEditorConfig>(
|
||||
entityRegistryUpdate: this.entityRegistryUpdate,
|
||||
}
|
||||
);
|
||||
hooks.checkValidation();
|
||||
this.checkValidation();
|
||||
} catch (err: any) {
|
||||
if (!this.isConnected) {
|
||||
return;
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { ContextProvider } from "@lit/context";
|
||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { tinykeys } from "tinykeys";
|
||||
@@ -5,6 +6,7 @@ import { consume } from "../../../common/decorators/consume";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { computeRTL } from "../../../common/util/compute_rtl";
|
||||
import { pathEquals } from "../../../common/util/config-path";
|
||||
import "../../../components/ha-snap-bottom-sheet";
|
||||
import type { HaSnapBottomSheet } from "../../../components/ha-snap-bottom-sheet";
|
||||
import {
|
||||
@@ -22,6 +24,11 @@ import { manifestsContext } from "../../../data/context";
|
||||
import type { DomainManifestLookup } from "../../../data/integration";
|
||||
import { isTriggerList } from "../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../types";
|
||||
import {
|
||||
automationPathContext,
|
||||
automationRowRegistryContext,
|
||||
type AutomationRowRegistry,
|
||||
} from "./automation-path";
|
||||
import "./sidebar/ha-automation-sidebar-action";
|
||||
import type HaAutomationSidebarAction from "./sidebar/ha-automation-sidebar-action";
|
||||
import "./sidebar/ha-automation-sidebar-condition";
|
||||
@@ -61,6 +68,17 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
@consume({ context: manifestsContext, subscribe: true })
|
||||
private _manifests?: DomainManifestLookup;
|
||||
|
||||
@consume({ context: automationRowRegistryContext })
|
||||
private _registry?: AutomationRowRegistry;
|
||||
|
||||
// Editors in the sidebar are not inside their row, so they get its path here
|
||||
private readonly _pathProvider = new ContextProvider(this, {
|
||||
context: automationPathContext,
|
||||
initialValue: [],
|
||||
});
|
||||
|
||||
private _unsubscribeRegistry?: () => void;
|
||||
|
||||
@query("ha-snap-bottom-sheet")
|
||||
private _bottomSheetElement?: HaSnapBottomSheet;
|
||||
|
||||
@@ -83,6 +101,28 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
private _tinykeysUnsub?: () => void;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
// A row dragged to another place while it is open changes its path
|
||||
this._unsubscribeRegistry = this._registry?.subscribe(() =>
|
||||
this._syncPath()
|
||||
);
|
||||
}
|
||||
|
||||
protected willUpdate(changedProperties: PropertyValues<this>) {
|
||||
super.willUpdate(changedProperties);
|
||||
if (changedProperties.has("config")) {
|
||||
this._syncPath();
|
||||
}
|
||||
}
|
||||
|
||||
private _syncPath() {
|
||||
const path = this.config ? (this._registry?.selectedPath() ?? []) : [];
|
||||
if (!pathEquals(path, this._pathProvider.value)) {
|
||||
this._pathProvider.setValue(path);
|
||||
}
|
||||
}
|
||||
|
||||
protected updated(changedProperties: PropertyValues<this>) {
|
||||
super.updated(changedProperties);
|
||||
if (
|
||||
@@ -105,6 +145,8 @@ export default class HaAutomationSidebar extends LitElement {
|
||||
|
||||
disconnectedCallback() {
|
||||
super.disconnectedCallback();
|
||||
this._unsubscribeRegistry?.();
|
||||
this._unsubscribeRegistry = undefined;
|
||||
this._unregisterResizeHandlers();
|
||||
this._tinykeysUnsub?.();
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import type { AutomationClipboard } from "../../../data/automation";
|
||||
import type { Constructor, HomeAssistant } from "../../../types";
|
||||
import { AutomationPathController } from "./automation-path";
|
||||
|
||||
const rowKeys = new WeakMap<object, string>();
|
||||
|
||||
@@ -75,6 +76,13 @@ export const AutomationSortableListMixin = <T extends object>(
|
||||
@property({ type: Boolean, attribute: "sidebar" }) public optionsInSidebar =
|
||||
false;
|
||||
|
||||
/** Key of this list in the config of the row or editor rendering it. */
|
||||
@property({ attribute: "path-key" }) public pathKey?: string;
|
||||
|
||||
protected readonly pathController = new AutomationPathController(this, {
|
||||
segment: () => (this.pathKey === undefined ? [] : [this.pathKey]),
|
||||
});
|
||||
|
||||
@state() protected rowSortSelected?: number;
|
||||
|
||||
@state()
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { provide } from "@lit/context";
|
||||
import { mdiContentSave } from "@mdi/js";
|
||||
import {
|
||||
html,
|
||||
nothing,
|
||||
type LitElement,
|
||||
type nothing,
|
||||
type PropertyValues,
|
||||
type TemplateResult,
|
||||
} from "lit";
|
||||
@@ -15,6 +16,7 @@ import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
import { replaceCurrentUrl } from "../../../common/navigate";
|
||||
import { constructUrlCurrentPath } from "../../../common/url/construct-url";
|
||||
import { nextRender } from "../../../common/util/render-status";
|
||||
import {
|
||||
extractSearchParam,
|
||||
removeSearchParam,
|
||||
@@ -23,6 +25,7 @@ import "../../../components/ha-button";
|
||||
import "../../../components/ha-icon-button";
|
||||
import "../../../components/ha-markdown";
|
||||
import type { SidebarConfig } from "../../../data/automation";
|
||||
import type { ConfigValidationError } from "../../../data/config";
|
||||
import {
|
||||
dirtyStateContext,
|
||||
type DirtyStateContext,
|
||||
@@ -33,6 +36,19 @@ import type {
|
||||
ValueChangedEvent,
|
||||
} from "../../../types";
|
||||
import { showToast } from "../../../util/toast";
|
||||
import "../../../components/ha-alert";
|
||||
import {
|
||||
automationErrorsContext,
|
||||
errorOwner,
|
||||
errorText,
|
||||
NO_ERRORS,
|
||||
} from "./automation-errors";
|
||||
import {
|
||||
AutomationRowRegistry,
|
||||
automationRowRegistryContext,
|
||||
type AutomationPathRow,
|
||||
} from "./automation-path";
|
||||
import { scrollRowIntoView } from "./scroll-row-into-view";
|
||||
import "./ha-automation-sidebar";
|
||||
import type HaAutomationSidebar from "./ha-automation-sidebar";
|
||||
|
||||
@@ -82,6 +98,13 @@ export const ManualEditorMixin = <TConfig>(
|
||||
@query("ha-automation-sidebar")
|
||||
protected sidebarElement?: HaAutomationSidebar;
|
||||
|
||||
@provide({ context: automationRowRegistryContext })
|
||||
protected rowRegistry = new AutomationRowRegistry();
|
||||
|
||||
@provide({ context: automationErrorsContext })
|
||||
@property({ attribute: false })
|
||||
public configErrors: ConfigValidationError[] = NO_ERRORS;
|
||||
|
||||
declare protected collapsableElements?: NodeListOf<
|
||||
Element & {
|
||||
expandAll(): void;
|
||||
@@ -95,16 +118,44 @@ export const ManualEditorMixin = <TConfig>(
|
||||
throw new Error("Not implemented");
|
||||
};
|
||||
|
||||
private _unsubscribeRows?: () => void;
|
||||
|
||||
// Rows register while they render, so errors only count as without a
|
||||
// row once the rows had a frame to render
|
||||
@state() private _rowsRendered = false;
|
||||
|
||||
public connectedCallback() {
|
||||
super.connectedCallback();
|
||||
window.addEventListener("paste", this.handlePaste);
|
||||
// Which errors have a row changes as rows render
|
||||
this._unsubscribeRows = this.rowRegistry.subscribe(() => {
|
||||
if (this.configErrors.length) {
|
||||
this.requestUpdate();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public disconnectedCallback() {
|
||||
window.removeEventListener("paste", this.handlePaste);
|
||||
this._unsubscribeRows?.();
|
||||
this._unsubscribeRows = undefined;
|
||||
super.disconnectedCallback();
|
||||
}
|
||||
|
||||
private _renderUnownedErrors() {
|
||||
if (!this._rowsRendered) {
|
||||
return nothing;
|
||||
}
|
||||
const unowned = this.configErrors.filter(
|
||||
(error) => !errorOwner(this.rowRegistry, error)
|
||||
);
|
||||
return unowned.length
|
||||
? html`<ha-alert alert-type="error" class="unowned-errors">
|
||||
${unowned.map((error) => html`<p>${errorText(error)}</p>`)}
|
||||
</ha-alert>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
protected renderContent(): TemplateResult | typeof nothing {
|
||||
throw new Error("Not implemented");
|
||||
}
|
||||
@@ -133,7 +184,7 @@ export const ManualEditorMixin = <TConfig>(
|
||||
>
|
||||
<div class="content">
|
||||
<slot name="alerts"></slot>
|
||||
${this.renderContent()}
|
||||
${this._renderUnownedErrors()} ${this.renderContent()}
|
||||
</div>
|
||||
<div class="fab-positioner">
|
||||
<ha-button
|
||||
@@ -171,6 +222,9 @@ export const ManualEditorMixin = <TConfig>(
|
||||
|
||||
protected firstUpdated(changedProps: PropertyValues<this>): void {
|
||||
super.firstUpdated(changedProps);
|
||||
nextRender().then(() => {
|
||||
this._rowsRendered = true;
|
||||
});
|
||||
|
||||
this.style.setProperty(
|
||||
"--sidebar-dynamic-width",
|
||||
@@ -295,6 +349,54 @@ export const ManualEditorMixin = <TConfig>(
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Expands the rows along the path of every error, then scrolls to the
|
||||
* first one and opens it in the sidebar. Does nothing while a row with an
|
||||
* error is open, so the user stays where they are.
|
||||
*/
|
||||
public async revealErrors() {
|
||||
await this.updateComplete;
|
||||
const errorRows = () =>
|
||||
this.configErrors.map((error) =>
|
||||
this.rowRegistry.rowsAlong(error.path)
|
||||
);
|
||||
const owners = (rows: AutomationPathRow[][]) =>
|
||||
rows
|
||||
.map((along) => along[along.length - 1])
|
||||
.filter((owner) => owner !== undefined);
|
||||
let rows = errorRows();
|
||||
if (owners(rows).some((owner) => owner.selected)) {
|
||||
return;
|
||||
}
|
||||
const depthOf = (along: AutomationPathRow[][]) =>
|
||||
along.reduce((sum, path) => sum + path.length, 0);
|
||||
// Collapsed rows don't render their children, so expand one level at a
|
||||
// time until no deeper row shows up
|
||||
let depth = -1;
|
||||
while (depthOf(rows) > depth) {
|
||||
depth = depthOf(rows);
|
||||
rows.flat().forEach((row) => row.expand());
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
await nextRender();
|
||||
rows = errorRows();
|
||||
}
|
||||
// Errors aren't in config order, so take the topmost row
|
||||
const first = owners(rows).reduce<AutomationPathRow | undefined>(
|
||||
(top, row) =>
|
||||
!top ||
|
||||
row.getBoundingClientRect().top < top.getBoundingClientRect().top
|
||||
? row
|
||||
: top,
|
||||
undefined
|
||||
);
|
||||
if (!first) {
|
||||
return;
|
||||
}
|
||||
first.openSidebar();
|
||||
await nextRender();
|
||||
scrollRowIntoView(first);
|
||||
}
|
||||
|
||||
public expandAll() {
|
||||
this.collapsableElements?.forEach((element) => {
|
||||
element.expandAll();
|
||||
|
||||
@@ -111,6 +111,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
|
||||
<ha-automation-trigger
|
||||
role="region"
|
||||
path-key="triggers"
|
||||
aria-labelledby="triggers-heading"
|
||||
.triggers=${this.config.triggers || []}
|
||||
.highlightedTriggers=${this.pastedConfig?.triggers}
|
||||
@@ -149,6 +150,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
|
||||
<ha-automation-condition
|
||||
role="region"
|
||||
path-key="conditions"
|
||||
aria-labelledby="conditions-heading"
|
||||
.conditions=${this.config.conditions || []}
|
||||
.highlightedConditions=${this.pastedConfig?.conditions}
|
||||
@@ -183,6 +185,7 @@ export class HaManualAutomationEditor extends ManualEditorMixin<ManualAutomation
|
||||
|
||||
<ha-automation-action
|
||||
role="region"
|
||||
path-key="actions"
|
||||
aria-labelledby="actions-heading"
|
||||
.actions=${this.config.actions || []}
|
||||
.highlightedActions=${this.pastedConfig?.actions}
|
||||
|
||||
@@ -64,6 +64,8 @@ import {
|
||||
} from "../styles";
|
||||
import "../trigger/ha-automation-trigger-references";
|
||||
import { getUiSupportWarnings, isConfigObject } from "../ui-support";
|
||||
import { AutomationPathController } from "../automation-path";
|
||||
import { renderRowErrorIcon } from "../automation-row-error-icon";
|
||||
|
||||
@customElement("ha-automation-option-row")
|
||||
export default class HaAutomationOptionRow extends LitElement {
|
||||
@@ -89,6 +91,11 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
@property({ type: Boolean, attribute: "sort-selected" })
|
||||
public sortSelected = false;
|
||||
|
||||
protected readonly pathController = new AutomationPathController(this, {
|
||||
segment: () => (this.defaultActions ? ["default"] : [this.index]),
|
||||
row: () => (this.optionsInSidebar ? this : undefined),
|
||||
});
|
||||
|
||||
@state() private _expanded = false;
|
||||
|
||||
@state() private _selected = false;
|
||||
@@ -266,6 +273,11 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
}
|
||||
</h3>
|
||||
|
||||
${renderRowErrorIcon(
|
||||
this.pathController.errors,
|
||||
this.pathController.path,
|
||||
this.hass.localize("ui.panel.config.automation.editor.errors.label")
|
||||
)}
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
${
|
||||
@@ -448,6 +460,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-condition
|
||||
path-key="conditions"
|
||||
.conditions=${ensureArray<string | Condition>(
|
||||
this.option.conditions
|
||||
)}
|
||||
@@ -466,6 +479,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
)}:
|
||||
</h4>
|
||||
<ha-automation-action
|
||||
.pathKey=${this.option ? "sequence" : undefined}
|
||||
.actions=${
|
||||
(this.option
|
||||
? ensureArray(this.option.sequence) || []
|
||||
@@ -494,6 +508,7 @@ export default class HaAutomationOptionRow extends LitElement {
|
||||
.collapsed=${this._collapsed}
|
||||
.selected=${this._selected}
|
||||
.sortSelected=${this.sortSelected}
|
||||
.error=${this.pathController.errors.length > 0}
|
||||
@click=${this._toggleSidebar}
|
||||
@toggle-collapsed=${this._toggleCollapse}
|
||||
@delete-row=${this._removeOption}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { provide } from "@lit/context";
|
||||
import { mdiClose, mdiDotsVertical } from "@mdi/js";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query } from "lit/decorators";
|
||||
@@ -12,6 +13,13 @@ import "../../../../components/ha-icon-button";
|
||||
import { ScrollableFadeMixin } from "../../../../mixins/scrollable-fade-mixin";
|
||||
import { haStyleScrollbar } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
import "../../../../components/ha-alert";
|
||||
import { errorField, errorText } from "../automation-errors";
|
||||
import {
|
||||
automationInlineErrorFieldsContext,
|
||||
AutomationFieldErrorsController,
|
||||
InlineErrorFields,
|
||||
} from "../automation-field-errors";
|
||||
import "../ha-automation-editor-warning";
|
||||
|
||||
export interface SidebarOverflowMenuEntry {
|
||||
@@ -44,11 +52,22 @@ export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
|
||||
|
||||
@query(".card-content") private _contentElement!: HTMLDivElement;
|
||||
|
||||
private readonly _errors = new AutomationFieldErrorsController(this);
|
||||
|
||||
@provide({ context: automationInlineErrorFieldsContext })
|
||||
protected inlineErrorFields = new InlineErrorFields(() =>
|
||||
this.requestUpdate()
|
||||
);
|
||||
|
||||
protected get scrollableElement(): HTMLElement | null {
|
||||
return this._contentElement;
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const errors = this._errors.rowErrors.filter((error) => {
|
||||
const field = errorField(error, this._errors.path);
|
||||
return field === undefined || !this.inlineErrorFields.has(field);
|
||||
});
|
||||
return html`
|
||||
<ha-card
|
||||
outlined
|
||||
@@ -90,6 +109,15 @@ export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
|
||||
</ha-automation-editor-warning>`
|
||||
: nothing
|
||||
}
|
||||
${
|
||||
errors.length
|
||||
? html`<ha-alert alert-type="error" class="errors">
|
||||
${errors.map(
|
||||
(error) => html`<p>${errorText(error, this._errors.path)}</p>`
|
||||
)}
|
||||
</ha-alert>`
|
||||
: nothing
|
||||
}
|
||||
<div class="card-content ha-scrollbar">
|
||||
<slot></slot>
|
||||
${this.renderScrollableFades(this.isWide)}
|
||||
@@ -148,6 +176,15 @@ export default class HaAutomationSidebarCard extends ScrollableFadeMixin(
|
||||
padding-bottom: max(var(--safe-area-inset-bottom, 0px), 32px);
|
||||
}
|
||||
|
||||
.errors {
|
||||
margin: 0 var(--ha-space-4) var(--ha-space-4);
|
||||
}
|
||||
|
||||
.errors p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
.fade-top {
|
||||
top: var(--ha-space-17);
|
||||
}
|
||||
|
||||
@@ -124,6 +124,19 @@ export const rowStyles = css`
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.error-indicator {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
padding: var(--ha-space-3);
|
||||
}
|
||||
.error-indicator + ha-tooltip::part(body) {
|
||||
cursor: default;
|
||||
max-width: 300px;
|
||||
}
|
||||
.error-indicator + ha-tooltip p {
|
||||
white-space: pre-wrap;
|
||||
margin: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
export const editorStyles = css`
|
||||
@@ -280,6 +293,15 @@ export const manualEditorStyles = css`
|
||||
.header a {
|
||||
color: var(--secondary-text-color);
|
||||
}
|
||||
|
||||
.unowned-errors {
|
||||
display: block;
|
||||
margin-bottom: var(--ha-space-2);
|
||||
}
|
||||
.unowned-errors p {
|
||||
margin: 0;
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
`;
|
||||
|
||||
export const automationRowsStyles = css`
|
||||
|
||||
@@ -120,6 +120,8 @@ import "./types/ha-automation-trigger-webhook";
|
||||
import "./types/ha-automation-trigger-zone";
|
||||
import { renderCtrlOrCmd } from "../../../../common/keyboard/ctrl-or-cmd";
|
||||
import { scrollRowIntoView } from "../scroll-row-into-view";
|
||||
import { AutomationPathController } from "../automation-path";
|
||||
import { renderRowErrorIcon } from "../automation-row-error-icon";
|
||||
import { targetExpandGroupContext } from "../../../../components/target-picker/target-expand-group-context";
|
||||
|
||||
export interface TriggerElement extends LitElement {
|
||||
@@ -164,7 +166,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
|
||||
@property({ type: Boolean }) public last?: boolean;
|
||||
|
||||
@property({ type: Number }) public index?: number;
|
||||
@property({ attribute: false }) public index!: number;
|
||||
|
||||
@property({ type: Boolean }) public highlight?: boolean;
|
||||
|
||||
@@ -174,6 +176,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
@property({ type: Boolean, attribute: "sort-selected" })
|
||||
public sortSelected = false;
|
||||
|
||||
protected readonly pathController = new AutomationPathController(this, {
|
||||
segment: () => [this.index],
|
||||
row: () => (this.optionsInSidebar ? this : undefined),
|
||||
});
|
||||
|
||||
@state() private _yamlMode = false;
|
||||
|
||||
@state() private _triggered = false;
|
||||
@@ -394,6 +401,11 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
)}
|
||||
</ha-automation-row-event-chip>
|
||||
|
||||
${renderRowErrorIcon(
|
||||
this.pathController.errors,
|
||||
this.pathController.path,
|
||||
this.hass.localize("ui.panel.config.automation.editor.errors.label")
|
||||
)}
|
||||
<slot name="icons" slot="icons"></slot>
|
||||
|
||||
<ha-dropdown
|
||||
@@ -658,6 +670,7 @@ export default class HaAutomationTriggerRow extends LitElement {
|
||||
.selected=${this._selected}
|
||||
.highlight=${this.highlight}
|
||||
.sortSelected=${this.sortSelected}
|
||||
.error=${this.pathController.errors.length > 0}
|
||||
.dim=${this._triggered}
|
||||
@click=${this._toggleSidebar}
|
||||
>${
|
||||
|
||||
@@ -29,9 +29,18 @@ import {
|
||||
} from "../../../../../data/device/device_registry";
|
||||
import type { EntityRegistryEntry } from "../../../../../data/entity/entity_registry";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import {
|
||||
deviceAutomationErrorText,
|
||||
deviceErrorText,
|
||||
} from "../../automation-errors";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
|
||||
@customElement("ha-automation-trigger-device")
|
||||
export class HaDeviceTrigger extends LitElement {
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["device_id", "domain"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ type: Object }) public trigger!: DeviceTrigger;
|
||||
@@ -136,12 +145,20 @@ export class HaDeviceTrigger extends LitElement {
|
||||
}
|
||||
|
||||
protected render() {
|
||||
const deviceError = this._fieldErrors.fieldError("device_id");
|
||||
const automationError = this._fieldErrors.fieldError("domain");
|
||||
const deviceId = this._deviceId || this.trigger.device_id;
|
||||
|
||||
return html`
|
||||
<ha-device-picker
|
||||
.value=${deviceId}
|
||||
.replacementDeviceIds=${this._replacementDeviceIds}
|
||||
.invalid=${!!deviceError}
|
||||
.errorMessage=${
|
||||
deviceError
|
||||
? deviceErrorText(this.hass.localize, deviceError)
|
||||
: undefined
|
||||
}
|
||||
@value-changed=${this._devicePicked}
|
||||
.disabled=${this.disabled}
|
||||
.label=${this.hass.localize(
|
||||
@@ -149,6 +166,16 @@ export class HaDeviceTrigger extends LitElement {
|
||||
)}
|
||||
></ha-device-picker>
|
||||
<ha-device-trigger-picker
|
||||
.invalid=${!!automationError}
|
||||
.errorMessage=${
|
||||
automationError
|
||||
? deviceAutomationErrorText(
|
||||
this.hass.localize,
|
||||
"triggers",
|
||||
automationError
|
||||
)
|
||||
: undefined
|
||||
}
|
||||
.value=${this.trigger}
|
||||
.deviceId=${deviceId}
|
||||
@value-changed=${this._deviceTriggerPicked}
|
||||
|
||||
@@ -26,6 +26,7 @@ export class HaTriggerList extends LitElement implements TriggerElement {
|
||||
|
||||
return html`
|
||||
<ha-automation-trigger
|
||||
path-key="triggers"
|
||||
.triggers=${triggers}
|
||||
.hass=${this.hass}
|
||||
.disabled=${this.disabled}
|
||||
|
||||
@@ -4,6 +4,7 @@ import memoizeOne from "memoize-one";
|
||||
import { ensureArray } from "../../../../../common/array/ensure-array";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
import type { NumericStateTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
@@ -183,6 +184,17 @@ const SCHEMA = [
|
||||
|
||||
@customElement("ha-automation-trigger-numeric_state")
|
||||
export class HaNumericStateTrigger extends LitElement {
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: [
|
||||
"entity_id",
|
||||
"attribute",
|
||||
"value_template",
|
||||
"above",
|
||||
"below",
|
||||
"for",
|
||||
],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: NumericStateTrigger;
|
||||
@@ -230,6 +242,7 @@ export class HaNumericStateTrigger extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.error=${this._fieldErrors.fields}
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${SCHEMA}
|
||||
|
||||
@@ -23,6 +23,9 @@ import {
|
||||
} from "../../../../../data/trigger";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import { documentationUrl } from "../../../../../util/documentation-url";
|
||||
import "../../../../../components/ha-input-helper-text";
|
||||
import { targetErrorText } from "../../automation-errors";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
import { targetExpandGroupContext } from "../../../../../components/target-picker/target-expand-group-context";
|
||||
|
||||
const showOptionalToggle = (field: TriggerDescription["fields"][string]) =>
|
||||
@@ -43,6 +46,10 @@ export class HaPlatformTrigger extends LitElement {
|
||||
@provide({ context: targetExpandGroupContext })
|
||||
protected _targetExpandGroup = false;
|
||||
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["target"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: PlatformTrigger;
|
||||
@@ -226,6 +233,7 @@ export class HaPlatformTrigger extends LitElement {
|
||||
></ha-selector>`
|
||||
: nothing
|
||||
}
|
||||
${this._renderTargetError()}
|
||||
${
|
||||
shouldRenderDataYaml
|
||||
? html`<ha-yaml-editor
|
||||
@@ -250,6 +258,15 @@ export class HaPlatformTrigger extends LitElement {
|
||||
`;
|
||||
}
|
||||
|
||||
private _renderTargetError() {
|
||||
const error = this._fieldErrors.fieldError("target");
|
||||
return error
|
||||
? html`<ha-input-helper-text class="target-error">
|
||||
${targetErrorText(this.hass.localize, error)}
|
||||
</ha-input-helper-text>`
|
||||
: nothing;
|
||||
}
|
||||
|
||||
private _targetSelector = memoizeOne(
|
||||
(targetSelector: TargetSelector["target"] | null | undefined) =>
|
||||
targetSelector ? { target: { ...targetSelector } } : { target: {} }
|
||||
@@ -558,6 +575,10 @@ export class HaPlatformTrigger extends LitElement {
|
||||
1px solid var(--divider-color)
|
||||
);
|
||||
}
|
||||
.target-error {
|
||||
color: var(--ha-color-on-danger-quiet);
|
||||
padding-bottom: var(--ha-space-2);
|
||||
}
|
||||
ha-yaml-editor {
|
||||
padding: var(--ha-space-4) 0;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import { hasTemplate } from "../../../../../common/string/has-template";
|
||||
import type { LocalizeFunc } from "../../../../../common/translations/localize";
|
||||
import { ANY_STATE_VALUE } from "../../../../../components/entity/const";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
import type {
|
||||
HaFormSchema,
|
||||
SchemaUnion,
|
||||
@@ -57,6 +58,10 @@ const stateTriggerStruct = assign(
|
||||
|
||||
@customElement("ha-automation-trigger-state")
|
||||
export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["entity_id", "attribute", "from", "to", "for"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: StateTrigger;
|
||||
@@ -311,6 +316,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement {
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.error=${this._fieldErrors.fields}
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${schema}
|
||||
|
||||
@@ -3,6 +3,7 @@ import { customElement, property } from "lit/decorators";
|
||||
import type { TemplateTrigger } from "../../../../../data/automation";
|
||||
import type { HomeAssistant } from "../../../../../types";
|
||||
import "../../../../../components/ha-form/ha-form";
|
||||
import { AutomationFieldErrorsController } from "../../automation-field-errors";
|
||||
import { fireEvent } from "../../../../../common/dom/fire_event";
|
||||
import type { SchemaUnion } from "../../../../../components/ha-form/types";
|
||||
|
||||
@@ -25,6 +26,10 @@ const SCHEMA = [
|
||||
|
||||
@customElement("ha-automation-trigger-template")
|
||||
export class HaTemplateTrigger extends LitElement {
|
||||
private readonly _fieldErrors = new AutomationFieldErrorsController(this, {
|
||||
inlineFields: ["value_template", "for"],
|
||||
});
|
||||
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
|
||||
@property({ attribute: false }) public trigger!: TemplateTrigger;
|
||||
@@ -54,6 +59,7 @@ export class HaTemplateTrigger extends LitElement {
|
||||
|
||||
return html`
|
||||
<ha-form
|
||||
.error=${this._fieldErrors.fields}
|
||||
.hass=${this.hass}
|
||||
.data=${data}
|
||||
.schema=${SCHEMA}
|
||||
|
||||
@@ -216,7 +216,7 @@ class HaConfigBackupOverview extends LitElement {
|
||||
back-path="/config/system"
|
||||
.hass=${this.hass}
|
||||
.narrow=${this.narrow}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.overview.header")}
|
||||
.header=${this.hass.localize("ui.panel.config.backup.caption")}
|
||||
>
|
||||
<ha-dropdown
|
||||
slot="toolbar-icon"
|
||||
|
||||
@@ -123,7 +123,7 @@ class DialogAssignCategory extends DirtyStateProviderMixin<AssignCategoryFormSta
|
||||
this._error = undefined;
|
||||
try {
|
||||
await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this.hass.callWS,
|
||||
this._params!.entityReg.entity_id,
|
||||
{
|
||||
categories: { [this._scope!]: this._category || null },
|
||||
|
||||
@@ -17,6 +17,8 @@ import type { LabelRegistryEntry } from "../../../../data/label/label_registry";
|
||||
import { haStyle } from "../../../../resources/styles";
|
||||
import type { HomeAssistant } from "../../../../types";
|
||||
|
||||
const ADDRESS_TYPES = ["mac", "bluetooth", "zigbee"];
|
||||
|
||||
@customElement("ha-device-info-card")
|
||||
export class HaDeviceCard extends LitElement {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@@ -234,9 +236,13 @@ export class HaDeviceCard extends LitElement {
|
||||
}
|
||||
|
||||
protected _getAddresses() {
|
||||
return this.device.connections.filter((conn) =>
|
||||
["mac", "bluetooth", "zigbee"].includes(conn[0])
|
||||
);
|
||||
return this.device.connections
|
||||
.filter((conn) => ADDRESS_TYPES.includes(conn[0]))
|
||||
.sort(
|
||||
(a, b) =>
|
||||
ADDRESS_TYPES.indexOf(a[0]) - ADDRESS_TYPES.indexOf(b[0]) ||
|
||||
stringCompare(a[1], b[1])
|
||||
);
|
||||
}
|
||||
|
||||
private _computeDeviceNameDisplay(deviceId: string) {
|
||||
|
||||
@@ -1600,7 +1600,11 @@ export class HaConfigDevicePage extends LitElement {
|
||||
delete updates.disabled_by;
|
||||
}
|
||||
try {
|
||||
await updateDeviceRegistryEntry(this.hass, this.deviceId, updates);
|
||||
await updateDeviceRegistryEntry(
|
||||
this.hass.callWS,
|
||||
this.deviceId,
|
||||
updates
|
||||
);
|
||||
} catch (err: unknown) {
|
||||
showAlertDialog(this, {
|
||||
title: this.hass.localize(
|
||||
@@ -1616,7 +1620,7 @@ export class HaConfigDevicePage extends LitElement {
|
||||
};
|
||||
|
||||
private async _enableDevice(): Promise<void> {
|
||||
await updateDeviceRegistryEntry(this.hass, this.deviceId, {
|
||||
await updateDeviceRegistryEntry(this.hass.callWS, this.deviceId, {
|
||||
disabled_by: null,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1167,7 +1167,7 @@ export class HaConfigDeviceDashboard extends LitElement {
|
||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||
this._selected.forEach((deviceId) => {
|
||||
promises.push(
|
||||
updateDeviceRegistryEntry(this.hass, deviceId, {
|
||||
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
|
||||
area_id: area,
|
||||
})
|
||||
);
|
||||
@@ -1219,7 +1219,7 @@ ${rejected
|
||||
const promises: Promise<DeviceRegistryEntry>[] = [];
|
||||
this._selected.forEach((deviceId) => {
|
||||
promises.push(
|
||||
updateDeviceRegistryEntry(this.hass, deviceId, {
|
||||
updateDeviceRegistryEntry(this.hass.callWS, deviceId, {
|
||||
labels:
|
||||
action === "add"
|
||||
? this.hass.devices[deviceId].labels.concat(label)
|
||||
|
||||
@@ -114,7 +114,7 @@ export class DialogVacuumSegmentMapping
|
||||
last_seen_segments: mapper.lastSeenSegments,
|
||||
};
|
||||
|
||||
await updateEntityRegistryEntry(this.hass, this._params.entityId, {
|
||||
await updateEntityRegistryEntry(this.hass.callWS, this._params.entityId, {
|
||||
options_domain: "vacuum",
|
||||
options: options,
|
||||
});
|
||||
|
||||
@@ -100,7 +100,6 @@ export class EntitySettingsHelperTab extends LitElement {
|
||||
: html`
|
||||
<span @value-changed=${this._valueChanged}>
|
||||
${dynamicElement(`ha-${this.entry.platform}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
entry: this.entry,
|
||||
disabled: this._item === null,
|
||||
|
||||
@@ -1414,7 +1414,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
}
|
||||
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass!,
|
||||
this.hass!.callWS,
|
||||
this.entry.entity_id,
|
||||
params
|
||||
);
|
||||
@@ -1593,7 +1593,7 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
private async _restoreEntityId(): Promise<void> {
|
||||
// use the unsaved name and area, as they are part of the entity ID
|
||||
const entityIds = await getAutomaticEntityIds(
|
||||
this.hass,
|
||||
this.hass.callWS,
|
||||
[this._origEntityId],
|
||||
{
|
||||
[this._origEntityId]: {
|
||||
@@ -1821,7 +1821,11 @@ export class EntityRegistrySettingsEditor extends LitElement {
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device: this._device!,
|
||||
updateEntry: async (updates) => {
|
||||
await updateDeviceRegistryEntry(this.hass, this._device!.id, updates);
|
||||
await updateDeviceRegistryEntry(
|
||||
this.hass.callWS,
|
||||
this._device!.id,
|
||||
updates
|
||||
);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -179,7 +179,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
showDeviceRegistryDetailDialog(this, {
|
||||
device: device,
|
||||
updateEntry: async (updates) => {
|
||||
await updateDeviceRegistryEntry(this.hass, device.id, updates);
|
||||
await updateDeviceRegistryEntry(this.hass.callWS, device.id, updates);
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -189,7 +189,7 @@ export class EntityRegistrySettings extends SubscribeMixin(LitElement) {
|
||||
this._submitting = true;
|
||||
try {
|
||||
const result = await updateEntityRegistryEntry(
|
||||
this.hass!,
|
||||
this.hass!.callWS,
|
||||
this.entry.entity_id,
|
||||
{ disabled_by: null }
|
||||
);
|
||||
|
||||
@@ -1388,7 +1388,7 @@ export class HaConfigEntities extends LitElement {
|
||||
const result = await Promise.allSettled(
|
||||
this._selected.map(async (entity) => {
|
||||
const updateResult = await updateEntityRegistryEntry(
|
||||
this.hass,
|
||||
this.hass.callWS,
|
||||
entity,
|
||||
{
|
||||
disabled_by: null,
|
||||
@@ -1453,7 +1453,7 @@ export class HaConfigEntities extends LitElement {
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirm: () => {
|
||||
this._selected.forEach((entity) =>
|
||||
updateEntityRegistryEntry(this.hass, entity, {
|
||||
updateEntityRegistryEntry(this.hass.callWS, entity, {
|
||||
disabled_by: "user",
|
||||
})
|
||||
);
|
||||
@@ -1475,7 +1475,7 @@ export class HaConfigEntities extends LitElement {
|
||||
dismissText: this.hass.localize("ui.common.cancel"),
|
||||
confirm: () => {
|
||||
this._selected.forEach((entity) =>
|
||||
updateEntityRegistryEntry(this.hass, entity, {
|
||||
updateEntityRegistryEntry(this.hass.callWS, entity, {
|
||||
hidden_by: "user",
|
||||
})
|
||||
);
|
||||
@@ -1486,7 +1486,7 @@ export class HaConfigEntities extends LitElement {
|
||||
|
||||
private _unhideSelected = () => {
|
||||
this._selected.forEach((entity) =>
|
||||
updateEntityRegistryEntry(this.hass, entity, {
|
||||
updateEntityRegistryEntry(this.hass.callWS, entity, {
|
||||
hidden_by: null,
|
||||
})
|
||||
);
|
||||
@@ -1523,7 +1523,7 @@ export class HaConfigEntities extends LitElement {
|
||||
return;
|
||||
}
|
||||
promises.push(
|
||||
updateEntityRegistryEntry(this.hass, entityId, {
|
||||
updateEntityRegistryEntry(this.hass.callWS, entityId, {
|
||||
labels:
|
||||
action === "add"
|
||||
? entityReg.labels.concat(label)
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
import { mdiAlertOutline } from "@mdi/js";
|
||||
import Fuse from "fuse.js";
|
||||
import type { HassConfig } from "home-assistant-js-websocket";
|
||||
import type { Connection, HassConfig } from "home-assistant-js-websocket";
|
||||
import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit";
|
||||
import { css, html, LitElement, nothing } from "lit";
|
||||
import { customElement, property, query, state } from "lit/decorators";
|
||||
import { customElement, query, state } from "lit/decorators";
|
||||
import memoizeOne from "memoize-one";
|
||||
import { isComponentLoaded } from "../../../common/config/is_component_loaded";
|
||||
import { consume } from "../../../common/decorators/consume";
|
||||
import { transform } from "../../../common/decorators/transform";
|
||||
import { dynamicElement } from "../../../common/dom/dynamic-element-directive";
|
||||
import type { HASSDomEvent } from "../../../common/dom/fire_event";
|
||||
import { fireEvent } from "../../../common/dom/fire_event";
|
||||
@@ -29,6 +31,12 @@ import "../../../components/ha-spinner";
|
||||
import "../../../components/ha-svg-icon";
|
||||
import "../../../components/ha-tooltip";
|
||||
import { getConfigFlowHandlers } from "../../../data/config_flow";
|
||||
import {
|
||||
apiContext,
|
||||
configContext,
|
||||
connectionContext,
|
||||
internationalizationContext,
|
||||
} from "../../../data/context";
|
||||
import { createCounter } from "../../../data/counter";
|
||||
import {
|
||||
fetchEntityRegistry,
|
||||
@@ -58,7 +66,13 @@ import {
|
||||
type FuseWeightedKey,
|
||||
} from "../../../resources/fuseMultiTerm";
|
||||
import { haStyleDialog } from "../../../resources/styles";
|
||||
import type { HomeAssistant, ValueChangedEvent } from "../../../types";
|
||||
import type {
|
||||
HomeAssistantApi,
|
||||
HomeAssistantConfig,
|
||||
HomeAssistantConnection,
|
||||
HomeAssistantInternationalization,
|
||||
ValueChangedEvent,
|
||||
} from "../../../types";
|
||||
import type {
|
||||
Helper,
|
||||
HelperCategory,
|
||||
@@ -77,7 +91,7 @@ type HelperCreators = Record<
|
||||
HelperDomain,
|
||||
{
|
||||
create: (
|
||||
hass: HomeAssistant,
|
||||
callWS: HomeAssistantApi["callWS"],
|
||||
// Not properly typed because there is currently a mismatch for this._item between:
|
||||
// 1. Type passed to form should be Helper
|
||||
// 2. Type received by creator should be MutableParams version
|
||||
@@ -147,7 +161,25 @@ const HELPER_SEARCH_KEYS: FuseWeightedKey[] = [
|
||||
export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
{ item?: HelperFormItem; area?: string } | undefined
|
||||
>()(LitElement) {
|
||||
@property({ attribute: false }) public hass!: HomeAssistant;
|
||||
@state()
|
||||
@consume({ context: internationalizationContext, subscribe: true })
|
||||
private _i18n!: HomeAssistantInternationalization;
|
||||
|
||||
@state()
|
||||
@consume({ context: configContext, subscribe: true })
|
||||
@transform<HomeAssistantConfig, HassConfig>({
|
||||
transformer: ({ config }) => config,
|
||||
})
|
||||
private _hassConfig!: HassConfig;
|
||||
|
||||
@consume({ context: apiContext, subscribe: true })
|
||||
private _api!: HomeAssistantApi;
|
||||
|
||||
@consume({ context: connectionContext, subscribe: true })
|
||||
@transform<HomeAssistantConnection, Connection>({
|
||||
transformer: ({ connection }) => connection,
|
||||
})
|
||||
private _connection!: Connection;
|
||||
|
||||
@state() private _item?: HelperFormItem;
|
||||
|
||||
@@ -203,9 +235,9 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
}
|
||||
this._open = true;
|
||||
await this.updateComplete;
|
||||
this.hass.loadFragmentTranslation("config");
|
||||
const flows = await getConfigFlowHandlers(this.hass, "helper");
|
||||
await this.hass.loadBackendTranslation("title", flows, true);
|
||||
this._i18n.loadFragmentTranslation("config");
|
||||
const flows = await getConfigFlowHandlers(this._api.callApi, "helper");
|
||||
await this._i18n.loadBackendTranslation("title", flows, true);
|
||||
// Ensure the titles are loaded before we render the flows.
|
||||
this._helperFlows = flows;
|
||||
|
||||
@@ -214,13 +246,13 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
this._focusPicker();
|
||||
}
|
||||
|
||||
protected updated(changedProps: PropertyValues<this>): void {
|
||||
protected updated(changedProps: PropertyValues): void {
|
||||
super.updated(changedProps);
|
||||
const oldHass = changedProps.get("hass");
|
||||
const oldI18n = changedProps.get("_i18n") as
|
||||
HomeAssistantInternationalization | undefined;
|
||||
if (
|
||||
oldHass &&
|
||||
(oldHass.localize !== this.hass.localize ||
|
||||
oldHass.config !== this.hass.config)
|
||||
(oldI18n && oldI18n.localize !== this._i18n.localize) ||
|
||||
changedProps.get("_hassConfig") !== undefined
|
||||
) {
|
||||
this._comboBox?.refreshItems();
|
||||
}
|
||||
@@ -272,7 +304,6 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
<div class="form" @value-changed=${this._valueChanged}>
|
||||
${this._error ? html`<div class="error">${this._error}</div>` : ""}
|
||||
${dynamicElement(`ha-${this._domain}-form`, {
|
||||
hass: this.hass,
|
||||
item: this._item,
|
||||
new: true,
|
||||
autofocus: true,
|
||||
@@ -295,7 +326,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
@click=${this._goBack}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.common.back")}
|
||||
${this._i18n.localize("ui.common.back")}
|
||||
</ha-button>`
|
||||
}
|
||||
<ha-button
|
||||
@@ -303,7 +334,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
@click=${this._createItem}
|
||||
.disabled=${this._submitting}
|
||||
>
|
||||
${this.hass!.localize("ui.panel.config.helpers.dialog.create")}
|
||||
${this._i18n.localize("ui.panel.config.helpers.dialog.create")}
|
||||
</ha-button>
|
||||
</ha-dialog-footer>
|
||||
`;
|
||||
@@ -317,16 +348,16 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
mode="dialog"
|
||||
.shown=${this._shown}
|
||||
no-sort
|
||||
.label=${this.hass.localize(
|
||||
.label=${this._i18n.localize(
|
||||
"ui.panel.config.integrations.search_helper"
|
||||
)}
|
||||
.getItems=${this._getItems}
|
||||
.rowRenderer=${this._renderHelperRow}
|
||||
.sections=${this._getSections(
|
||||
this._helperFlows,
|
||||
this.hass.localize,
|
||||
this.hass.config,
|
||||
this.hass.locale.language
|
||||
this._i18n.localize,
|
||||
this._hassConfig,
|
||||
this._i18n.locale.language
|
||||
)}
|
||||
.selectedSection=${this._category}
|
||||
@index-selected=${this._helperPicked}
|
||||
@@ -342,12 +373,12 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
.preventScrimClose=${this.isDirtyState}
|
||||
header-title=${
|
||||
this._domain
|
||||
? this.hass.localize(
|
||||
? this._i18n.localize(
|
||||
"ui.panel.config.helpers.dialog.create_platform",
|
||||
{
|
||||
platform:
|
||||
(isHelperDomain(this._domain) &&
|
||||
this.hass.localize(
|
||||
this._i18n.localize(
|
||||
`ui.panel.config.helpers.types.${
|
||||
this._domain as HelperDomain
|
||||
}`
|
||||
@@ -355,7 +386,9 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
this._domain,
|
||||
}
|
||||
)
|
||||
: this.hass.localize("ui.panel.config.helpers.dialog.create_helper")
|
||||
: this._i18n.localize(
|
||||
"ui.panel.config.helpers.dialog.create_helper"
|
||||
)
|
||||
}
|
||||
@after-show=${this._dialogShown}
|
||||
@closed=${this._dialogClosed}
|
||||
@@ -441,9 +474,9 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
this._search = search;
|
||||
const allItems = this._getHelperItems(
|
||||
this._helperFlows ?? [],
|
||||
this.hass.localize,
|
||||
this.hass.config,
|
||||
this.hass.locale.language
|
||||
this._i18n.localize,
|
||||
this._hassConfig,
|
||||
this._i18n.locale.language
|
||||
);
|
||||
const items = search
|
||||
? multiTermSortedSearch(
|
||||
@@ -463,7 +496,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
const categoryItems = items.filter((item) => item.category === category);
|
||||
return categoryItems.length
|
||||
? [
|
||||
this.hass.localize(
|
||||
this._i18n.localize(
|
||||
`ui.panel.config.helpers.dialog.categories.${category}`
|
||||
),
|
||||
...categoryItems,
|
||||
@@ -498,7 +531,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
@click=${stopPropagation}
|
||||
></ha-svg-icon>
|
||||
<ha-tooltip slot="end" .for="icon-${domain}">
|
||||
${this.hass.localize(
|
||||
${this._i18n.localize(
|
||||
"ui.dialogs.helper_settings.platform_not_loaded",
|
||||
{ platform: domain }
|
||||
)}
|
||||
@@ -523,7 +556,10 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
this._submitting = true;
|
||||
this._error = "";
|
||||
try {
|
||||
const createdEntity = await HELPERS[domain].create(this.hass, this._item);
|
||||
const createdEntity = await HELPERS[domain].create(
|
||||
this._api.callWS,
|
||||
this._item
|
||||
);
|
||||
// The dialog was closed or reopened while waiting
|
||||
if (this._params !== params) {
|
||||
return;
|
||||
@@ -565,13 +601,13 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
try {
|
||||
// The entity ID usually matches the helper ID, unless it was taken
|
||||
const entry = await getExtendedEntityRegistryEntry(
|
||||
this.hass,
|
||||
this._api,
|
||||
`${domain}.${uniqueId}`
|
||||
).catch(() => undefined);
|
||||
if (entry && isCreated(entry)) {
|
||||
return entry.entity_id;
|
||||
}
|
||||
return (await fetchEntityRegistry(this.hass.connection)).find(isCreated)
|
||||
return (await fetchEntityRegistry(this._connection)).find(isCreated)
|
||||
?.entity_id;
|
||||
} catch (_err) {
|
||||
return undefined;
|
||||
@@ -585,7 +621,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
let error = "Entity not found";
|
||||
if (entityId) {
|
||||
try {
|
||||
return (await updateEntitiesArea(this.hass, [entityId], areaId))[
|
||||
return (await updateEntitiesArea(this._api.callWS, [entityId], areaId))[
|
||||
entityId
|
||||
];
|
||||
} catch (err: unknown) {
|
||||
@@ -593,7 +629,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
}
|
||||
}
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
text: this._i18n.localize(
|
||||
"ui.panel.config.integrations.config_flow.error_saving_entity",
|
||||
{ error }
|
||||
),
|
||||
@@ -614,7 +650,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
const { id: domain, loaded } = ev.detail.item as HelperPickerItem;
|
||||
if (!loaded) {
|
||||
showAlertDialog(this, {
|
||||
text: this.hass.localize(
|
||||
text: this._i18n.localize(
|
||||
"ui.dialogs.helper_settings.platform_not_loaded",
|
||||
{ platform: domain }
|
||||
),
|
||||
@@ -637,7 +673,7 @@ export class DialogHelperDetail extends DirtyStateProviderMixin<
|
||||
} else {
|
||||
this._pendingConfigFlow = {
|
||||
startFlowHandler: domain,
|
||||
manifest: await fetchIntegrationManifest(this.hass, domain),
|
||||
manifest: await fetchIntegrationManifest(this._api, domain),
|
||||
dialogClosedCallback: this._params?.dialogClosedCallback,
|
||||
navigateToResult: this._params?.navigateToResult,
|
||||
};
|
||||
|
||||
Loaded 100 of 187 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user