Compare commits

...

172 Commits

Author SHA1 Message Date
Bram Kragten
6021bec5ee Bumped version to 20210302.1 2021-03-03 10:42:45 +01:00
Bram Kragten
7fcadc85fa Dont show config changes when user saved it (#8520) 2021-03-03 10:42:13 +01:00
Bram Kragten
5d7f971a82 Merge pull request #8517 from home-assistant/dev 2021-03-02 16:16:22 +01:00
Bram Kragten
d8cdbac15e Bumped version to 20210302.0 2021-03-02 15:49:51 +01:00
Joakim Sørensen
03b8c1348c Use localize in the Supervisor panel (#8515) 2021-03-02 14:46:30 +01:00
Bram Kragten
25a0be7672 Bump codemirror view (#8512)
Fixes search bug in Chrome
2021-03-02 10:05:16 +01:00
GitHub Action
08f1ce2d54 Translation update 2021-03-02 01:09:30 +00:00
Joakim Sørensen
bea20d0495 🌐 Add MVP for translation in the Supervisor panel (#8425)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-03-02 00:37:39 +01:00
Bram Kragten
c42430ccf9 Merge pull request #8509 from home-assistant/dev 2021-03-01 23:33:33 +01:00
Bram Kragten
5ae10e8516 Bumped version to 20210301.0 2021-03-01 23:17:24 +01:00
Philip Allgaier
e3f4a9ce5b Bump MDI icons to 5.9.55 (#8508) 2021-03-01 23:08:04 +01:00
Bram Kragten
cf1fb606fb Bump codemirror + add more styling (#8503) 2021-03-01 22:55:02 +01:00
Bram Kragten
54ec81b67d Fix en-gb translations (#8502)
Renamed lang key in lokalise
2021-03-01 22:48:36 +01:00
Philip Allgaier
f2a9725572 Make spelling more consistent (#8507) 2021-03-01 22:41:52 +01:00
Bram Kragten
4765114e80 Allow decimal slider steps (#8501) 2021-03-01 18:41:36 +01:00
Bram Kragten
5ff757ad65 Handle reconnect while in raw edit (#8500) 2021-03-01 16:55:39 +01:00
David F. Mulcahey
1642c68493 Add view in visualization button to the device page for ZHA devices (#8090) 2021-03-01 15:54:49 +01:00
Philip Allgaier
f31f10cea9 Take cover "opening" and "closing" into account (#8490) 2021-03-01 12:58:12 +01:00
Philip Allgaier
76e0bbb55d Make section row text color themeable (#8488) 2021-03-01 12:56:56 +01:00
Joakim Sørensen
f43af9c0a5 Show config if options or schema (#8487) 2021-03-01 12:55:14 +01:00
Paulus Schoutsen
f7a3d2705c Preserve url params in redirect uri (#8495) 2021-03-01 12:54:39 +01:00
Joakim Sørensen
22c8af0cc5 Fix add-on store search (#8479) 2021-03-01 12:41:55 +01:00
Joakim Sørensen
f263a5221d Adjust header and wording in update dialogs (#8476) 2021-03-01 12:40:52 +01:00
Bram Kragten
3834ab8ede Service dev tools: Add service picker to YAML mode (#8482) 2021-03-01 11:09:15 +01:00
GitHub Action
e2e167630d Translation update 2021-03-01 01:25:12 +00:00
GitHub Action
01dd44300b Translation update 2021-02-28 01:24:14 +00:00
Bram Kragten
b30160d671 Fix device picker (#8481) 2021-02-27 21:20:28 +01:00
GitHub Action
f44d505b41 Translation update 2021-02-27 01:20:54 +00:00
Bram Kragten
16fa6904d9 Merge pull request #8474 from home-assistant/dev 2021-02-26 22:05:19 +01:00
Bram Kragten
b58c17e75e make setup.py quite 2021-02-26 22:03:32 +01:00
Bram Kragten
ae590d42dc Bumped version to 20210226.0 2021-02-26 21:39:43 +01:00
Bram Kragten
d7917160c0 Update translations 2021-02-26 21:39:28 +01:00
Joakim Sørensen
01e4414d17 Ignore error if we are not connected (#8472) 2021-02-26 21:37:06 +01:00
Joakim Sørensen
0bc2eb530d Remove closing event from dialog (#8470) 2021-02-26 18:14:55 +01:00
Bram Kragten
12b124e5a3 Add search, history to codemirror (#8469)
And prevent jump on focus
2021-02-26 18:01:48 +01:00
Joakim Sørensen
478a4b2593 Add snapshot to core update dialogs (#8468) 2021-02-26 15:07:29 +01:00
Joakim Sørensen
9752e30eb4 Add snapshot to add-on update dialog. (#8463) 2021-02-26 14:44:27 +01:00
Joakim Sørensen
af6e87ba31 Fix messaging when addon is not available (#8454) 2021-02-26 14:15:36 +01:00
Bram Kragten
64d390ad0f Fix wrong tag component (#8451)
Will rename the keys in Lokalise after merge
2021-02-26 14:01:16 +01:00
Joakim Sørensen
c94bcb6896 Subscribe to message instead of event (#8443) 2021-02-26 13:47:48 +01:00
Joakim Sørensen
97f9df2f2d Add toggle to show_hide optional fields in add-on config (#8430) 2021-02-26 13:47:08 +01:00
GitHub Action
4e7f68a86c Translation update 2021-02-26 01:21:07 +00:00
Philip Allgaier
2f7f677549 Restore previous codemirror tab behavior (#8461)
* Restore previous tab behavior

* Handle via ondemand logic

* Combine imports
2021-02-25 22:24:07 +01:00
Bram Kragten
12a8a1531d Merge pull request #8459 from home-assistant/dev 2021-02-25 18:54:46 +01:00
Bram Kragten
f44d867d3a Bumped version to 20210225.0 2021-02-25 18:40:34 +01:00
Bram Kragten
6f636187f7 Clean translations (#8458) 2021-02-25 17:18:38 +01:00
Marc Randolph
9414f89e50 Add theme variables for text of picture cards (#8022) 2021-02-25 16:47:33 +01:00
Bram Kragten
60bf1a5451 Fix integrations page (#8457) 2021-02-25 15:59:04 +01:00
Philip Allgaier
32ba8f4731 Make clear that automation run button skips conditions + remove word "execute" from UI (#8259)
* Do not skip conditions when triggering an automation

* Remove usage of word "execute"

* More concise function names
2021-02-25 14:17:31 +01:00
Philip Allgaier
81f96de2bd Fix codemirror caret color (#8452) 2021-02-25 13:49:42 +01:00
Jesse Hills
0c417755ed Fix my redirect for tags (#8450) 2021-02-25 12:03:03 +01:00
GitHub Action
93e5bde797 Translation update 2021-02-25 01:20:40 +00:00
Bram Kragten
c85f69c9ee Merge pull request #8448 from home-assistant/dev
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: GitHub Action <github-action@users.noreply.github.com>
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
Co-authored-by: Álvaro Fernández Rojas <noltari@gmail.com>
Co-authored-by: Marc Mueller <30130371+cdce8p@users.noreply.github.com>
Co-authored-by: Kendell R <KTibow@users.noreply.github.com>
Co-authored-by: larena1 <60823161+larena1@users.noreply.github.com>
2021-02-24 20:36:51 +01:00
Bram Kragten
b6eaf0a7c5 Fix setting service data on load when in yaml mode 2021-02-24 20:17:31 +01:00
Bram Kragten
5f1851bade Bumped version to 20210224.0 2021-02-24 20:06:20 +01:00
Bram Kragten
5c66a02711 My redirects tweaks (#8447) 2021-02-24 20:05:40 +01:00
Bram Kragten
bde925a0e3 Migrate to codemirror 6 (#8382) 2021-02-24 19:16:54 +01:00
larena1
0f574a765b Fix excessive rerendering of history charts (#8340)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-02-24 17:48:50 +01:00
Kendell R
782b941531 Save attribute checkbox state (#8010) 2021-02-24 17:44:37 +01:00
Kendell R
f42c0a0717 Add clipboard button (#8411) 2021-02-24 17:36:18 +01:00
Marc Mueller
13ac14d449 Add additional weblink attributes (#8295) 2021-02-24 17:34:44 +01:00
Philip Allgaier
db9cea81db Correctly color script state icon + handle "single" mode for cancel buttons (#8383) 2021-02-24 17:18:38 +01:00
Bram Kragten
7c1fd542da Allow to disable config entry (#8442) 2021-02-24 17:10:59 +01:00
Joakim Sørensen
54a2b2534a Add add-on selector/picker (#8422) 2021-02-24 17:05:42 +01:00
Álvaro Fernández Rojas
f5fb6c1e03 Support binary sensor batteries (#8367) 2021-02-24 17:00:07 +01:00
Bram Kragten
781c0701fc Show correct fields in UI mode (#8445) 2021-02-24 14:27:00 +01:00
GitHub Action
742f1f85dc Translation update 2021-02-24 01:20:56 +00:00
Joakim Sørensen
a648e9be49 Fix atLeastVersion (#8437)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-02-23 22:17:22 +01:00
Joakim Sørensen
fd9441dde2 Fix blank page in ingress when resizing window (#8439) 2021-02-23 16:16:24 +01:00
Philip Allgaier
b5ec59c396 Dev-tools service: Tweak to target description (#8434) 2021-02-23 16:10:15 +01:00
Bram Kragten
60e4594abd Fix area picker with both entity and device filter (#8438) 2021-02-23 15:07:45 +01:00
GitHub Action
79692ef58a Translation update 2021-02-23 01:19:57 +00:00
J. Nick Koston
ace7ee5622 Add support for percentage step size to fans (#8393) 2021-02-22 15:59:59 -06:00
Philip Allgaier
741ac679a0 Ensure we have all mandatory action keys present in action editor (#8424) 2021-02-22 21:10:06 +01:00
Bram Kragten
216526e391 Merge pull request #8433 from home-assistant/dev 2021-02-22 20:18:48 +01:00
Bram Kragten
d76af2cb61 Bumped version to 20210222.0 2021-02-22 20:06:30 +01:00
Bram Kragten
b7d4c40736 Show flows in progress when picking a handler (#8368) 2021-02-22 20:06:18 +01:00
Bram Kragten
6092af8de6 Re-do developer tools service (#8410) 2021-02-22 19:53:52 +01:00
Bram Kragten
627424b8b9 Migrate mfa to Lit (#8276)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-02-22 19:53:37 +01:00
Joakim Sørensen
e33aff7cf3 Fix mouseevent in blueprint import popup (#8432) 2021-02-22 17:56:43 +01:00
Joakim Sørensen
ef0bfb237a bump webpack-manifest-plugin to 3.0.0 (#8426) 2021-02-22 10:30:02 +01:00
Joakim Sørensen
c042c5568b Fix WS command for validating ingress session (#8427) 2021-02-22 10:28:18 +01:00
GitHub Action
d84a7ee358 Translation update 2021-02-22 01:20:19 +00:00
GitHub Action
8bfc8ece9d Translation update 2021-02-21 01:21:30 +00:00
GitHub Action
2d3cf7d84d Translation update 2021-02-20 01:18:25 +00:00
Franck Nijhof
520ef8f1df Update GitHub Issue Form template (#8423) 2021-02-19 22:13:27 +01:00
Bram Kragten
f251d4267f Revert "Allow viewport scaling (zooming) of frontend" (#8353)
This reverts commit da9faccada.
2021-02-19 18:06:36 +01:00
Bram Kragten
2052a5351c Ha-form: Don't change data (#8277) 2021-02-19 18:03:31 +01:00
Bram Kragten
9807d0aede Move localizing to render (#8419) 2021-02-19 18:02:25 +01:00
Bram Kragten
a41afcd714 Update lovelace call service action (#8421) 2021-02-19 17:58:14 +01:00
Bram Kragten
d93d2b5945 Fix password field in ha-form (#8400) 2021-02-19 17:47:51 +01:00
Bram Kragten
d54a129605 Bump marked (#8420) 2021-02-19 17:46:33 +01:00
Philip Allgaier
77911980cb Correctly handle seconds in top "delay" key (#8415)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-02-19 11:03:25 +01:00
Bram Kragten
d51fd1e2f9 Add supervisor_logs and supervisor_info redirects (#8417) 2021-02-19 10:01:21 +01:00
GitHub Action
fe54f8eb16 Translation update 2021-02-19 01:18:52 +00:00
Bram Kragten
fc7c4af27a Add more redirects (#8413) 2021-02-18 20:35:16 +01:00
Joakim Sørensen
09e7600d86 Use websockets (#8403)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-02-18 18:18:05 +01:00
GitHub Action
17410874e3 Translation update 2021-02-18 01:18:34 +00:00
GitHub Action
03d4174163 Translation update 2021-02-17 01:18:01 +00:00
Bram Kragten
99eff73b0d Add support for target to automation call service action (#8372) 2021-02-16 21:46:47 +01:00
Joakim Sørensen
acefa39796 Update supervisor info on addon action (#8404) 2021-02-16 21:38:23 +01:00
Joakim Sørensen
c01c0528a6 Show options if no options and schema (#8408) 2021-02-16 21:25:59 +01:00
Bram Kragten
0ec58007c9 Add my support to supervisor (#8405)
* Add my support to supervisor

* Remove localize

* Comments

* Update ha-panel-my.ts
2021-02-16 19:50:35 +01:00
GitHub Action
e8daf88729 Translation update 2021-02-16 01:18:09 +00:00
Matteo Agnoletto
ab74c7f7eb Add select selector for blueprints (#8297) 2021-02-15 10:22:00 +01:00
GitHub Action
6b673c7f44 Translation update 2021-02-15 01:18:50 +00:00
GitHub Action
53510a3cb9 Translation update 2021-02-14 01:19:47 +00:00
GitHub Action
d4d38a880d Translation update 2021-02-13 01:17:10 +00:00
GitHub Action
18783d5e3b Translation update 2021-02-12 01:17:41 +00:00
Philip Allgaier
eb235cb552 Add bottom margin to button card icon (#8362) 2021-02-11 13:39:31 +01:00
GitHub Action
435a6b6d53 Translation update 2021-02-11 01:17:07 +00:00
GitHub Action
8d13745c6b Translation update 2021-02-10 01:16:43 +00:00
Franck Nijhof
14c7cfc64c Add GitHub Issue Form (#8363) 2021-02-09 18:15:57 +01:00
Joakim Sørensen
c7821b9cee Don't show add-on config if no schema (#8361) 2021-02-09 11:51:46 +01:00
GitHub Action
a1d66aef0c Translation update 2021-02-09 01:17:05 +00:00
Jaroslav Hanslík
e275f1f4b9 Fixed state card of number entity (#8325) 2021-02-08 16:28:28 +01:00
Joakim Sørensen
48de8b0739 Block snapshots when system is not running (#8350) 2021-02-08 16:18:33 +01:00
Joakim Sørensen
b75dc0efe0 Fix issue with jumping config (#8355) 2021-02-08 16:18:01 +01:00
Paulus Schoutsen
1d498349c5 Update container port (#8352)
* Update container port

* Update .devcontainer/devcontainer.json

Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-02-08 16:09:40 +01:00
Bram Kragten
311e1cfb00 Merge pull request #8354 from home-assistant/dev 2021-02-08 15:28:40 +01:00
Bram Kragten
5cdcec699b Merge branch 'master' into dev 2021-02-08 15:14:36 +01:00
Bram Kragten
cd72287d99 Bumped version to 20210208.0 2021-02-08 15:12:42 +01:00
Bram Kragten
c8717bfa32 Add my panel (#8349) 2021-02-08 14:48:54 +01:00
GitHub Action
83de75b689 Translation update 2021-02-08 01:17:33 +00:00
Philip Allgaier
e5ea762cbc Resolve merge conflict from PR #8121 2021-02-07 16:36:25 +01:00
Philip Allgaier
01df01cd66 Provide stub config for entity-filter (#8121)
* Provide stub config for entity-filter

* "card" option is optional since it has a default

* Search dynamically for stub config entities
2021-02-07 14:38:54 +01:00
Philip Allgaier
2c07a2c825 Correct typo in "find-entities.ts" file name (#8343) 2021-02-07 14:37:35 +01:00
chriss158
c3f50ba0fb Fix no disconnect after 5 minute timeout (#8339) 2021-02-07 14:33:44 +01:00
GitHub Action
c04419fd09 Translation update 2021-02-07 01:18:53 +00:00
Paulus Schoutsen
9c7af0dfce Drop margin from cast header (#8331) 2021-02-06 23:00:06 +01:00
GitHub Action
b66d14e980 Translation update 2021-02-06 01:15:59 +00:00
GitHub Action
6a553e9554 Translation update 2021-02-05 01:17:26 +00:00
Joakim Sørensen
4273b72d71 Fix issue where schema is null (#8322)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2021-02-04 14:01:53 +01:00
GitHub Action
9ccfa79199 Translation update 2021-02-04 01:16:30 +00:00
Tobias Sauerwein
fe3d22d4f8 Only display current temp when not None (#8316)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-02-03 20:51:28 +01:00
Joakim Sørensen
e06642e892 Show the reason why an add-on is not available (#8312)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-02-03 17:35:55 +01:00
Joakim Sørensen
5199e946a1 Fix button layout for addon-info (#8315) 2021-02-03 16:08:29 +01:00
Joakim Sørensen
17aff2f9b8 Move save button to the right (#8314) 2021-02-03 15:58:17 +01:00
Joakim Sørensen
f7c7ac44f7 Show eMMC lifetime (#8302) 2021-02-03 15:52:52 +01:00
Joakim Sørensen
62dd0a561e Fix display issue wtih addon-info grid (#8313) 2021-02-03 15:45:01 +01:00
GitHub Action
858eacddea Translation update 2021-02-03 01:23:54 +00:00
Bram Kragten
471bb5169c Bumped version to 20210127.7 2021-02-02 21:24:52 +01:00
Bram Kragten
9d89aa329c Revert "Add icon support to gauge" (#8303)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-02-02 21:23:08 +01:00
Bram Kragten
4e4d8bdc5e Revert "Add icon support to gauge" (#8303)
Co-authored-by: Joakim Sørensen <joasoe@gmail.com>
2021-02-02 21:17:38 +01:00
GitHub Action
a30ec32ac1 Translation update 2021-02-02 01:31:33 +00:00
Bram Kragten
d79e5dd8fb Bumped version to 20210127.6 2021-01-30 22:51:57 +01:00
Philip Allgaier
92b116c0da More precise name handling for auto-generated dashboards (#8289)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2021-01-30 22:51:46 +01:00
Bram Kragten
da3f911deb Fix tts try on ios (#8292) 2021-01-30 22:51:32 +01:00
Philip Allgaier
9d82ce8ab4 Add missing device_classes to sensor (#8288) 2021-01-30 22:51:13 +01:00
Bram Kragten
db9597d2e7 Don't use badges in generated Lovelace + group entities by area (#8291) 2021-01-30 22:50:56 +01:00
Bram Kragten
8ea6baaf5d Bumped version to 20210127.5 2021-01-29 18:38:36 +01:00
Bram Kragten
1ed03842c0 Fix grid + map editor (#8284) 2021-01-29 18:38:25 +01:00
Philip Allgaier
362b419814 Add missing extra field translation for cover position (#8273)
* Ensure ha-form-integer passes "0" to form data

* Only keep the translation change
2021-01-29 18:14:07 +01:00
chriss158
bffcccc1fe Fix external auth reconnection loop if connection lost after refresh token expiration (#8279) 2021-01-29 18:13:48 +01:00
Bram Kragten
b8e9a4ce9f Fix map editor (#8280) 2021-01-29 18:13:26 +01:00
Bram Kragten
bdff3fd452 Z-wave migration tweaks (#8283) 2021-01-29 18:11:25 +01:00
Bram Kragten
1fc51f0087 Bumped version to 20210127.4 2021-01-29 18:10:58 +01:00
Bram Kragten
9a088a21da Bumped version to 20210127.3 2021-01-28 22:35:49 +01:00
Bram Kragten
1160d27004 Revert "Bumped version to 20210127.2"
This reverts commit 3766f44787.
2021-01-28 22:34:31 +01:00
Bram Kragten
b4e5740050 Fix race condition in zwave migration (#8268) 2021-01-28 20:59:54 +01:00
Bram Kragten
12bb3f5796 Use close dialog function to close device registry detail dialog (#8269) 2021-01-28 20:59:37 +01:00
Bram Kragten
ff62fdb69d hide config links in demo (#8267) 2021-01-28 20:59:15 +01:00
Bram Kragten
4ebf32cb1f Move try tss button to bottom (#8266) 2021-01-28 20:58:59 +01:00
Thomas Lovén
5afb8a77a9 Make input_text entity row usable when value is "unknown" (#8258) 2021-01-28 20:58:43 +01:00
Jaroslav Hanslík
48ed33af95 Typo in texts (#8265) 2021-01-28 20:58:26 +01:00
Jaroslav Hanslík
4a64cd4464 Typo in texts (#8264) 2021-01-28 20:58:12 +01:00
Paulus Schoutsen
8ae1a1b558 Fix tts (#8261) 2021-01-28 20:57:56 +01:00
Philip Allgaier
ef1dd8b761 Add check to prevent undefined access during action validation (#8257) 2021-01-28 20:57:41 +01:00
Bram Kragten
3766f44787 Bumped version to 20210127.2 2021-01-28 20:57:15 +01:00
Bram Kragten
178605664e Bumped version to 20210127.1 2021-01-27 17:17:48 +01:00
Joakim Sørensen
0cf8004b8d Add twine to release flow (#8254) 2021-01-27 17:14:00 +01:00
Bram Kragten
00412c7216 Merge pull request #8252 from home-assistant/dev 2021-01-27 16:24:07 +01:00
275 changed files with 14640 additions and 21602 deletions

View File

@@ -4,7 +4,7 @@
"dockerfile": "Dockerfile",
"context": ".."
},
"appPort": 8123,
"appPort": "8124:8123",
"context": "..",
"postCreateCommand": "script/bootstrap",
"extensions": [

138
.github/ISSUE_TEMPLATE/bug_report.yml vendored Normal file
View File

@@ -0,0 +1,138 @@
name: Report a bug with the UI, Frontend or Lovelace
about: Report an issue related to the Home Assistant frontend.
labels: bug
title: ""
issue_body: true
body:
- type: markdown
attributes:
value: |
Make sure you are running the [latest version of Home Assistant][releases] before reporting an issue.
If you have a feature or enhancement request for the frontend, please [start an discussion][fr] instead of creating an issue.
**Please not not report issues for custom Lovelace cards.**
[fr]: https://github.com/home-assistant/frontend/discussions
[releases]: https://github.com/home-assistant/home-assistant/releases
- type: checkboxes
attributes:
label: Checklist
description: Please verify that you've followed these steps
options:
- label: I have updated to the latest available Home Assistant version.
required: true
- label: I have cleared the cache of my browser.
required: true
- label: I have tried a different browser to see if it is related to my browser.
required: true
- type: markdown
attributes:
value: |
## The problem
- type: textarea
validations:
required: true
attributes:
label: Describe the issue you are experiencing
description: Provide a clear and concise description of what the bug is.
- type: textarea
validations:
required: true
attributes:
label: Describe the behavior you expected
description: Describe what you expected to happen or it should look/behave.
- type: textarea
validations:
required: true
attributes:
label: Steps to reproduce the issue
description: |
Please tell us exactly how to reproduce your issue.
Provide clear and concise step by step instructions and add code snippets if needed.
value: |
1.
2.
3.
...
- type: markdown
attributes:
value: |
## Environment
- type: input
validations:
required: true
attributes:
label: What version of Home Assistant Core has the issue?
placeholder: core-
description: >
Can be found in the Configuration panel -> Info.
- type: input
attributes:
label: What was the last working version of Home Assistant Core?
placeholder: core-
description: >
If known, otherwise leave blank.
- type: input
attributes:
label: In which browser are you experiencing the issue with?
placeholder: Google Chrome 88.0.4324.150
description: >
Provide the full name and don't forget to add the version!
- type: input
attributes:
label: Which operating system are you using to run this browser?
placeholder: macOS Big Sur (1.11)
description: >
Don't forget to add the version!
- type: markdown
attributes:
value: |
# Details
- type: textarea
attributes:
label: State of relevant entities
description: >
If your issue is about how an entity is shown in the UI, please add the
state and attributes for all situations. You can find this information
at Developer Tools -> States.
value: |
```yaml
# Paste your state here.
```
- type: textarea
attributes:
label: Problem-relevant frontend configuration
description: >
An example configuration that caused the problem for you, e.g., the YAML
configuration of the used cards. Fill this out even if it seems
unimportant to you. Please be sure to remove personal information like
passwords, private URLs and other credentials.
value: |
```yaml
# Paste your YAML here.
```
- type: textarea
attributes:
label: Javascript errors shown in your browser console/inspector
description: >
If you come across any Javascript or other error logs, e.g., in your
browser console/inspector please provide them.
value: |
```txt
# Paste your logs here.
```
- type: markdown
attributes:
value: |
## Additional information
- type: markdown
attributes:
value: |
If you have any additional information for us, use the field below.
Please note, you can attach screenshots or screen recordings here,
by dragging and dropping files in the field below.

View File

@@ -85,6 +85,11 @@ gulp.task("copy-translations-app", async () => {
copyTranslations(staticDir);
});
gulp.task("copy-translations-supervisor", async () => {
const staticDir = paths.hassio_output_static;
copyTranslations(staticDir);
});
gulp.task("copy-static-app", async () => {
const staticDir = paths.app_output_static;
// Basic static files

View File

@@ -10,6 +10,8 @@ require("./gen-icons-json.js");
require("./webpack.js");
require("./compress.js");
require("./rollup.js");
require("./gather-static.js");
require("./translations.js");
gulp.task(
"develop-hassio",
@@ -20,6 +22,8 @@ gulp.task(
"clean-hassio",
"gen-icons-json",
"gen-index-hassio-dev",
"build-supervisor-translations",
"copy-translations-supervisor",
env.useRollup() ? "rollup-watch-hassio" : "webpack-watch-hassio"
)
);
@@ -32,6 +36,8 @@ gulp.task(
},
"clean-hassio",
"gen-icons-json",
"build-supervisor-translations",
"copy-translations-supervisor",
env.useRollup() ? "rollup-prod-hassio" : "webpack-prod-hassio",
"gen-index-hassio-prod",
...// Don't compress running tests

View File

@@ -266,6 +266,7 @@ gulp.task(taskName, function () {
TRANSLATION_FRAGMENTS.forEach((fragment) => {
delete data.ui.panel[fragment];
});
delete data.supervisor;
return data;
})
)
@@ -342,6 +343,62 @@ gulp.task(
}
);
gulp.task("build-translation-fragment-supervisor", function () {
return gulp
.src(fullDir + "/*.json")
.pipe(transform((data) => data.supervisor))
.pipe(gulp.dest(workDir + "/supervisor"));
});
gulp.task("build-translation-flatten-supervisor", function () {
return gulp
.src(workDir + "/supervisor/*.json")
.pipe(
transform(function (data) {
// Polymer.AppLocalizeBehavior requires flattened json
return flatten(data);
})
)
.pipe(gulp.dest(outDir));
});
gulp.task("build-translation-write-metadata", function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (value.nativeName) {
newData[key] = value;
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
});
gulp.task(
"build-translations",
gulp.series(
@@ -353,42 +410,20 @@ gulp.task(
gulp.parallel(...splitTasks),
"build-flattened-translations",
"build-translation-fingerprints",
function writeMetadata() {
return gulp
.src(
[
path.join(paths.translations_src, "translationMetadata.json"),
workDir + "/testMetadata.json",
workDir + "/translationFingerprints.json",
],
{ allowEmpty: true }
)
.pipe(merge({}))
.pipe(
transform(function (data) {
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})
)
.pipe(
transform((data) => ({
fragments: TRANSLATION_FRAGMENTS,
translations: data,
}))
)
.pipe(rename("translationMetadata.json"))
.pipe(gulp.dest(workDir));
}
"build-translation-write-metadata"
)
);
gulp.task(
"build-supervisor-translations",
gulp.series(
"clean-translations",
"ensure-translations-build-dir",
"build-master-translation",
"build-merged-translations",
"build-translation-fragment-supervisor",
"build-translation-flatten-supervisor",
"build-translation-fingerprints",
"build-translation-write-metadata"
)
);

View File

@@ -137,7 +137,12 @@ gulp.task("webpack-watch-hassio", () => {
isProdBuild: false,
latestBuild: true,
})
).watch({}, doneHandler());
).watch({ ignored: /build-translations/ }, doneHandler());
gulp.watch(
path.join(paths.translations_src, "en.json"),
gulp.series("build-supervisor-translations", "copy-translations-supervisor")
);
});
gulp.task("webpack-prod-hassio", () =>

View File

@@ -34,6 +34,7 @@ module.exports = {
hassio_dir: path.resolve(__dirname, "../hassio"),
hassio_output_root: path.resolve(__dirname, "../hassio/build"),
hassio_output_static: path.resolve(__dirname, "../hassio/build/static"),
hassio_output_latest: path.resolve(
__dirname,
"../hassio/build/frontend_latest"

View File

@@ -1,7 +1,7 @@
const webpack = require("webpack");
const path = require("path");
const TerserPlugin = require("terser-webpack-plugin");
const ManifestPlugin = require("webpack-manifest-plugin");
const { WebpackManifestPlugin } = require("webpack-manifest-plugin");
const paths = require("./paths.js");
const bundle = require("./bundle");
const log = require("fancy-log");
@@ -68,7 +68,7 @@ const createWebpackConfig = ({
],
},
plugins: [
new ManifestPlugin({
new WebpackManifestPlugin({
// Only include the JS of entrypoints
filter: (file) => file.isInitial && !file.name.endsWith(".map"),
}),

View File

@@ -48,7 +48,7 @@ class HcCast extends LitElement {
protected render(): TemplateResult {
if (this.lovelaceConfig === undefined) {
return html` <hass-loading-screen no-toolbar></hass-loading-screen>> `;
return html`<hass-loading-screen no-toolbar></hass-loading-screen>`;
}
const error =

View File

@@ -98,8 +98,12 @@ class HcLayout extends LitElement {
line-height: 32px;
padding: 24px 16px 16px;
display: block;
margin: 0;
}
.hero {
border-radius: 4px 4px 0 0;
}
.subtitle {
font-size: 14px;
color: var(--secondary-text-color);

View File

@@ -15,6 +15,7 @@ import {
HassioAddonInfo,
HassioAddonRepository,
} from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content";
import { filterAndSort } from "../components/hassio-filter-addons";
@@ -23,6 +24,8 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioAddonRepositoryEl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public repo!: HassioAddonRepository;
@property({ attribute: false }) public addons!: HassioAddonInfo[];
@@ -54,7 +57,11 @@ class HassioAddonRepositoryEl extends LitElement {
return html`
<div class="content">
<p class="description">
No results found in "${repo.name}."
${this.supervisor.localize(
"store.no_results_found",
"repository",
repo.name
)}
</p>
</div>
`;
@@ -83,11 +90,13 @@ class HassioAddonRepositoryEl extends LitElement {
: mdiPuzzle}
.iconTitle=${addon.installed
? addon.update_available
? "New version available"
: "Add-on is installed"
? this.supervisor.localize(
"common.new_version_available"
)
: this.supervisor.localize("addon.installed")
: addon.available
? "Add-on is not installed"
: "Add-on is not available on your system"}
? this.supervisor.localize("addon.not_installed")
: this.supervisor.localize("addon.not_available")}
.iconClass=${addon.installed
? addon.update_available
? "update"

View File

@@ -11,19 +11,18 @@ import {
PropertyValues,
} from "lit-element";
import { html, TemplateResult } from "lit-html";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/common/search/search-input";
import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-svg-icon";
import {
fetchHassioAddonsInfo,
HassioAddonInfo,
HassioAddonRepository,
reloadHassioAddons,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioSupervisorInfo } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import { HomeAssistant, Route } from "../../../src/types";
@@ -51,58 +50,43 @@ const sortRepos = (a: HassioAddonRepository, b: HassioAddonRepository) => {
class HassioAddonStore extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) private _addons?: HassioAddonInfo[];
@property({ attribute: false }) private _repos?: HassioAddonRepository[];
@internalProperty() private _filter?: string;
public async refreshData() {
this._repos = undefined;
this._addons = undefined;
this._filter = undefined;
await reloadHassioAddons(this.hass);
await this._loadData();
}
protected render(): TemplateResult {
const repos: TemplateResult[] = [];
let repos: TemplateResult[] = [];
if (this._repos) {
for (const repo of this._repos) {
const addons = this._addons!.filter(
(addon) => addon.repository === repo.slug
);
if (addons.length === 0) {
continue;
}
repos.push(html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${addons}
.filter=${this._filter!}
></hassio-addon-repository>
`);
}
if (this.supervisor.addon.repositories) {
repos = this.addonRepositories(
this.supervisor.addon.repositories,
this.supervisor.addon.addons,
this._filter
);
}
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.route=${this.route}
hassio
main-page
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Add-on Store</span>
<span slot="header">
${this.supervisor.localize("panel.store")}
</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@@ -112,15 +96,15 @@ class HassioAddonStore extends LitElement {
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
Repositories
${this.supervisor.localize("store.repositories")}
</mwc-list-item>
<mwc-list-item>
Reload
${this.supervisor.localize("common.reload")}
</mwc-list-item>
${this.hass.userData?.showAdvanced &&
atLeastVersion(this.hass.config.version, 0, 117)
? html`<mwc-list-item>
Registries
${this.supervisor.localize("store.registries")}
</mwc-list-item>`
: ""}
</ha-button-menu>
@@ -141,11 +125,9 @@ class HassioAddonStore extends LitElement {
${!this.hass.userData?.showAdvanced
? html`
<div class="advanced">
Missing add-ons? Enable advanced mode on
<a href="/profile" target="_top">
your profile page
${this.supervisor.localize("store.missing_addons")}
</a>
.
</div>
`
: ""}
@@ -159,6 +141,32 @@ class HassioAddonStore extends LitElement {
this._loadData();
}
private addonRepositories = memoizeOne(
(
repositories: HassioAddonRepository[],
addons: HassioAddonInfo[],
filter?: string
) => {
return repositories.sort(sortRepos).map((repo) => {
const filteredAddons = addons.filter(
(addon) => addon.repository === repo.slug
);
return filteredAddons.length !== 0
? html`
<hassio-addon-repository
.hass=${this.hass}
.repo=${repo}
.addons=${filteredAddons}
.filter=${filter!}
.supervisor=${this.supervisor}
></hassio-addon-repository>
`
: html``;
});
}
);
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
@@ -181,28 +189,20 @@ class HassioAddonStore extends LitElement {
private async _manageRepositories() {
showRepositoriesDialog(this, {
repos: this._repos!,
supervisor: this.supervisor,
loadData: () => this._loadData(),
});
}
private async _manageRegistries() {
showRegistriesDialog(this);
showRegistriesDialog(this, { supervisor: this.supervisor });
}
private async _loadData() {
try {
const [addonsInfo, supervisor] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
]);
fireEvent(this, "supervisor-update", { supervisor });
this._repos = addonsInfo.repositories;
this._repos.sort(sortRepos);
this._addons = addonsInfo.addons;
} catch (err) {
alert(extractApiErrorMessage(err));
}
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
}
private async _filterChanged(e) {

View File

@@ -25,6 +25,7 @@ import {
fetchHassioHardwareAudio,
HassioHardwareAudioDevice,
} from "../../../../src/data/hassio/hardware";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -34,6 +35,8 @@ import { hassioStyle } from "../../resources/hassio-style";
class HassioAddonAudio extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -48,12 +51,16 @@ class HassioAddonAudio extends LitElement {
protected render(): TemplateResult {
return html`
<ha-card header="Audio">
<ha-card
.header=${this.supervisor.localize("addon.configuration.audio.header")}
>
<div class="card-content">
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<paper-dropdown-menu
label="Input"
.label=${this.supervisor.localize(
"addon.configuration.audio.input"
)}
@iron-select=${this._setInputDevice}
>
<paper-listbox
@@ -64,15 +71,17 @@ class HassioAddonAudio extends LitElement {
${this._inputDevices &&
this._inputDevices.map((item) => {
return html`
<paper-item device=${item.device || ""}
>${item.name}</paper-item
>
<paper-item device=${item.device || ""}>
${item.name}
</paper-item>
`;
})}
</paper-listbox>
</paper-dropdown-menu>
<paper-dropdown-menu
label="Output"
.label=${this.supervisor.localize(
"addon.configuration.audio.output"
)}
@iron-select=${this._setOutputDevice}
>
<paper-listbox
@@ -93,7 +102,7 @@ class HassioAddonAudio extends LitElement {
</div>
<div class="card-actions">
<ha-progress-button @click=${this._saveSettings}>
Save
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -152,7 +161,7 @@ class HassioAddonAudio extends LitElement {
const noDevice: HassioHardwareAudioDevice = {
device: "default",
name: "Default",
name: this.supervisor.localize("addon.configuration.audio.default"),
};
try {
@@ -189,7 +198,7 @@ class HassioAddonAudio extends LitElement {
try {
await setHassioAddonOption(this.hass, this.addon.slug, data);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch {
this._error = "Failed to set addon audio device";

View File

@@ -9,6 +9,7 @@ import {
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -20,26 +21,28 @@ import "./hassio-addon-network";
class HassioAddonConfigDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
}
const hasOptions =
this.addon.options && Object.keys(this.addon.options).length;
const hasSchema =
this.addon.schema && Object.keys(this.addon.schema).length;
const hasConfiguration =
(this.addon.options && Object.keys(this.addon.options).length) ||
(this.addon.schema && Object.keys(this.addon.schema).length);
return html`
<div class="content">
${hasOptions || hasSchema || this.addon.network || this.addon.audio
${hasConfiguration || this.addon.network || this.addon.audio
? html`
${hasOptions || hasSchema
${hasConfiguration
? html`
<hassio-addon-config
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-config>
`
: ""}
@@ -48,6 +51,7 @@ class HassioAddonConfigDashboard extends LitElement {
<hassio-addon-network
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-network>
`
: ""}
@@ -56,11 +60,12 @@ class HassioAddonConfigDashboard extends LitElement {
<hassio-addon-audio
.hass=${this.hass}
.addon=${this.addon}
.supervisor=${this.supervisor}
></hassio-addon-audio>
`
: ""}
`
: "This add-on does not expose configuration for you to mess with.... 👋"}
: this.supervisor.localize("addon.configuration.no_configuration")}
</div>
`;
}

View File

@@ -15,11 +15,15 @@ import {
query,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/buttons/ha-progress-button";
import "../../../../src/components/ha-button-menu";
import "../../../../src/components/ha-card";
import "../../../../src/components/ha-form/ha-form";
import type { HaFormSchema } from "../../../../src/components/ha-form/ha-form";
import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-switch";
import "../../../../src/components/ha-yaml-editor";
import type { HaYamlEditor } from "../../../../src/components/ha-yaml-editor";
import {
@@ -28,6 +32,7 @@ import {
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
@@ -42,12 +47,16 @@ class HassioAddonConfig extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) private _configHasChanged = false;
@property({ type: Boolean }) private _valid = true;
@internalProperty() private _canShowSchema = false;
@internalProperty() private _showOptional = false;
@internalProperty() private _error?: string;
@internalProperty() private _options?: Record<string, unknown>;
@@ -56,33 +65,70 @@ class HassioAddonConfig extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
public computeLabel = (entry: HaFormSchema): string => {
return (
this.addon.translations[this.hass.language]?.configuration?.[entry.name]
?.name ||
this.addon.translations.en?.configuration?.[entry.name].name ||
entry.name
);
};
private _filteredShchema = memoizeOne(
(options: Record<string, unknown>, schema: HaFormSchema[]) => {
return schema.filter((entry) => entry.name in options || entry.required);
}
);
protected render(): TemplateResult {
const showForm =
!this._yamlMode && this._canShowSchema && this.addon.schema;
const hasHiddenOptions =
showForm &&
JSON.stringify(this.addon.schema) !==
JSON.stringify(
this._filteredShchema(this.addon.options, this.addon.schema!)
);
return html`
<h1>${this.addon.name}</h1>
<ha-card>
<div class="header">
<h2>Configuration</h2>
<h2>
${this.supervisor.localize("addon.configuration.options.header")}
</h2>
<div class="card-menu">
<ha-button-menu corner="BOTTOM_START" @action=${this._handleAction}>
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item .disabled=${!this._canShowSchema}>
${this._yamlMode ? "Edit in UI" : "Edit in YAML"}
${this._yamlMode
? this.supervisor.localize(
"addon.configuration.options.edit_in_ui"
)
: this.supervisor.localize(
"addon.configuration.options.edit_in_yaml"
)}
</mwc-list-item>
<mwc-list-item class="warning">
Reset to defaults
${this.supervisor.localize("common.reset_defaults")}
</mwc-list-item>
</ha-button-menu>
</div>
</div>
<div class="card-content">
${!this._yamlMode && this._canShowSchema && this.addon.schema
${showForm
? html`<ha-form
.data=${this._options!}
@value-changed=${this._configChanged}
.schema=${this.addon.schema}
.computeLabel=${this.computeLabel}
.schema=${this._showOptional
? this.addon.schema!
: this._filteredShchema(
this.addon.options,
this.addon.schema!
)}
></ha-form>`
: html` <ha-yaml-editor
@value-changed=${this._configChanged}
@@ -92,14 +138,34 @@ class HassioAddonConfig extends LitElement {
(this._canShowSchema && this.addon.schema) ||
this._valid
? ""
: html` <div class="errors">Invalid YAML</div> `}
: html`
<div class="errors">
${this.supervisor.localize(
"addon.configuration.options.invalid_yaml"
)}
</div>
`}
</div>
<div class="card-actions">
${hasHiddenOptions
? html`<ha-formfield
class="show-additional"
.label=${this.supervisor.localize(
"addon.configuration.options.show_unused_optional"
)}
>
<ha-switch
@change=${this._toggleOptional}
.checked=${this._showOptional}
>
</ha-switch>
</ha-formfield>`
: ""}
<div class="card-actions right">
<ha-progress-button
@click=${this._saveTapped}
.disabled=${!this._configHasChanged || !this._valid}
>
Save
Save ${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -108,7 +174,7 @@ class HassioAddonConfig extends LitElement {
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
this._canShowSchema = !this.addon.schema.find(
this._canShowSchema = !this.addon.schema!.find(
// @ts-ignore
(entry) => !SUPPORTED_UI_TYPES.includes(entry.type) || entry.multiple
);
@@ -144,17 +210,19 @@ class HassioAddonConfig extends LitElement {
}
}
private _toggleOptional() {
this._showOptional = !this._showOptional;
}
private _configChanged(ev): void {
if (this.addon.schema && this._canShowSchema && !this._yamlMode) {
this._valid = true;
this._configHasChanged = true;
this._options! = ev.detail.value;
} else {
this._configHasChanged = true;
this._valid = ev.detail.isValid;
}
if (this._valid) {
this._options! = ev.detail.value;
}
}
private async _resetTapped(ev: CustomEvent): Promise<void> {
@@ -162,10 +230,10 @@ class HassioAddonConfig extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: this.addon.name,
text: "Are you sure you want to reset all your options?",
confirmText: "reset options",
dismissText: "no",
title: this.supervisor.localize("confirm.reset_options.title"),
text: this.supervisor.localize("confirm.reset_options.text"),
confirmText: this.supervisor.localize("common.reset_options"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -187,9 +255,11 @@ class HassioAddonConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to reset addon configuration, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.common.update_available",
"error",
extractApiErrorMessage(err)
);
}
button.progress = false;
}
@@ -202,8 +272,9 @@ class HassioAddonConfig extends LitElement {
try {
await setHassioAddonOption(this.hass, this.addon.slug, {
options: this._options!,
options: this._yamlMode ? this._editor?.value : this._options,
});
this._configHasChanged = false;
const eventdata = {
success: true,
@@ -212,12 +283,14 @@ class HassioAddonConfig extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err) {
this._error = `Failed to save addon configuration, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.configuration.options.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
button.progress = false;
}
@@ -271,6 +344,13 @@ class HassioAddonConfig extends LitElement {
margin-block: 0px;
font-weight: normal;
}
.card-actions.right {
justify-content: flex-end;
}
.show-additional {
padding: 16px;
}
`,
];
}

View File

@@ -19,6 +19,7 @@ import {
setHassioAddonOption,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { suggestAddonRestart } from "../../dialogs/suggestAddonRestart";
@@ -38,6 +39,8 @@ interface NetworkItemInput extends PaperInputElement {
class HassioAddonNetwork extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -55,16 +58,30 @@ class HassioAddonNetwork extends LitElement {
}
return html`
<ha-card header="Network">
<ha-card
.header=${this.supervisor.localize(
"addon.configuration.network.header"
)}
>
<div class="card-content">
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
<table>
<tbody>
<tr>
<th>Container</th>
<th>Host</th>
<th>Description</th>
<th>
${this.supervisor.localize(
"addon.configuration.network.container"
)}
</th>
<th>
${this.supervisor.localize(
"addon.configuration.network.host"
)}
</th>
<th>
${this.supervisor.localize("common.description")}
</th>
</tr>
${this._config!.map((item) => {
return html`
@@ -73,13 +90,15 @@ class HassioAddonNetwork extends LitElement {
<td>
<paper-input
@value-changed=${this._configChanged}
placeholder="disabled"
placeholder="${this.supervisor.localize(
"addon.configuration.network.disabled"
)}"
.value=${item.host ? String(item.host) : ""}
.container=${item.container}
no-label-float
></paper-input>
</td>
<td>${item.description}</td>
<td>${this._computeDescription(item)}</td>
</tr>
`;
})}
@@ -88,10 +107,10 @@ class HassioAddonNetwork extends LitElement {
</div>
<div class="card-actions">
<ha-progress-button class="warning" @click=${this._resetTapped}>
Reset to defaults
${this.supervisor.localize("common.reset_defaults")}
</ha-progress-button>
<ha-progress-button @click=${this._saveTapped}>
Save
${this.supervisor.localize("common.save")}
</ha-progress-button>
</div>
</ha-card>
@@ -105,6 +124,15 @@ class HassioAddonNetwork extends LitElement {
}
}
private _computeDescription = (item: NetworkItem): string => {
return (
this.addon.translations[this.hass.language]?.network?.[item.container]
?.description ||
this.addon.translations.en?.network?.[item.container]?.description ||
item.description
);
};
private _setNetworkConfig(): void {
const network = this.addon.network || {};
const description = this.addon.network_description || {};
@@ -147,12 +175,14 @@ class HassioAddonNetwork extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err) {
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_reset",
"error",
extractApiErrorMessage(err)
);
}
button.progress = false;
@@ -181,12 +211,14 @@ class HassioAddonNetwork extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
if (this.addon?.state === "started") {
await suggestAddonRestart(this, this.hass, this.addon);
await suggestAddonRestart(this, this.hass, this.supervisor, this.addon);
}
} catch (err) {
this._error = `Failed to set addon network configuration, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
button.progress = false;
}

View File

@@ -1,3 +1,4 @@
import "../../../../src/components/ha-card";
import {
css,
CSSResult,
@@ -19,11 +20,14 @@ import "../../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
@customElement("hassio-addon-documentation-tab")
class HassioAddonDocumentationDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -81,9 +85,11 @@ class HassioAddonDocumentationDashboard extends LitElement {
this.addon!.slug
);
} catch (err) {
this._error = `Failed to get addon documentation, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.documentation.get_logs",
"error",
extractApiErrorMessage(err)
);
}
}
}

View File

@@ -9,16 +9,24 @@ import {
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import { navigate } from "../../../src/common/navigate";
import { extractSearchParam } from "../../../src/common/url/search-params";
import "../../../src/components/ha-circular-progress";
import {
fetchHassioAddonInfo,
HassioAddonDetails,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-error-screen";
import "../../../src/layouts/hass-loading-screen";
import "../../../src/layouts/hass-tabs-subpage";
import type { PageNavigation } from "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles";
@@ -35,12 +43,16 @@ import "./log/hassio-addon-logs";
class HassioAddonDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public addon?: HassioAddonDetails;
@property({ type: Boolean }) public narrow!: boolean;
@internalProperty() _error?: string;
private _computeTail = memoizeOne((route: Route) => {
const dividerPos = route.path.indexOf("/", 1);
return dividerPos === -1
@@ -55,13 +67,19 @@ class HassioAddonDashboard extends LitElement {
});
protected render(): TemplateResult {
if (this._error) {
return html`<hass-error-screen
.error=${this._error}
></hass-error-screen>`;
}
if (!this.addon) {
return html`<ha-circular-progress active></ha-circular-progress>`;
return html`<hass-loading-screen></hass-loading-screen>`;
}
const addonTabs: PageNavigation[] = [
{
name: "Info",
translationKey: "addon.panel.info",
path: `/hassio/addon/${this.addon.slug}/info`,
iconPath: mdiInformationVariant,
},
@@ -69,7 +87,7 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.documentation) {
addonTabs.push({
name: "Documentation",
translationKey: "addon.panel.documentation",
path: `/hassio/addon/${this.addon.slug}/documentation`,
iconPath: mdiFileDocument,
});
@@ -78,12 +96,12 @@ class HassioAddonDashboard extends LitElement {
if (this.addon.version) {
addonTabs.push(
{
name: "Configuration",
translationKey: "addon.panel.configuration",
path: `/hassio/addon/${this.addon.slug}/config`,
iconPath: mdiCogs,
},
{
name: "Log",
translationKey: "addon.panel.log",
path: `/hassio/addon/${this.addon.slug}/logs`,
iconPath: mdiMathLog,
}
@@ -95,17 +113,19 @@ class HassioAddonDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
.backPath=${this.addon.version ? "/hassio/dashboard" : "/hassio/store"}
.route=${route}
hassio
.tabs=${addonTabs}
supervisor
>
<span slot="header">${this.addon.name}</span>
<hassio-addon-router
.route=${route}
.narrow=${this.narrow}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-router>
</hass-tabs-subpage>
@@ -152,30 +172,53 @@ class HassioAddonDashboard extends LitElement {
}
protected async firstUpdated(): Promise<void> {
await this._routeDataChanged(this.route);
if (this.route.path === "") {
const addon = extractSearchParam("addon");
if (addon) {
navigate(this, `/hassio/addon/${addon}`, true);
}
}
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
private async _apiCalled(ev): Promise<void> {
const path: string = ev.detail.path;
const pathSplit: string[] = ev.detail.path?.split("/");
if (!path) {
if (!pathSplit || pathSplit.length === 0) {
return;
}
const path: string = pathSplit[pathSplit.length - 1];
if (["uninstall", "install", "update", "start", "stop"].includes(path)) {
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
}
if (path === "uninstall") {
history.back();
window.history.back();
} else {
await this._routeDataChanged(this.route);
await this._routeDataChanged();
}
}
private async _routeDataChanged(routeData: Route): Promise<void> {
const addon = routeData.path.split("/")[1];
protected updated(changedProperties) {
if (changedProperties.has("route") && !this.addon) {
this._routeDataChanged();
}
}
private async _routeDataChanged(): Promise<void> {
const addon = this.route.path.split("/")[1];
if (!addon) {
return;
}
try {
const addoninfo = await fetchHassioAddonInfo(this.hass, addon);
this.addon = addoninfo;
} catch {
} catch (err) {
this._error = `Error fetching addon info: ${extractApiErrorMessage(err)}`;
this.addon = undefined;
}
}

View File

@@ -1,5 +1,6 @@
import { customElement, property } from "lit-element";
import { HassioAddonDetails } from "../../../src/data/hassio/addon";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
HassRouterPage,
RouterOptions,
@@ -17,6 +18,8 @@ class HassioAddonRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
protected routerOptions: RouterOptions = {
@@ -41,6 +44,7 @@ class HassioAddonRouter extends HassRouterPage {
protected updatePageEl(el) {
el.route = this.routeTail;
el.hass = this.hass;
el.supervisor = this.supervisor;
el.addon = this.addon;
el.narrow = this.narrow;
}

View File

@@ -9,6 +9,7 @@ import {
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -20,6 +21,8 @@ class HassioAddonInfoDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
@@ -32,6 +35,7 @@ class HassioAddonInfoDashboard extends LitElement {
<hassio-addon-info
.narrow=${this.narrow}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-info>
</div>

View File

@@ -25,6 +25,7 @@ import {
TemplateResult,
} from "lit-element";
import { classMap } from "lit-html/directives/class-map";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../../src/common/config/version";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { navigate } from "../../../../src/common/navigate";
@@ -43,9 +44,11 @@ import {
HassioAddonSetOptionParams,
HassioAddonSetSecurityParams,
installHassioAddon,
restartHassioAddon,
setHassioAddonOption,
setHassioAddonSecurity,
startHassioAddon,
stopHassioAddon,
uninstallHassioAddon,
validateHassioAddonOption,
} from "../../../../src/data/hassio/addon";
@@ -54,6 +57,8 @@ import {
fetchHassioStats,
HassioStats,
} from "../../../../src/data/hassio/common";
import { StoreAddon } from "../../../../src/data/supervisor/store";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -63,8 +68,10 @@ import { HomeAssistant } from "../../../../src/types";
import { bytesToString } from "../../../../src/util/bytes-to-string";
import "../../components/hassio-card-content";
import "../../components/supervisor-metric";
import { showDialogSupervisorAddonUpdate } from "../../dialogs/addon/show-dialog-addon-update";
import { showHassioMarkdownDialog } from "../../dialogs/markdown/show-dialog-hassio-markdown";
import { hassioStyle } from "../../resources/hassio-style";
import { addonArchIsSupported } from "../../util/addon";
const STAGE_ICON = {
stable: mdiCheckCircle,
@@ -72,63 +79,6 @@ const STAGE_ICON = {
deprecated: mdiExclamationThick,
};
const PERMIS_DESC = {
stage: {
title: "Add-on Stage",
description: `Add-ons can have one of three stages:\n\n<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon> **Stable**: These are add-ons ready to be used in production.\n\n<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon> **Experimental**: These may contain bugs, and may be unfinished.\n\n<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon> **Deprecated**: These add-ons will no longer receive any updates.`,
},
rating: {
title: "Add-on Security Rating",
description:
"Home Assistant provides a security rating to each of the add-ons, which indicates the risks involved when using this add-on. The more access an add-on requires on your system, the lower the score, thus raising the possible security risks.\n\nA score is on a scale from 1 to 6. Where 1 is the lowest score (considered the most insecure and highest risk) and a score of 6 is the highest score (considered the most secure and lowest risk).",
},
host_network: {
title: "Host Network",
description:
"Add-ons usually run in their own isolated network layer, which prevents them from accessing the network of the host operating system. In some cases, this network isolation can limit add-ons in providing their services and therefore, the isolation can be lifted by the add-on author, giving the add-on full access to the network capabilities of the host machine. This gives the add-on more networking capabilities but lowers the security, hence, the security rating of the add-on will be lowered when this option is used by the add-on.",
},
homeassistant_api: {
title: "Home Assistant API Access",
description:
"This add-on is allowed to access your running Home Assistant instance directly via the Home Assistant API. This mode handles authentication for the add-on as well, which enables an add-on to interact with Home Assistant without the need for additional authentication tokens.",
},
full_access: {
title: "Full Hardware Access",
description:
"This add-on is given full access to the hardware of your system, by request of the add-on author. Access is comparable to the privileged mode in Docker. Since this opens up possible security risks, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
hassio_api: {
title: "Supervisor API Access",
description:
"The add-on was given access to the Supervisor API, by request of the add-on author. By default, the add-on can access general version information of your system. When the add-on requests 'manager' or 'admin' level access to the API, it will gain access to control multiple parts of your Home Assistant system. This permission is indicated by this badge and will impact the security score of the addon negatively.",
},
docker_api: {
title: "Full Docker Access",
description:
"The add-on author has requested the add-on to have management access to the Docker instance running on your system. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
host_pid: {
title: "Host Processes Namespace",
description:
"Usually, the processes the add-on runs, are isolated from all other system processes. The add-on author has requested the add-on to have access to the system processes running on the host system instance, and allow the add-on to spawn processes on the host system as well. This mode gives the add-on full access and control to your entire Home Assistant system, which adds security risks, and could damage your system when misused. Therefore, this feature impacts the add-on security score negatively.\n\nThis level of access is not granted automatically and needs to be confirmed by you. To do this, you need to disable the protection mode on the add-on manually. Only disable the protection mode if you know, need AND trust the source of this add-on.",
},
apparmor: {
title: "AppArmor",
description:
"AppArmor ('Application Armor') is a Linux kernel security module that restricts add-ons capabilities like network access, raw socket access, and permission to read, write, or execute specific files.\n\nAdd-on authors can provide their security profiles, optimized for the add-on, or request it to be disabled. If AppArmor is disabled, it will raise security risks and therefore, has a negative impact on the security score of the add-on.",
},
auth_api: {
title: "Home Assistant Authentication",
description:
"An add-on can authenticate users against Home Assistant, allowing add-ons to give users the possibility to log into applications running inside add-ons, using their Home Assistant username/password. This badge indicates if the add-on author requests this capability.",
},
ingress: {
title: "Ingress",
description:
"This add-on is using Ingress to embed its interface securely into Home Assistant.",
},
};
@customElement("hassio-addon-info")
class HassioAddonInfo extends LitElement {
@property({ type: Boolean }) public narrow!: boolean;
@@ -137,18 +87,29 @@ class HassioAddonInfo extends LitElement {
@property({ attribute: false }) public addon!: HassioAddonDetails;
@property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _metrics?: HassioStats;
@internalProperty() private _error?: string;
private _addonStoreInfo = memoizeOne(
(slug: string, storeAddons: StoreAddon[]) =>
storeAddons.find((addon) => addon.slug === slug)
);
protected render(): TemplateResult {
const addonStoreInfo =
!this.addon.detached && !this.addon.available
? this._addonStoreInfo(this.addon.slug, this.supervisor.store.addons)
: undefined;
const metrics = [
{
description: "Add-on CPU Usage",
description: this.supervisor.localize("addon.dashboard.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: "Add-on RAM Usage",
description: this.supervisor.localize("addon.dashboard.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -158,37 +119,64 @@ class HassioAddonInfo extends LitElement {
return html`
${this.addon.update_available
? html`
<ha-card header="Update available! 🎉">
<ha-card
.header="${this.supervisor.localize(
"common.update_available",
"count",
1
)}🎉"
>
<div class="card-content">
<hassio-card-content
.hass=${this.hass}
.title="${this.addon.name} ${this.addon
.version_latest} is available"
.description="You are currently running version ${this.addon
.version}"
.title="${this.supervisor.localize(
"addon.dashboard.new_update_available",
"name",
this.addon.name,
"version",
this.addon.version_latest
)}"
.description="${this.supervisor.localize(
"common.running_version",
"version",
this.addon.version
)}"
icon=${mdiArrowUpBoldCircle}
iconClass="update"
></hassio-card-content>
${!this.addon.available
? html`
<p>
This update is no longer compatible with your system.
</p>
`
${!this.addon.available && addonStoreInfo
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
</p>
`
: html`
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo.homeassistant
)}
</p>
`
: ""}
</div>
<div class="card-actions">
<ha-call-api-button
.hass=${this.hass}
.disabled=${!this.addon.available}
path="hassio/addons/${this.addon.slug}/update"
>
Update
</ha-call-api-button>
<mwc-button @click=${this._updateClicked}>
${this.supervisor.localize("common.update")}
</mwc-button>
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
Changelog
${this.supervisor.localize("addon.dashboard.changelog")}
</mwc-button>
`
: ""}
@@ -199,12 +187,19 @@ class HassioAddonInfo extends LitElement {
${!this.addon.protected
? html`
<ha-card class="warning">
<h1 class="card-header">Warning: Protection mode is disabled!</h1>
<h1 class="card-header">${this.supervisor.localize(
"addon.dashboard.protection_mode.title"
)}
</h1>
<div class="card-content">
Protection mode on this add-on is disabled! This gives the add-on full access to the entire system, which adds security risks, and could damage your system when used incorrectly. Only disable the protection mode if you know, need AND trust the source of this add-on.
${this.supervisor.localize("addon.dashboard.protection_mode.content")}
</div>
<div class="card-actions protection-enable">
<mwc-button @click=${this._protectionToggled}>Enable Protection mode</mwc-button>
<mwc-button @click=${this._protectionToggled}>
${this.supervisor.localize(
"addon.dashboard.protection_mode.enable"
)}
</mwc-button>
</div>
</div>
</ha-card>
@@ -221,14 +216,18 @@ class HassioAddonInfo extends LitElement {
${this._computeIsRunning
? html`
<ha-svg-icon
title="Add-on is running"
.title=${this.supervisor.localize(
"dashboard.addon_running"
)}
class="running"
.path=${mdiCircle}
></ha-svg-icon>
`
: html`
<ha-svg-icon
title="Add-on is stopped"
.title=${this.supervisor.localize(
"dashboard.addon_stopped"
)}
class="stopped"
.path=${mdiCircle}
></ha-svg-icon>
@@ -242,21 +241,29 @@ class HassioAddonInfo extends LitElement {
? html`
Current version: ${this.addon.version}
<div class="changelog" @click=${this._openChangelog}>
(<span class="changelog-link">changelog</span>)
(<span class="changelog-link">
${this.supervisor.localize("addon.dashboard.changelog")} </span
>)
</div>
`
: html`<span class="changelog-link" @click=${this._openChangelog}
>Changelog</span
>`}
: html`<span class="changelog-link" @click=${this._openChangelog}>
${this.supervisor.localize("addon.dashboard.changelog")}
</span>`}
</div>
<div class="description light-color">
${this.addon.description}.<br />
Visit
<a href="${this.addon.url!}" target="_blank" rel="noreferrer">
${this.addon.name} page</a
>
for details.
${this.supervisor.localize(
"addon.dashboard.visit_addon_page",
"name",
html`<a
href="${this.addon.url!}"
target="_blank"
rel="noreferrer"
>
${this.addon.name}
</a>`
)}
</div>
<div class="addon-container">
<div>
@@ -277,7 +284,9 @@ class HassioAddonInfo extends LitElement {
})}
@click=${this._showMoreInfo}
id="stage"
label="stage"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.stage"
)}
description=""
>
<ha-svg-icon
@@ -303,7 +312,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="host_network"
label="host"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host"
)}
description=""
>
<ha-svg-icon .path=${mdiNetwork}></ha-svg-icon>
@@ -315,7 +326,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="full_access"
label="hardware"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hardware"
)}
description=""
>
<ha-svg-icon .path=${mdiChip}></ha-svg-icon>
@@ -327,7 +340,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="homeassistant_api"
label="hass"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hass"
)}
description=""
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
@@ -339,8 +354,12 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="hassio_api"
label="hassio"
.description=${this.addon.hassio_role}
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.hassio"
)}
.description=${this.supervisor.localize(
`addon.dashboard.capability.role.${this.addon.hassio_role}`
) || this.addon.hassio_role}
>
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</ha-label-badge>
@@ -351,7 +370,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="docker_api"
label="docker"
.label=".${this.supervisor.localize(
"addon.dashboard.capability.label.docker"
)}"
description=""
>
<ha-svg-icon .path=${mdiDocker}></ha-svg-icon>
@@ -363,7 +384,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="host_pid"
label="host pid"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.host_pid"
)}
description=""
>
<ha-svg-icon .path=${mdiPound}></ha-svg-icon>
@@ -376,7 +399,9 @@ class HassioAddonInfo extends LitElement {
@click=${this._showMoreInfo}
class=${this._computeApparmorClassName}
id="apparmor"
label="apparmor"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.apparmor"
)}
description=""
>
<ha-svg-icon .path=${mdiShield}></ha-svg-icon>
@@ -388,7 +413,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="auth_api"
label="auth"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.auth"
)}
description=""
>
<ha-svg-icon .path=${mdiKey}></ha-svg-icon>
@@ -400,7 +427,9 @@ class HassioAddonInfo extends LitElement {
<ha-label-badge
@click=${this._showMoreInfo}
id="ingress"
label="ingress"
.label=${this.supervisor.localize(
"addon.dashboard.capability.label.ingress"
)}
description=""
>
<ha-svg-icon
@@ -421,10 +450,14 @@ class HassioAddonInfo extends LitElement {
>
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Start on boot
${this.supervisor.localize(
"addon.dashboard.option.boot.title"
)}
</span>
<span slot="description">
Make the add-on start during a system boot
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
)}
</span>
<ha-switch
@change=${this._startOnBootToggled}
@@ -437,10 +470,14 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Watchdog
${this.supervisor.localize(
"addon.dashboard.option.watchdog.title"
)}
</span>
<span slot="description">
This will start the add-on if it crashes
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
)}
</span>
<ha-switch
@change=${this._watchdogToggled}
@@ -455,11 +492,14 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Auto update
${this.supervisor.localize(
"addon.dashboard.option.auto_update.title"
)}
</span>
<span slot="description">
Auto update the add-on when there is a new
version available
${this.supervisor.localize(
"addon.dashboard.option.boot.description"
)}
</span>
<ha-switch
@change=${this._autoUpdateToggled}
@@ -469,21 +509,22 @@ class HassioAddonInfo extends LitElement {
</ha-settings-row>
`
: ""}
${this.addon.ingress
${!this._computeCannotIngressSidebar && this.addon.ingress
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Show in sidebar
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.title"
)}
</span>
<span slot="description">
${this._computeCannotIngressSidebar
? "This option requires Home Assistant 0.92 or later."
: "Add this add-on to your sidebar"}
${this.supervisor.localize(
"addon.dashboard.option.ingress_panel.description"
)}
</span>
<ha-switch
@change=${this._panelToggled}
.checked=${this.addon.ingress_panel}
.disabled=${this._computeCannotIngressSidebar}
haptic
></ha-switch>
</ha-settings-row>
@@ -493,10 +534,14 @@ class HassioAddonInfo extends LitElement {
? html`
<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Protection mode
${this.supervisor.localize(
"addon.dashboard.option.protected.title"
)}
</span>
<span slot="description">
Blocks elevated system access from the add-on
${this.supervisor.localize(
"addon.dashboard.option.protected.description"
)}
</span>
<ha-switch
@change=${this._protectionToggled}
@@ -514,7 +559,7 @@ class HassioAddonInfo extends LitElement {
${this.addon.state === "started"
? html`<ha-settings-row ?three-line=${this.narrow}>
<span slot="heading">
Hostname
${this.supervisor.localize("addon.dashboard.hostname")}
</span>
<code slot="description">
${this.addon.hostname}
@@ -534,87 +579,109 @@ class HassioAddonInfo extends LitElement {
</div>
</div>
${this._error ? html` <div class="errors">${this._error}</div> ` : ""}
${!this.addon.version && addonStoreInfo && !this.addon.available
? !addonArchIsSupported(
this.supervisor.info.supported_arch,
this.addon.arch
)
? html`
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_arch"
)}
</p>
`
: html`
<p class="warning">
${this.supervisor.localize(
"addon.dashboard.not_available_version",
"core_version_installed",
this.supervisor.core.version,
"core_version_needed",
addonStoreInfo!.homeassistant
)}
</p>
`
: ""}
</div>
<div class="card-actions">
${this.addon.version
? html`
${this._computeIsRunning
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/stop"
>
Stop
</ha-call-api-button>
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/restart"
>
Restart
</ha-call-api-button>
`
: html`
<ha-progress-button @click=${this._startClicked}>
Start
</ha-progress-button>
`}
${this._computeShowWebUI
? html`
<a
href=${this._pathWebui!}
tabindex="-1"
target="_blank"
class="right"
rel="noopener"
>
<mwc-button>
Open web UI
<div>
${this.addon.version
? this._computeIsRunning
? html`
<ha-progress-button
class="warning"
@click=${this._stopClicked}
>
${this.supervisor.localize("addon.dashboard.stop")}
</ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._restartClicked}
>
${this.supervisor.localize("addon.dashboard.restart")}
</ha-progress-button>
`
: html`
<ha-progress-button @click=${this._startClicked}>
${this.supervisor.localize("addon.dashboard.start")}
</ha-progress-button>
`
: html`
<ha-progress-button
.disabled=${!this.addon.available}
@click=${this._installClicked}
>
${this.supervisor.localize("addon.dashboard.install")}
</ha-progress-button>
`}
</div>
<div>
${this.addon.version
? html` ${this._computeShowWebUI
? html`
<a
href=${this._pathWebui!}
tabindex="-1"
target="_blank"
rel="noopener"
>
<mwc-button>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</mwc-button>
</a>
`
: ""}
${this._computeShowIngressUI
? html`
<mwc-button @click=${this._openIngress}>
${this.supervisor.localize(
"addon.dashboard.open_web_ui"
)}
</mwc-button>
</a>
`
: ""}
${this._computeShowIngressUI
? html`
<mwc-button class="right" @click=${this._openIngress}>
Open web UI
</mwc-button>
`
: ""}
<ha-progress-button
class=" right warning"
@click=${this._uninstallClicked}
>
Uninstall
</ha-progress-button>
${this.addon.build
? html`
<ha-call-api-button
class="warning right"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
Rebuild
</ha-call-api-button>
`
: ""}
`
: html`
${!this.addon.available
? html`
<p class="warning">
This add-on is not available on your system.
</p>
`
: ""}
<ha-progress-button
.disabled=${!this.addon.available}
@click=${this._installClicked}
>
Install
</ha-progress-button>
`}
`
: ""}
<ha-progress-button
class="warning"
@click=${this._uninstallClicked}
>
${this.supervisor.localize("addon.dashboard.uninstall")}
</ha-progress-button>
${this.addon.build
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
${this.supervisor.localize("addon.dashboard.rebuild")}
</ha-call-api-button>
`
: ""}`
: ""}
</div>
</div>
</ha-card>
@@ -669,8 +736,21 @@ class HassioAddonInfo extends LitElement {
private _showMoreInfo(ev): void {
const id = ev.currentTarget.id;
showHassioMarkdownDialog(this, {
title: PERMIS_DESC[id].title,
content: PERMIS_DESC[id].description,
title: this.supervisor.localize(`addon.dashboard.capability.${id}.title`),
content:
id === "stage"
? this.supervisor.localize(
`addon.dashboard.capability.${id}.description`,
"icon_stable",
`<ha-svg-icon path="${STAGE_ICON.stable}"></ha-svg-icon>`,
"icon_experimental",
`<ha-svg-icon path="${STAGE_ICON.experimental}"></ha-svg-icon>`,
"icon_deprecated",
`<ha-svg-icon path="${STAGE_ICON.deprecated}"></ha-svg-icon>`
)
: this.supervisor.localize(
`addon.dashboard.capability.${id}.description`
),
});
}
@@ -723,9 +803,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -743,9 +825,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -763,9 +847,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -783,9 +869,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon security option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -803,9 +891,11 @@ class HassioAddonInfo extends LitElement {
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
this._error = `Failed to set addon option, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"addon.failed_to_save",
"error",
extractApiErrorMessage(err)
);
}
}
@@ -816,12 +906,14 @@ class HassioAddonInfo extends LitElement {
this.addon.slug
);
showHassioMarkdownDialog(this, {
title: "Changelog",
title: this.supervisor.localize("addon.dashboard.changelog"),
content,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get addon changelog",
title: this.supervisor.localize(
"addon.dashboard.action_error.get_changelog"
),
text: extractApiErrorMessage(err),
});
}
@@ -841,13 +933,59 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to install addon",
title: this.supervisor.localize("addon.dashboard.action_error.install"),
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _stopClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await stopHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "stop",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.stop"),
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _restartClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await restartHassioAddon(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "stop",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: this.supervisor.localize("addon.dashboard.action_error.restart"),
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _updateClicked(): Promise<void> {
showDialogSupervisorAddonUpdate(this, { addon: this.addon });
}
private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
@@ -856,13 +994,17 @@ class HassioAddonInfo extends LitElement {
this.hass,
this.addon.slug
);
if (!validate.data.valid) {
if (!validate.valid) {
await showConfirmationDialog(this, {
title: "Failed to start addon - configuration validation failed!",
text: validate.data.message.split(" Got ")[0],
title: this.supervisor.localize(
"addon.dashboard.action_error.start_invalid_config"
),
text: validate.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: "Go to configuration",
dismissText: "Cancel",
confirmText: this.supervisor.localize(
"addon.dashboard.action_error.go_to_config"
),
dismissText: this.supervisor.localize("common.cancel"),
});
button.progress = false;
return;
@@ -879,9 +1021,15 @@ class HassioAddonInfo extends LitElement {
try {
await startHassioAddon(this.hass, this.addon.slug);
this.addon = await fetchHassioAddonInfo(this.hass, this.addon.slug);
const eventdata = {
success: true,
response: undefined,
path: "start",
};
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to start addon",
title: this.supervisor.localize("addon.dashboard.action_error.start"),
text: extractApiErrorMessage(err),
});
}
@@ -919,7 +1067,9 @@ class HassioAddonInfo extends LitElement {
fireEvent(this, "hass-api-called", eventdata);
} catch (err) {
showAlertDialog(this, {
title: "Failed to uninstall addon",
title: this.supervisor.localize(
"addon.dashboard.action_error.uninstall"
),
text: extractApiErrorMessage(err),
});
}
@@ -994,9 +1144,6 @@ class HassioAddonInfo extends LitElement {
font-weight: 500;
color: var(--primary-color);
}
.right {
float: right;
}
protection-enable mwc-button {
--mdc-theme-primary: white;
}
@@ -1019,7 +1166,8 @@ class HassioAddonInfo extends LitElement {
margin-bottom: 16px;
}
.card-actions {
display: flow-root;
justify-content: space-between;
display: flex;
}
.security h3 {
margin-bottom: 8px;
@@ -1055,18 +1203,16 @@ class HassioAddonInfo extends LitElement {
}
.addon-options {
max-width: 50%;
}
.addon-options.started {
max-width: 90%;
}
.addon-container {
display: grid;
grid-auto-flow: column;
grid-template-columns: 1fr auto;
grid-template-columns: 60% 40%;
}
.addon-container div:last-of-type {
.addon-container > div:last-of-type {
align-self: end;
}

View File

@@ -9,6 +9,7 @@ import {
} from "lit-element";
import "../../../../src/components/ha-circular-progress";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import { hassioStyle } from "../../resources/hassio-style";
@@ -18,6 +19,8 @@ import "./hassio-addon-logs";
class HassioAddonLogDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon?: HassioAddonDetails;
protected render(): TemplateResult {
@@ -28,6 +31,7 @@ class HassioAddonLogDashboard extends LitElement {
<div class="content">
<hassio-addon-logs
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-logs>
</div>

View File

@@ -15,6 +15,7 @@ import {
HassioAddonDetails,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
import "../../components/hassio-ansi-to-html";
@@ -24,6 +25,8 @@ import { hassioStyle } from "../../resources/hassio-style";
class HassioAddonLogs extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public addon!: HassioAddonDetails;
@internalProperty() private _error?: string;
@@ -48,7 +51,9 @@ class HassioAddonLogs extends LitElement {
: ""}
</div>
<div class="card-actions">
<mwc-button @click=${this._refresh}>Refresh</mwc-button>
<mwc-button @click=${this._refresh}>
${this.supervisor.localize("common.refresh")}
</mwc-button>
</div>
</ha-card>
`;
@@ -76,7 +81,11 @@ class HassioAddonLogs extends LitElement {
try {
this._content = await fetchHassioAddonLogs(this.hass, this.addon.slug);
} catch (err) {
this._error = `Failed to get addon logs, ${extractApiErrorMessage(err)}`;
this._error = this.supervisor.localize(
"addon.logs.get_logs",
"error",
extractApiErrorMessage(err)
);
}
}

View File

@@ -44,7 +44,7 @@ class HassioCardContent extends LitElement {
${this.iconImage
? html`
<div class="icon_image ${this.iconClass}">
<img src="${this.iconImage}" title="${this.iconTitle}" />
<img src="${this.iconImage}" .title=${this.iconTitle} />
<div></div>
</div>
`

View File

@@ -26,7 +26,7 @@ class SupervisorMetric extends LitElement {
<span slot="heading">
${this.description}
</span>
<div slot="description" title="${this.tooltip ?? ""}">
<div slot="description" .title=${this.tooltip ?? ""}>
<span class="value">
${roundedValue}%
</span>

View File

@@ -27,17 +27,15 @@ class HassioAddons extends LitElement {
protected render(): TemplateResult {
return html`
<div class="content">
<h1>Add-ons</h1>
<h1>${this.supervisor.localize("dashboard.addons")}</h1>
<div class="card-group">
${!this.supervisor.supervisor.addons?.length
? html`
<ha-card>
<div class="card-content">
You don't have any add-ons installed yet. Head over to
<button class="link" @click=${this._openStore}>
the add-on store
${this.supervisor.localize("dashboard.no_addons")}
</button>
to get started!
</div>
</ha-card>
`
@@ -58,10 +56,16 @@ class HassioAddons extends LitElement {
? mdiArrowUpBoldCircle
: mdiPuzzle}
.iconTitle=${addon.state !== "started"
? "Add-on is stopped"
? this.supervisor.localize(
"dashboard.addon_stopped"
)
: addon.update_available!
? "New version available"
: "Add-on is running"}
? this.supervisor.localize(
"dashboard.addon_new_version"
)
: this.supervisor.localize(
"dashboard.addon_running"
)}
.iconClass=${addon.update_available
? addon.state === "started"
? "update"

View File

@@ -29,13 +29,16 @@ class HassioDashboard extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Dashboard</span>
<span slot="header">
${this.supervisor.localize("panel.dashboard")}
</span>
<div class="content">
<hassio-update
.hass=${this.hass}

View File

@@ -10,8 +10,11 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-svg-icon";
import {
extractApiErrorMessage,
@@ -23,15 +26,23 @@ import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
Supervisor,
supervisorApiWsRequest,
} from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../src/dialogs/generic/show-dialog-box";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
import { hassioStyle } from "../resources/hassio-style";
const computeVersion = (key: string, version: string): string => {
return key === "os" ? version : `${key}-${version}`;
};
@customElement("hassio-update")
export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@@ -57,13 +68,17 @@ export class HassioUpdate extends LitElement {
return html`
<div class="content">
<h1>
${updatesAvailable > 1
? "Updates Available 🎉"
: "Update Available 🎉"}
${this.supervisor.localize(
"common.update_available",
"count",
updatesAvailable
)}
🎉
</h1>
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
"core",
this.supervisor.core,
"hassio/homeassistant/update",
`https://${
@@ -72,6 +87,7 @@ export class HassioUpdate extends LitElement {
)}
${this._renderUpdateCard(
"Supervisor",
"supervisor",
this.supervisor.supervisor,
"hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
@@ -79,6 +95,7 @@ export class HassioUpdate extends LitElement {
${this.supervisor.host.features.includes("hassos")
? this._renderUpdateCard(
"Operating System",
"os",
this.supervisor.os,
"hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
@@ -91,6 +108,7 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard(
name: string,
key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
apiPath: string,
releaseNotesUrl: string
@@ -104,22 +122,39 @@ export class HassioUpdate extends LitElement {
<div class="icon">
<ha-svg-icon .path=${mdiHomeAssistant}></ha-svg-icon>
</div>
<div class="update-heading">${name} ${object.version_latest}</div>
<div class="warning">
You are currently running version ${object.version}
</div>
<div class="update-heading">${name}</div>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
${computeVersion(key, object.version!)}
</span>
</ha-settings-row>
<ha-settings-row two-line>
<span slot="heading">
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
${computeVersion(key, object.version_latest!)}
</span>
</ha-settings-row>
</div>
<div class="card-actions">
<a href="${releaseNotesUrl}" target="_blank" rel="noreferrer">
<mwc-button>Release notes</mwc-button>
<mwc-button>
${this.supervisor.localize("common.release_notes")}
</mwc-button>
</a>
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.key=${key}
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
Update
${this.supervisor.localize("common.update")}
</ha-progress-button>
</div>
</ha-card>
@@ -128,12 +163,26 @@ export class HassioUpdate extends LitElement {
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorCoreUpdate(this, { core: this.supervisor.core });
return;
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
text: `Are you sure you want to update ${item.name} to version ${item.version}?`,
confirmText: "update",
dismissText: "cancel",
title: this.supervisor.localize(
"confirm.update.title",
"name",
item.name
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
item.name,
"version",
computeVersion(item.key, item.version)
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -141,13 +190,28 @@ export class HassioUpdate extends LitElement {
return;
}
try {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
await supervisorApiWsRequest(this.hass.connection, {
method: "post",
endpoint: item.apiPath.replace("hassio", ""),
timeout: null,
});
} else {
await this.hass.callApi<HassioResponse<void>>("POST", item.apiPath);
}
fireEvent(this, "supervisor-colllection-refresh", {
colllection: item.key,
});
} catch (err) {
// Only show an error if the status code was not expected (user behind proxy)
// or no status at all(connection terminated)
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
if (
this.hass.connection.connected &&
err.status_code &&
!ignoredStatusCodes.has(err.status_code)
) {
showAlertDialog(this, {
title: "Update failed",
title: this.supervisor.localize("common.error.update_failed"),
text: extractApiErrorMessage(err),
});
}
@@ -172,9 +236,6 @@ export class HassioUpdate extends LitElement {
margin-bottom: 0.5em;
color: var(--primary-text-color);
}
.warning {
color: var(--secondary-text-color);
}
.card-content {
height: calc(100% - 47px);
box-sizing: border-box;
@@ -182,13 +243,13 @@ export class HassioUpdate extends LitElement {
.card-actions {
text-align: right;
}
.errors {
color: var(--error-color);
padding: 16px;
}
a {
text-decoration: none;
}
ha-settings-row {
padding: 0;
--paper-item-body-two-line-min-height: 32px;
}
`,
];
}

View File

@@ -0,0 +1,179 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import {
HassioAddonDetails,
updateHassioAddon,
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorAddonUpdateParams } from "./show-dialog-addon-update";
@customElement("dialog-supervisor-addon-update")
class DialogSupervisorAddonUpdate extends LitElement {
public hass!: HomeAssistant;
public addon!: HassioAddonDetails;
@internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true;
@internalProperty() private _action: "snapshot" | "update" | null = null;
@internalProperty() private _error?: string;
public async showDialog(
params: SupervisorDialogSupervisorAddonUpdateParams
): Promise<void> {
this._opened = true;
this.addon = params.addon;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
protected render(): TemplateResult {
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
Update ${this.addon.name}
</h2>
</slot>
<div>
Are you sure you want to update the ${this.addon.name} add-on to
version ${this.addon.version_latest}?
</div>
<ha-settings-row>
<span slot="heading">
Snapshot
</span>
<span slot="description">
Create a snapshot of the ${this.addon.name} add-on before
updating
</span>
<ha-switch
.checked=${this._createSnapshot}
haptic
title="Create snapshot"
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
Cancel
</mwc-button>
<mwc-button @click=${this._update} slot="primaryAction">
Update
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? `Updating ${this.addon.name} to version ${this.addon.version_latest}`
: "Creating snapshot of Home Assistant Core"}
</p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog>
`;
}
private _toggleSnapshot() {
this._createSnapshot = !this._createSnapshot;
}
private async _update() {
if (this._createSnapshot) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(this.hass, {
name: `addon_${this.addon.slug}_${this.addon.version}`,
addons: [this.addon.slug],
homeassistant: false,
});
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await updateHassioAddon(this.hass, this.addon.slug);
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
fireEvent(this, "supervisor-colllection-refresh", { colllection: "addon" });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
this.closeDialog();
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-addon-update": DialogSupervisorAddonUpdate;
}
}

View File

@@ -0,0 +1,17 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
export interface SupervisorDialogSupervisorAddonUpdateParams {
addon: HassioAddonDetails;
}
export const showDialogSupervisorAddonUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorAddonUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-addon-update",
dialogImport: () => import("./dialog-supervisor-addon-update"),
dialogParams,
});
};

View File

@@ -0,0 +1,175 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-settings-row";
import "../../../../src/components/ha-svg-icon";
import "../../../../src/components/ha-switch";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { createHassioPartialSnapshot } from "../../../../src/data/hassio/snapshot";
import { HassioHomeAssistantInfo } from "../../../../src/data/hassio/supervisor";
import { updateCore } from "../../../../src/data/supervisor/core";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { SupervisorDialogSupervisorCoreUpdateParams } from "./show-dialog-core-update";
@customElement("dialog-supervisor-core-update")
class DialogSupervisorCoreUpdate extends LitElement {
public hass!: HomeAssistant;
public core!: HassioHomeAssistantInfo;
@internalProperty() private _opened = false;
@internalProperty() private _createSnapshot = true;
@internalProperty() private _action: "snapshot" | "update" | null = null;
@internalProperty() private _error?: string;
public async showDialog(
params: SupervisorDialogSupervisorCoreUpdateParams
): Promise<void> {
this._opened = true;
this.core = params.core;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._opened = false;
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
public focus(): void {
this.updateComplete.then(() =>
(this.shadowRoot?.querySelector(
"[dialogInitialFocus]"
) as HTMLElement)?.focus()
);
}
protected render(): TemplateResult {
return html`
<ha-dialog .open=${this._opened} scrimClickAction escapeKeyAction>
${this._action === null
? html`<slot name="heading">
<h2 id="title" class="header_title">
Update Home Assistant Core
</h2>
</slot>
<div>
Are you sure you want to update Home Assistant Core to version
${this.core.version_latest}?
</div>
<ha-settings-row three-rows>
<span slot="heading">
Snapshot
</span>
<span slot="description">
Create a snapshot of Home Assistant Core before updating
</span>
<ha-switch
.checked=${this._createSnapshot}
haptic
title="Create snapshot"
@click=${this._toggleSnapshot}
>
</ha-switch>
</ha-settings-row>
<mwc-button @click=${this.closeDialog} slot="secondaryAction">
Cancel
</mwc-button>
<mwc-button @click=${this._update} slot="primaryAction">
Update
</mwc-button>`
: html`<ha-circular-progress alt="Updating" size="large" active>
</ha-circular-progress>
<p class="progress-text">
${this._action === "update"
? `Updating Home Assistant Core to version ${this.core.version_latest}`
: "Creating snapshot of Home Assistant Core"}
</p>`}
${this._error ? html`<p class="error">${this._error}</p>` : ""}
</ha-dialog>
`;
}
private _toggleSnapshot() {
this._createSnapshot = !this._createSnapshot;
}
private async _update() {
if (this._createSnapshot) {
this._action = "snapshot";
try {
await createHassioPartialSnapshot(this.hass, {
name: `core_${this.core.version}`,
folders: ["homeassistant"],
homeassistant: true,
});
} catch (err) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
this._action = "update";
try {
await updateCore(this.hass);
} catch (err) {
if (this.hass.connection.connected) {
this._error = extractApiErrorMessage(err);
this._action = null;
return;
}
}
fireEvent(this, "supervisor-colllection-refresh", { colllection: "core" });
this.closeDialog();
}
static get styles(): CSSResult[] {
return [
haStyle,
haStyleDialog,
css`
.form {
color: var(--primary-text-color);
}
ha-settings-row {
margin-top: 32px;
padding: 0;
}
ha-circular-progress {
display: block;
margin: 32px;
text-align: center;
}
.progress-text {
text-align: center;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"dialog-supervisor-core-update": DialogSupervisorCoreUpdate;
}
}

View File

@@ -0,0 +1,17 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioHomeAssistantInfo } from "../../../../src/data/hassio/supervisor";
export interface SupervisorDialogSupervisorCoreUpdateParams {
core: HassioHomeAssistantInfo;
}
export const showDialogSupervisorCoreUpdate = (
element: HTMLElement,
dialogParams: SupervisorDialogSupervisorCoreUpdateParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-supervisor-core-update",
dialogImport: () => import("./dialog-supervisor-core-update"),
dialogParams,
});
};

View File

@@ -35,6 +35,7 @@ import {
updateNetworkInterface,
WifiConfiguration,
} from "../../../../src/data/hassio/network";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -51,6 +52,8 @@ export class DialogHassioNetwork extends LitElement
implements HassDialog<HassioNetworkDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _accessPoints?: AccessPoints;
@internalProperty() private _curTabIndex = 0;
@@ -73,7 +76,8 @@ export class DialogHassioNetwork extends LitElement
this._params = params;
this._dirty = false;
this._curTabIndex = 0;
this._interfaces = params.network.interfaces.sort((a, b) => {
this.supervisor = params.supervisor;
this._interfaces = params.supervisor.network.interfaces.sort((a, b) => {
return a.primary > b.primary ? -1 : 1;
});
this._interface = { ...this._interfaces[this._curTabIndex] };
@@ -104,7 +108,7 @@ export class DialogHassioNetwork extends LitElement
<div slot="heading">
<ha-header-bar>
<span slot="title">
Network settings
${this.supervisor.localize("dialog.network.title")}
</span>
<mwc-icon-button slot="actionItems" dialogAction="cancel">
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
@@ -139,7 +143,13 @@ export class DialogHassioNetwork extends LitElement
? html`
<ha-expansion-panel header="Wi-Fi" outlined>
${this._interface?.wifi?.ssid
? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
? html`<p>
${this.supervisor.localize(
"dialog.network.connected_to",
"ssid",
this._interface?.wifi?.ssid
)}
</p>`
: ""}
<mwc-button
class="scan"
@@ -149,7 +159,7 @@ export class DialogHassioNetwork extends LitElement
${this._scanning
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: "Scan for accesspoints"}
: this.supervisor.localize("dialog.network.scan_ap")}
</mwc-button>
${this._accessPoints &&
this._accessPoints.accesspoints &&
@@ -181,7 +191,11 @@ export class DialogHassioNetwork extends LitElement
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield label="open">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.open"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -193,7 +207,11 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wep">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wep"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -203,7 +221,11 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wpa-psk">
<ha-formfield
.label=${this.supervisor.localize(
"dialog.network.wpa"
)}
>
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
@@ -237,18 +259,21 @@ export class DialogHassioNetwork extends LitElement
: ""}
${this._dirty
? html`<div class="warning">
If you are changing the Wi-Fi, IP or gateway addresses, you might
lose the connection!
${this.supervisor.localize("dialog.network.warning")}
</div>`
: ""}
</div>
<div class="buttons">
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button
.label=${this.supervisor.localize("common.cancel")}
@click=${this.closeDialog}
>
</mwc-button>
<mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._processing
? html`<ha-circular-progress active size="small">
</ha-circular-progress>`
: "Save"}
: this.supervisor.localize("common.save")}
</mwc-button>
</div>`;
}
@@ -285,7 +310,9 @@ export class DialogHassioNetwork extends LitElement
outlined
>
<div class="radio-row">
<ha-formfield label="DHCP">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.dhcp")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -295,7 +322,9 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Static">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.static")}
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -305,7 +334,10 @@ export class DialogHassioNetwork extends LitElement
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Disabled" class="warning">
<ha-formfield
.label=${this.supervisor.localize("dialog.network.disabled")}
class="warning"
>
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
@@ -321,7 +353,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="address"
label="IP address/Netmask"
.label=${this.supervisor.localize("dialog.network.ip_netmask")}
.version=${version}
.value=${this._toString(this._interface![version].address)}
@value-changed=${this._handleInputValueChanged}
@@ -330,7 +362,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="gateway"
label="Gateway address"
.label=${this.supervisor.localize("dialog.network.gateway")}
.version=${version}
.value=${this._interface![version].gateway}
@value-changed=${this._handleInputValueChanged}
@@ -339,7 +371,7 @@ export class DialogHassioNetwork extends LitElement
<paper-input
class="flex-auto"
id="nameservers"
label="DNS servers"
.label=${this.supervisor.localize("dialog.network.dns_servers")}
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@value-changed=${this._handleInputValueChanged}
@@ -424,7 +456,7 @@ export class DialogHassioNetwork extends LitElement
);
} catch (err) {
showAlertDialog(this, {
title: "Failed to change network settings",
title: this.supervisor.localize("dialog.network.failed_to_change"),
text: extractApiErrorMessage(err),
});
this._processing = false;
@@ -437,10 +469,9 @@ export class DialogHassioNetwork extends LitElement
private async _handleTabActivated(ev: CustomEvent): Promise<void> {
if (this._dirty) {
const confirm = await showConfirmationDialog(this, {
text:
"You have unsaved changes, these will get lost if you change tabs, do you want to continue?",
confirmText: "yes",
dismissText: "no",
text: this.supervisor.localize("dialog.network.unsaved"),
confirmText: this.supervisor.localize("common.yes"),
dismissText: this.supervisor.localize("common.no"),
});
if (!confirm) {
this.requestUpdate("_interface");

View File

@@ -1,9 +1,9 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { NetworkInfo } from "../../../../src/data/hassio/network";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-network";
export interface HassioNetworkDialogParams {
network: NetworkInfo;
supervisor: Supervisor;
loadData: () => Promise<void>;
}

View File

@@ -22,14 +22,18 @@ import {
fetchHassioDockerRegistries,
removeHassioDockerRegistry,
} from "../../../../src/data/hassio/docker";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { RegistriesDialogParams } from "./show-dialog-registries";
@customElement("dialog-hassio-registries")
class HassioRegistriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) private _registries?: {
registry: string;
username: string;
@@ -55,8 +59,8 @@ class HassioRegistriesDialog extends LitElement {
.heading=${createCloseHeading(
this.hass,
this._addingRegistry
? "Add New Docker Registry"
: "Manage Docker Registries"
? this.supervisor.localize("dialog.registries.title_add")
: this.supervisor.localize("dialog.registries.title_manage")
)}
>
<div class="form">
@@ -66,7 +70,9 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="registry"
label="Registry"
.label=${this.supervisor.localize(
"dialog.registries.registry"
)}
required
auto-validate
></paper-input>
@@ -74,7 +80,9 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="username"
label="Username"
.label=${this.supervisor.localize(
"dialog.registries.username"
)}
required
auto-validate
></paper-input>
@@ -82,7 +90,9 @@ class HassioRegistriesDialog extends LitElement {
@value-changed=${this._inputChanged}
class="flex-auto"
name="password"
label="Password"
.label=${this.supervisor.localize(
"dialog.registries.password"
)}
type="password"
required
auto-validate
@@ -94,7 +104,7 @@ class HassioRegistriesDialog extends LitElement {
)}
@click=${this._addNewRegistry}
>
Add registry
${this.supervisor.localize("dialog.registries.add_registry")}
</mwc-button>
`
: html`${this._registries?.length
@@ -103,11 +113,16 @@ class HassioRegistriesDialog extends LitElement {
<mwc-list-item class="option" hasMeta twoline>
<span>${entry.registry}</span>
<span slot="secondary"
>Username: ${entry.username}</span
>${this.supervisor.localize(
"dialog.registries.username"
)}:
${entry.username}</span
>
<mwc-icon-button
.entry=${entry}
title="Remove"
.title=${this.supervisor.localize(
"dialog.registries.remove"
)}
slot="meta"
@click=${this._removeRegistry}
>
@@ -118,11 +133,17 @@ class HassioRegistriesDialog extends LitElement {
})
: html`
<mwc-list-item>
<span>No registries configured</span>
<span
>${this.supervisor.localize(
"dialog.registries.no_registries"
)}</span
>
</mwc-list-item>
`}
<mwc-button @click=${this._addRegistry}>
Add new registry
${this.supervisor.localize(
"dialog.registries.add_new_registry"
)}
</mwc-button> `}
</div>
</ha-dialog>
@@ -134,8 +155,9 @@ class HassioRegistriesDialog extends LitElement {
this[`_${target.name}`] = target.value;
}
public async showDialog(_dialogParams: any): Promise<void> {
public async showDialog(dialogParams: RegistriesDialogParams): Promise<void> {
this._opened = true;
this.supervisor = dialogParams.supervisor;
await this._loadRegistries();
await this.updateComplete;
}
@@ -178,7 +200,7 @@ class HassioRegistriesDialog extends LitElement {
this._addingRegistry = false;
} catch (err) {
showAlertDialog(this, {
title: "Failed to add registry",
title: this.supervisor.localize("dialog.registries.failed_to_add"),
text: extractApiErrorMessage(err),
});
}
@@ -192,7 +214,7 @@ class HassioRegistriesDialog extends LitElement {
await this._loadRegistries();
} catch (err) {
showAlertDialog(this, {
title: "Failed to remove registry",
title: this.supervisor.localize("dialog.registries.failed_to_remove"),
text: extractApiErrorMessage(err),
});
}

View File

@@ -1,10 +1,18 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-registries";
export const showRegistriesDialog = (element: HTMLElement): void => {
export interface RegistriesDialogParams {
supervisor: Supervisor;
}
export const showRegistriesDialog = (
element: HTMLElement,
dialogParams: RegistriesDialogParams
): void => {
fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries",
dialogImport: () => import("./dialog-hassio-registries"),
dialogParams: {},
dialogParams,
});
};

View File

@@ -18,7 +18,7 @@ import {
} from "lit-element";
import memoizeOne from "memoize-one";
import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog";
import { createCloseHeading } from "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-svg-icon";
import {
fetchHassioAddonsInfo,
@@ -26,6 +26,7 @@ import {
} from "../../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { setSupervisorOption } from "../../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types";
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
@@ -34,6 +35,8 @@ import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
class HassioRepositoriesDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) private _repos: HassioAddonRepository[] = [];
@property({ attribute: false })
@@ -47,9 +50,11 @@ class HassioRepositoriesDialog extends LitElement {
@internalProperty() private _error?: string;
public async showDialog(_dialogParams: any): Promise<void> {
this._dialogParams = _dialogParams;
this._repos = _dialogParams.repos;
public async showDialog(
dialogParams: HassioRepositoryDialogParams
): Promise<void> {
this._dialogParams = dialogParams;
this.supervisor = dialogParams.supervisor;
this._opened = true;
await this.updateComplete;
}
@@ -66,14 +71,19 @@ class HassioRepositoriesDialog extends LitElement {
);
protected render(): TemplateResult {
const repositories = this._filteredRepositories(this._repos);
const repositories = this._filteredRepositories(
this.supervisor.addon.repositories
);
return html`
<ha-dialog
.open=${this._opened}
@closing=${this.closeDialog}
scrimClickAction
escapeKeyAction
heading="Manage add-on repositories"
.heading=${createCloseHeading(
this.hass,
this.supervisor.localize("dialog.repositories.title")
)}
>
${this._error ? html`<div class="error">${this._error}</div>` : ""}
<div class="form">
@@ -88,7 +98,9 @@ class HassioRepositoriesDialog extends LitElement {
</paper-item-body>
<mwc-icon-button
.slug=${repo.slug}
title="Remove"
.title=${this.supervisor.localize(
"dialog.repositories.remove"
)}
@click=${this._removeRepository}
>
<ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
@@ -105,13 +117,13 @@ class HassioRepositoriesDialog extends LitElement {
<paper-input
class="flex-auto"
id="repository_input"
label="Add repository"
.label=${this.supervisor.localize("dialog.repositories.add")}
@keydown=${this._handleKeyAdd}
></paper-input>
<mwc-button @click=${this._addRepository}>
${this._prosessing
? html`<ha-circular-progress active></ha-circular-progress>`
: "Add"}
: this.supervisor.localize("dialog.repositories.add")}
</mwc-button>
</div>
</div>

View File

@@ -1,9 +1,9 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioAddonRepository } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import "./dialog-hassio-repositories";
export interface HassioRepositoryDialogParams {
repos: HassioAddonRepository[];
supervisor: Supervisor;
loadData: () => Promise<void>;
}

View File

@@ -22,7 +22,11 @@ import {
fetchHassioSnapshotInfo,
HassioSnapshotDetail,
} from "../../../../src/data/hassio/snapshot";
import { showConfirmationDialog } from "../../../../src/dialogs/generic/show-dialog-box";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
} from "../../../../src/dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../../../../src/polymer-types";
import { haStyle, haStyleDialog } from "../../../../src/resources/styles";
import { HomeAssistant } from "../../../../src/types";
@@ -75,6 +79,8 @@ interface FolderItem {
class HassioSnapshotDialog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor?: Supervisor;
@internalProperty() private _error?: string;
@internalProperty() private _onboarding = false;
@@ -89,7 +95,7 @@ class HassioSnapshotDialog extends LitElement {
@internalProperty() private _snapshotPassword!: string;
@internalProperty() private _restoreHass: boolean | null | undefined = true;
@internalProperty() private _restoreHass = true;
public async showDialog(params: HassioSnapshotDialogParams) {
this._snapshot = await fetchHassioSnapshotInfo(this.hass, params.slug);
@@ -102,6 +108,10 @@ class HassioSnapshotDialog extends LitElement {
this._dialogParams = params;
this._onboarding = params.onboarding ?? false;
this.supervisor = params.supervisor;
if (!this._snapshot.homeassistant) {
this._restoreHass = false;
}
}
protected render(): TemplateResult {
@@ -127,15 +137,17 @@ class HassioSnapshotDialog extends LitElement {
(${this._computeSize})<br />
${this._formatDatetime(this._snapshot.date)}
</div>
<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked;
}}"
>
Home Assistant ${this._snapshot.homeassistant}
</paper-checkbox>
${this._snapshot.homeassistant
? html`<div>Home Assistant:</div>
<paper-checkbox
.checked=${this._restoreHass}
@change="${(ev: Event) => {
this._restoreHass = (ev.target as PaperCheckboxElement).checked!;
}}"
>
Home Assistant ${this._snapshot.homeassistant}
</paper-checkbox>`
: ""}
${this._folders.length
? html`
<div>Folders:</div>
@@ -298,6 +310,16 @@ class HassioSnapshotDialog extends LitElement {
}
private async _partialRestoreClicked() {
if (
this.supervisor !== undefined &&
this.supervisor.info.state !== "running"
) {
await showAlertDialog(this, {
title: "Could not restore snapshot",
text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
});
return;
}
if (
!(await showConfirmationDialog(this, {
title: "Are you sure you want partially to restore this snapshot?",
@@ -317,7 +339,7 @@ class HassioSnapshotDialog extends LitElement {
.map((folder) => folder.slug);
const data: {
homeassistant: boolean | null | undefined;
homeassistant: boolean;
addons: any;
folders: any;
password?: string;
@@ -359,6 +381,16 @@ class HassioSnapshotDialog extends LitElement {
}
private async _fullRestoreClicked() {
if (
this.supervisor !== undefined &&
this.supervisor.info.state !== "running"
) {
await showAlertDialog(this, {
title: "Could not restore snapshot",
text: `Restoring a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
});
return;
}
if (
!(await showConfirmationDialog(this, {
title:

View File

@@ -1,9 +1,11 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface HassioSnapshotDialogParams {
slug: string;
onDelete?: () => void;
onboarding?: boolean;
supervisor?: Supervisor;
}
export const showHassioSnapshotDialog = (

View File

@@ -4,6 +4,7 @@ import {
restartHassioAddon,
} from "../../../src/data/hassio/addon";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
showConfirmationDialog,
@@ -13,20 +14,25 @@ import { HomeAssistant } from "../../../src/types";
export const suggestAddonRestart = async (
element: LitElement,
hass: HomeAssistant,
supervisor: Supervisor,
addon: HassioAddonDetails
): Promise<void> => {
const confirmed = await showConfirmationDialog(element, {
title: addon.name,
text: "Do you want to restart the add-on with your changes?",
confirmText: "restart add-on",
dismissText: "no",
title: supervisor.localize("common.restart_name", "name", addon.name),
text: supervisor.localize("dialog.restart_addon.text"),
confirmText: supervisor.localize("dialog.restart_addon.confirm_text"),
dismissText: supervisor.localize("common.cancel"),
});
if (confirmed) {
try {
await restartHassioAddon(hass, addon.slug);
} catch (err) {
showAlertDialog(element, {
title: "Failed to restart",
title: supervisor.localize(
"common.failed_to_restart_name",
"name",
addon.name
),
text: extractApiErrorMessage(err),
});
}

View File

@@ -3,7 +3,9 @@ import { atLeastVersion } from "../../src/common/config/version";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import "../../src/layouts/hass-loading-screen";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-router";
import { SupervisorBaseElement } from "./supervisor-base-element";
@@ -12,6 +14,8 @@ import { SupervisorBaseElement } from "./supervisor-base-element";
export class HassioMain extends SupervisorBaseElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow!: boolean;
@@ -70,9 +74,6 @@ export class HassioMain extends SupervisorBaseElement {
}
protected render() {
if (!this.supervisor || !this.hass) {
return html``;
}
return html`
<hassio-router
.hass=${this.hass}

View File

@@ -0,0 +1,133 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { sanitizeUrl } from "@braintree/sanitize-url";
import {
createSearchParam,
extractSearchParamsObject,
} from "../../src/common/url/search-params";
import "../../src/layouts/hass-error-screen";
import {
ParamType,
Redirect,
Redirects,
} from "../../src/panels/my/ha-panel-my";
import { navigate } from "../../src/common/navigate";
import { HomeAssistant, Route } from "../../src/types";
import { Supervisor } from "../../src/data/supervisor/supervisor";
const REDIRECTS: Redirects = {
supervisor_logs: {
redirect: "/hassio/system",
},
supervisor_info: {
redirect: "/hassio/system",
},
supervisor_snapshots: {
redirect: "/hassio/snapshots",
},
supervisor_store: {
redirect: "/hassio/store",
},
supervisor: {
redirect: "/hassio/dashboard",
},
supervisor_addon: {
redirect: "/hassio/addon",
params: {
addon: "string",
},
},
};
@customElement("hassio-my-redirect")
class HassioMyRedirect extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route;
@internalProperty() public _error?: TemplateResult | string;
connectedCallback() {
super.connectedCallback();
const path = this.route.path.substr(1);
const redirect = REDIRECTS[path];
if (!redirect) {
this._error = this.supervisor.localize(
"my.not_supported",
"link",
html`<a
target="_blank"
rel="noreferrer noopener"
href="https://my.home-assistant.io/faq.html#supported-pages"
>
${this.supervisor.localize("my.faq_link")}
</a>`
);
return;
}
let url: string;
try {
url = this._createRedirectUrl(redirect);
} catch (err) {
this._error = this.supervisor.localize("my.error");
return;
}
navigate(this, url, true);
}
protected render(): TemplateResult {
if (this._error) {
return html`<hass-error-screen
.error=${this._error}
></hass-error-screen>`;
}
return html``;
}
private _createRedirectUrl(redirect: Redirect): string {
const params = this._createRedirectParams(redirect);
return `${redirect.redirect}${params}`;
}
private _createRedirectParams(redirect: Redirect): string {
const params = extractSearchParamsObject();
if (!redirect.params && !Object.keys(params).length) {
return "";
}
const resultParams = {};
Object.entries(redirect.params || {}).forEach(([key, type]) => {
if (!params[key] || !this._checkParamType(type, params[key])) {
throw Error();
}
resultParams[key] = params[key];
});
return `?${createSearchParam(resultParams)}`;
}
private _checkParamType(type: ParamType, value: string) {
if (type === "string") {
return true;
}
if (type === "url") {
return value && value === sanitizeUrl(value);
}
return false;
}
}
declare global {
interface HTMLElementTagNameMap {
"hassio-my-redirect": HassioMyRedirect;
}
}

View File

@@ -7,7 +7,10 @@ import {
property,
TemplateResult,
} from "lit-element";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import {
Supervisor,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router";
@@ -22,6 +25,17 @@ class HassioPanel extends LitElement {
@property({ attribute: false }) public route!: Route;
protected render(): TemplateResult {
if (!this.hass) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
if (
Object.keys(supervisorCollection).some(
(colllection) => !this.supervisor[colllection]
)
) {
return html`<hass-loading-screen></hass-loading-screen>`;
}
return html`
<hassio-panel-router
.hass=${this.hass}

View File

@@ -23,7 +23,7 @@ class HassioRouter extends HassRouterPage {
protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it.
defaultPage: "dashboard",
initialLoad: () => this._fetchData(),
initialLoad: () => this._redirectIngress(),
showLoading: true,
routes: {
dashboard: {
@@ -41,32 +41,42 @@ class HassioRouter extends HassRouterPage {
tag: "hassio-ingress-view",
load: () => import("./ingress-view/hassio-ingress-view"),
},
_my_redirect: {
tag: "hassio-my-redirect",
load: () => import("./hassio-my-redirect"),
},
},
};
protected updatePageEl(el) {
// the tabs page does its own routing so needs full route.
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
const hassioPanel = el.nodeName === "HASSIO-PANEL";
const route = hassioPanel ? this.route : this.routeTail;
if (hassioPanel && this.panel.config?.ingress) {
this._redirectIngress();
return;
}
el.hass = this.hass;
el.supervisor = this.supervisor;
el.narrow = this.narrow;
el.route = route;
if (el.localName === "hassio-ingress-view") {
el.ingressPanel = this.panel.config && this.panel.config.ingress;
} else {
el.supervisor = this.supervisor;
}
}
private async _fetchData() {
private async _redirectIngress() {
if (this.panel.config && this.panel.config.ingress) {
this._redirectIngress(this.panel.config.ingress);
this.route = {
prefix: "/hassio",
path: `/ingress/${this.panel.config.ingress}`,
};
}
}
private _redirectIngress(addonSlug: string) {
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
}
}
declare global {

View File

@@ -3,22 +3,22 @@ import type { PageNavigation } from "../../src/layouts/hass-tabs-subpage";
export const supervisorTabs: PageNavigation[] = [
{
name: "Dashboard",
translationKey: "panel.dashboard",
path: `/hassio/dashboard`,
iconPath: mdiViewDashboard,
},
{
name: "Add-on Store",
translationKey: "panel.store",
path: `/hassio/store`,
iconPath: mdiStore,
},
{
name: "Snapshots",
translationKey: "panel.snapshots",
path: `/hassio/snapshots`,
iconPath: mdiBackupRestore,
},
{
name: "System",
translationKey: "panel.system",
path: `/hassio/system`,
iconPath: mdiCogs,
},

View File

@@ -41,6 +41,7 @@ import {
reloadHassioSnapshots,
} from "../../../src/data/hassio/snapshot";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { showAlertDialog } from "../../../src/dialogs/generic/show-dialog-box";
import "../../../src/layouts/hass-tabs-subpage";
import { PolymerChangedEvent } from "../../../src/polymer-types";
import { haStyle } from "../../../src/resources/styles";
@@ -54,8 +55,8 @@ import { hassioStyle } from "../resources/hassio-style";
interface CheckboxItem {
slug: string;
name: string;
checked: boolean;
name?: string;
}
@customElement("hassio-snapshots")
@@ -83,13 +84,12 @@ class HassioSnapshots extends LitElement {
@internalProperty() private _folderList: CheckboxItem[] = [
{
slug: "homeassistant",
name: "Home Assistant configuration",
checked: true,
},
{ slug: "ssl", name: "SSL", checked: true },
{ slug: "share", name: "Share", checked: true },
{ slug: "media", name: "Media", checked: true },
{ slug: "addons/local", name: "Local add-ons", checked: true },
{ slug: "ssl", checked: true },
{ slug: "share", checked: true },
{ slug: "media", checked: true },
{ slug: "addons/local", checked: true },
];
@internalProperty() private _error = "";
@@ -103,13 +103,16 @@ class HassioSnapshots extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">Snapshots</span>
<span slot="header">
${this.supervisor.localize("panel.snapshots")}
</span>
<ha-button-menu
corner="BOTTOM_START"
slot="toolbar-icon"
@@ -119,50 +122,50 @@ class HassioSnapshots extends LitElement {
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item>
Reload
${this.supervisor.localize("common.reload")}
</mwc-list-item>
${atLeastVersion(this.hass.config.version, 0, 116)
? html`<mwc-list-item>
Upload snapshot
${this.supervisor.localize("snapshot.upload_snapshot")}
</mwc-list-item>`
: ""}
</ha-button-menu>
<div class="content">
<h1>
Create Snapshot
${this.supervisor.localize("snapshot.create_snapshot")}
</h1>
<p class="description">
Snapshots allow you to easily backup and restore all data of your
Home Assistant instance.
${this.supervisor.localize("snapshot.description")}
</p>
<div class="card-group">
<ha-card>
<div class="card-content">
<paper-input
autofocus
label="Name"
.label=${this.supervisor.localize("snapshot.name")}
name="snapshotName"
.value=${this._snapshotName}
@value-changed=${this._handleTextValueChanged}
></paper-input>
Type:
${this.supervisor.localize("snapshot.type")}:
<paper-radio-group
name="snapshotType"
type="${this.supervisor.localize("snapshot.type")}"
.selected=${this._snapshotType}
@selected-changed=${this._handleRadioValueChanged}
>
<paper-radio-button name="full">
Full snapshot
${this.supervisor.localize("snapshot.full_snapshot")}
</paper-radio-button>
<paper-radio-button name="partial">
Partial snapshot
${this.supervisor.localize("snapshot.partial_snapshot")}
</paper-radio-button>
</paper-radio-group>
${this._snapshotType === "full"
? undefined
: html`
Folders:
${this.supervisor.localize("snapshot.folders")}:
${this._folderList.map(
(folder, idx) => html`
<paper-checkbox
@@ -170,11 +173,13 @@ class HassioSnapshots extends LitElement {
.checked=${folder.checked}
@checked-changed=${this._folderChecked}
>
${folder.name}
${this.supervisor.localize(
`snapshot.folder.${folder.slug}`
)}
</paper-checkbox>
`
)}
Add-ons:
${this.supervisor.localize("snapshot.addons")}:
${this._addonList.map(
(addon, idx) => html`
<paper-checkbox
@@ -187,18 +192,18 @@ class HassioSnapshots extends LitElement {
`
)}
`}
Security:
${this.supervisor.localize("snapshot.security")}:
<paper-checkbox
name="snapshotHasPassword"
.checked=${this._snapshotHasPassword}
@checked-changed=${this._handleCheckboxValueChanged}
>
Password protection
${this.supervisor.localize("snapshot.password_protection")}
</paper-checkbox>
${this._snapshotHasPassword
? html`
<paper-input
label="Password"
.label=${this.supervisor.localize("snapshot.password")}
type="password"
name="snapshotPassword"
.value=${this._snapshotPassword}
@@ -211,14 +216,24 @@ class HassioSnapshots extends LitElement {
: undefined}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._createSnapshot}>
Create
<ha-progress-button
@click=${this._createSnapshot}
.title=${this.supervisor.info.state !== "running"
? this.supervisor.localize(
"snapshot.create_blocked_not_running",
"state",
this.supervisor.info.state
)
: ""}
.disabled=${this.supervisor.info.state !== "running"}
>
${this.supervisor.localize("snapshot.create")}
</ha-progress-button>
</div>
</ha-card>
</div>
<h1>Available Snapshots</h1>
<h1>${this.supervisor.localize("snapshot.available_snapshots")}</h1>
<div class="card-group">
${this._snapshots === undefined
? undefined
@@ -226,7 +241,7 @@ class HassioSnapshots extends LitElement {
? html`
<ha-card>
<div class="card-content">
You don't have any snapshots yet.
${this.supervisor.localize("snapshot.no_snapshots")}
</div>
</ha-card>
`
@@ -325,12 +340,22 @@ class HassioSnapshots extends LitElement {
}
private async _createSnapshot(ev: CustomEvent): Promise<void> {
if (this.supervisor.info.state !== "running") {
await showAlertDialog(this, {
title: this.supervisor.localize("snapshot.could_not_create"),
text: this.supervisor.localize(
"snapshot.create_blocked_not_running",
"state",
this.supervisor.info.state
),
});
}
const button = ev.currentTarget as any;
button.progress = true;
this._error = "";
if (this._snapshotHasPassword && !this._snapshotPassword.length) {
this._error = "Please enter a password.";
this._error = this.supervisor.localize("snapshot.enter_password");
button.progress = false;
return;
}
@@ -379,13 +404,16 @@ class HassioSnapshots extends LitElement {
private _computeDetails(snapshot: HassioSnapshot) {
const type =
snapshot.type === "full" ? "Full snapshot" : "Partial snapshot";
snapshot.type === "full"
? this.supervisor.localize("snapshot.full_snapshot")
: this.supervisor.localize("snapshot.partial_snapshot");
return snapshot.protected ? `${type}, password protected` : type;
}
private _snapshotClicked(ev) {
showHassioSnapshotDialog(this, {
slug: ev.currentTarget!.snapshot.slug,
supervisor: this.supervisor,
onDelete: () => this._updateSnapshots(),
});
}
@@ -395,6 +423,7 @@ class HassioSnapshots extends LitElement {
showSnapshot: (slug: string) =>
showHassioSnapshotDialog(this, {
slug,
supervisor: this.supervisor,
onDelete: () => this._updateSnapshots(),
}),
reloadSnapshot: () => this.refreshData(),

View File

@@ -1,4 +1,14 @@
import { LitElement, property, PropertyValues } from "lit-element";
import { Collection, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
internalProperty,
LitElement,
property,
PropertyValues,
} from "lit-element";
import { atLeastVersion } from "../../src/common/config/version";
import { computeLocalize } from "../../src/common/translations/localize";
import { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import { HassioResponse } from "../../src/data/hassio/common";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
@@ -10,60 +20,186 @@ import {
fetchHassioInfo,
fetchHassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { Supervisor } from "../../src/data/supervisor/supervisor";
import { fetchSupervisorStore } from "../../src/data/supervisor/store";
import {
getSupervisorEventCollection,
subscribeSupervisorEvents,
Supervisor,
SupervisorObject,
supervisorCollection,
} from "../../src/data/supervisor/supervisor";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { getTranslation } from "../../src/util/common-translation";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-colllection-refresh": { colllection: SupervisorObject };
}
}
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public supervisor?: Supervisor;
@property({ attribute: false }) public supervisor: Partial<Supervisor> = {
localize: () => "",
};
@internalProperty() private _unsubs: Record<string, UnsubscribeFunc> = {};
@internalProperty() private _collections: Record<
string,
Collection<unknown>
> = {};
@internalProperty() private _resources?: Record<string, any>;
@internalProperty() private _language = "en";
public connectedCallback(): void {
super.connectedCallback();
this._initializeLocalize();
}
public disconnectedCallback() {
super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => {
this._unsubs[unsub]();
});
}
protected updated(changedProperties: PropertyValues) {
super.updated(changedProperties);
if (changedProperties.has("_language")) {
if (changedProperties.get("_language") !== this._language) {
this._initializeLocalize();
}
}
}
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor!, ...obj };
this.supervisor = { ...this.supervisor, ...obj };
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
if (this._language !== this.hass.language) {
this._language = this.hass.language;
}
this._initializeLocalize();
this._initSupervisor();
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
}
private async _initializeLocalize() {
const { language, data } = await getTranslation(
null,
this._language,
"/api/hassio/app/static/translations"
);
this._resources = {
[language]: data,
};
this.supervisor = {
...this.supervisor,
localize: await computeLocalize(
this.constructor.prototype,
this._language,
this._resources
),
};
}
private async _handleSupervisorStoreRefreshEvent(ev) {
const colllection = ev.detail.colllection;
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
this._collections[colllection].refresh();
return;
}
const response = await this.hass.callApi<HassioResponse<any>>(
"GET",
`hassio${supervisorCollection[colllection]}`
);
this._updateSupervisor({ [colllection]: response.data });
}
private async _initSupervisor(): Promise<void> {
const [
supervisor,
host,
core,
info,
os,
network,
resolution,
] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
]);
this.addEventListener(
"supervisor-colllection-refresh",
this._handleSupervisorStoreRefreshEvent
);
this.supervisor = {
supervisor,
host,
core,
info,
os,
network,
resolution,
};
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
Object.keys(supervisorCollection).forEach((colllection) => {
this._unsubs[colllection] = subscribeSupervisorEvents(
this.hass,
(data) => this._updateSupervisor({ [colllection]: data }),
colllection,
supervisorCollection[colllection]
);
if (this._collections[colllection]) {
this._collections[colllection].refresh();
} else {
this._collections[colllection] = getSupervisorEventCollection(
this.hass.connection,
colllection,
supervisorCollection[colllection]
);
}
});
Object.keys(this._collections).forEach((collection) => {
if (
this.supervisor === undefined ||
this.supervisor[collection] === undefined
) {
this._updateSupervisor({
[collection]: this._collections[collection].state,
});
}
});
} else {
const [
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this.supervisor = {
addon,
supervisor,
host,
core,
info,
os,
network,
resolution,
store,
};
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
}
}

View File

@@ -19,7 +19,7 @@ import {
fetchHassioStats,
HassioStats,
} from "../../../src/data/hassio/common";
import { restartCore, updateCore } from "../../../src/data/supervisor/core";
import { restartCore } from "../../../src/data/supervisor/core";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
showAlertDialog,
@@ -29,6 +29,7 @@ import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string";
import "../components/supervisor-metric";
import { showDialogSupervisorCoreUpdate } from "../dialogs/core/show-dialog-core-update";
import { hassioStyle } from "../resources/hassio-style";
@customElement("hassio-core-info")
@@ -42,11 +43,11 @@ class HassioCoreInfo extends LitElement {
protected render(): TemplateResult | void {
const metrics = [
{
description: "Core CPU Usage",
description: this.supervisor.localize("system.core.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: "Core RAM Usage",
description: this.supervisor.localize("system.core.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -60,7 +61,7 @@ class HassioCoreInfo extends LitElement {
<div>
<ha-settings-row>
<span slot="heading">
Version
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
core-${this.supervisor.core.version}
@@ -68,7 +69,7 @@ class HassioCoreInfo extends LitElement {
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Newest Version
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
core-${this.supervisor.core.version_latest}
@@ -76,10 +77,10 @@ class HassioCoreInfo extends LitElement {
${this.supervisor.core.update_available
? html`
<ha-progress-button
title="Update the core"
.title=${this.supervisor.localize("common.update")}
@click=${this._coreUpdate}
>
Update
${this.supervisor.localize("common.update")}
</ha-progress-button>
`
: ""}
@@ -103,9 +104,13 @@ class HassioCoreInfo extends LitElement {
slot="primaryAction"
class="warning"
@click=${this._coreRestart}
title="Restart Home Assistant Core"
.title=${this.supervisor.localize(
"common.restart_name",
"name",
"Core"
)}
>
Restart Core
${this.supervisor.localize("common.restart_name", "name", "Core")}
</ha-progress-button>
</div>
</ha-card>
@@ -125,10 +130,18 @@ class HassioCoreInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Restart Home Assistant Core",
text: "Are you sure you want to restart Home Assistant Core",
confirmText: "restart",
dismissText: "cancel",
title: this.supervisor.localize(
"confirm.restart.title",
"name",
"Home Assistant Core"
),
text: this.supervisor.localize(
"confirm.restart.text",
"name",
"Home Assistant Core"
),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -139,41 +152,23 @@ class HassioCoreInfo extends LitElement {
try {
await restartCore(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to restart Home Assistant Core",
text: extractApiErrorMessage(err),
});
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_restart_name",
"name",
"Home AssistantCore"
),
text: extractApiErrorMessage(err),
});
}
} finally {
button.progress = false;
}
}
private async _coreUpdate(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update Home Assistant Core",
text: `Are you sure you want to update Home Assistant Core to version ${this.supervisor.core.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
});
if (!confirmed) {
button.progress = false;
return;
}
try {
await updateCore(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to update Home Assistant Core",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
}
private async _coreUpdate(): Promise<void> {
showDialogSupervisorCoreUpdate(this, { core: this.supervisor.core });
}
static get styles(): CSSResult[] {

View File

@@ -13,6 +13,7 @@ import {
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { atLeastVersion } from "../../../src/common/config/version";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu";
@@ -26,7 +27,6 @@ import { fetchHassioHardwareInfo } from "../../../src/data/hassio/hardware";
import {
changeHostOptions,
configSyncOS,
fetchHassioHostInfo,
rebootHost,
shutdownHost,
updateOS,
@@ -65,7 +65,7 @@ class HassioHostInfo extends LitElement {
const metrics = [
{
description: "Used Space",
description: this.supervisor.localize("system.host.used_space"),
value: this._getUsedSpace(
this.supervisor.host.disk_used,
this.supervisor.host.disk_total
@@ -80,14 +80,13 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.features.includes("hostname")
? html`<ha-settings-row>
<span slot="heading">
Hostname
${this.supervisor.localize("system.host.hostname")}
</span>
<span slot="description">
${this.supervisor.host.hostname}
</span>
<mwc-button
title="Change the hostname"
label="Change"
.label=${this.supervisor.localize("system.host.change")}
@click=${this._changeHostnameClicked}
>
</mwc-button>
@@ -96,14 +95,13 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.features.includes("network")
? html` <ha-settings-row>
<span slot="heading">
IP Address
${this.supervisor.localize("system.host.ip_address")}
</span>
<span slot="description">
${primaryIpAddress}
</span>
<mwc-button
title="Change the network"
label="Change"
.label=${this.supervisor.localize("system.host.change")}
@click=${this._changeNetworkClicked}
>
</mwc-button>
@@ -112,18 +110,15 @@ class HassioHostInfo extends LitElement {
<ha-settings-row>
<span slot="heading">
Operating System
${this.supervisor.localize("system.host.operating_system")}
</span>
<span slot="description">
${this.supervisor.host.operating_system}
</span>
${this.supervisor.os.update_available
? html`
<ha-progress-button
title="Update the host OS"
@click=${this._osUpdate}
>
Update
<ha-progress-button @click=${this._osUpdate}>
${this.supervisor.localize("commmon.update")}
</ha-progress-button>
`
: ""}
@@ -131,7 +126,7 @@ class HassioHostInfo extends LitElement {
${!this.supervisor.host.features.includes("hassos")
? html`<ha-settings-row>
<span slot="heading">
Docker version
${this.supervisor.localize("system.host.docker_version")}
</span>
<span slot="description">
${this.supervisor.info.docker}
@@ -141,7 +136,7 @@ class HassioHostInfo extends LitElement {
${this.supervisor.host.deployment
? html`<ha-settings-row>
<span slot="heading">
Deployment
${this.supervisor.localize("system.host.deployment")}
</span>
<span slot="description">
${this.supervisor.host.deployment}
@@ -150,6 +145,20 @@ class HassioHostInfo extends LitElement {
: ""}
</div>
<div>
${this.supervisor.host.disk_life_time !== "" &&
this.supervisor.host.disk_life_time >= 10
? html` <ha-settings-row>
<span slot="heading">
${this.supervisor.localize(
"system.host.emmc_lifetime_used"
)}
</span>
<span slot="description">
${this.supervisor.host.disk_life_time - 10}% -
${this.supervisor.host.disk_life_time}%
</span>
</ha-settings-row>`
: ""}
${metrics.map(
(metric) =>
html`
@@ -165,23 +174,18 @@ class HassioHostInfo extends LitElement {
<div class="card-actions">
${this.supervisor.host.features.includes("reboot")
? html`
<ha-progress-button
title="Reboot the host OS"
class="warning"
@click=${this._hostReboot}
>
Reboot Host
<ha-progress-button class="warning" @click=${this._hostReboot}>
${this.supervisor.localize("system.host.reboot_host")}
</ha-progress-button>
`
: ""}
${this.supervisor.host.features.includes("shutdown")
? html`
<ha-progress-button
title="Shutdown the host OS"
class="warning"
@click=${this._hostShutdown}
>
Shutdown Host
${this.supervisor.localize("system.host.shutdown_host")}
</ha-progress-button>
`
: ""}
@@ -193,14 +197,12 @@ class HassioHostInfo extends LitElement {
<mwc-icon-button slot="trigger">
<ha-svg-icon .path=${mdiDotsVertical}></ha-svg-icon>
</mwc-icon-button>
<mwc-list-item title="Show a list of hardware">
Hardware
<mwc-list-item>
${this.supervisor.localize("system.host.hardware")}
</mwc-list-item>
${this.supervisor.host.features.includes("hassos")
? html`<mwc-list-item
title="Load HassOS configs or updates from USB"
>
Import from USB
? html`<mwc-list-item>
${this.supervisor.localize("system.host.import_from_usb")}
</mwc-list-item>`
: ""}
</ha-button-menu>
@@ -239,12 +241,14 @@ class HassioHostInfo extends LitElement {
try {
const content = await fetchHassioHardwareInfo(this.hass);
showHassioMarkdownDialog(this, {
title: "Hardware",
title: this.supervisor.localize("system.host.hardware"),
content: `<pre>${safeDump(content, { indent: 2 })}</pre>`,
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to get hardware list",
title: this.supervisor.localize(
"system.host.failed_to_get_hardware_list"
),
text: extractApiErrorMessage(err),
});
}
@@ -255,10 +259,10 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Reboot",
text: "Are you sure you want to reboot the host?",
confirmText: "reboot host",
dismissText: "no",
title: this.supervisor.localize("system.host.reboot_host"),
text: this.supervisor.localize("system.host.confirm_reboot"),
confirmText: this.supervisor.localize("system.host.reboot_host"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -272,7 +276,7 @@ class HassioHostInfo extends LitElement {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: "Failed to reboot",
title: this.supervisor.localize("system.host.failed_to_reboot"),
text: extractApiErrorMessage(err),
});
}
@@ -285,10 +289,10 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Shutdown",
text: "Are you sure you want to shutdown the host?",
confirmText: "shutdown host",
dismissText: "no",
title: this.supervisor.localize("system.host.shutdown_host"),
text: this.supervisor.localize("system.host.confirm_shutdown"),
confirmText: this.supervisor.localize("system.host.shutdown_host"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -302,7 +306,7 @@ class HassioHostInfo extends LitElement {
// Ignore connection errors, these are all expected
if (err.status_code && !ignoredStatusCodes.has(err.status_code)) {
showAlertDialog(this, {
title: "Failed to shutdown",
title: this.supervisor.localize("system.host.failed_to_shutdown"),
text: extractApiErrorMessage(err),
});
}
@@ -315,9 +319,19 @@ class HassioHostInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update",
text: "Are you sure you want to update the OS?",
confirmText: "update os",
title: this.supervisor.localize(
"confirm.update.title",
"name",
"Home Assistant Operating System"
),
text: this.supervisor.localize(
"confirm.update.text",
"name",
"Home Assistant Operating System",
"version",
this.supervisor.os.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: "no",
});
@@ -328,18 +342,25 @@ class HassioHostInfo extends LitElement {
try {
await updateOS(this.hass);
fireEvent(this, "supervisor-colllection-refresh", { colllection: "os" });
} catch (err) {
showAlertDialog(this, {
title: "Failed to update",
text: extractApiErrorMessage(err),
});
if (this.hass.connection.connected) {
showAlertDialog(this, {
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Home Assistant Operating System"
),
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, {
network: this.supervisor.network!,
supervisor: this.supervisor,
loadData: () => this._loadData(),
});
}
@@ -347,20 +368,22 @@ class HassioHostInfo extends LitElement {
private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.supervisor.host.hostname;
const hostname = await showPromptDialog(this, {
title: "Change Hostname",
inputLabel: "Please enter a new hostname:",
title: this.supervisor.localize("system.host.change_hostname"),
inputLabel: this.supervisor.localize("system.host.new_hostname"),
inputType: "string",
defaultValue: curHostname,
confirmText: this.supervisor.localize("common.update"),
});
if (hostname && hostname !== curHostname) {
try {
await changeHostOptions(this.hass, { hostname });
const host = await fetchHassioHostInfo(this.hass);
fireEvent(this, "supervisor-update", { host });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "host",
});
} catch (err) {
showAlertDialog(this, {
title: "Setting hostname failed",
title: this.supervisor.localize("system.host.failed_to_set_hostname"),
text: extractApiErrorMessage(err),
});
}
@@ -370,19 +393,28 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> {
try {
await configSyncOS(this.hass);
const host = await fetchHassioHostInfo(this.hass);
fireEvent(this, "supervisor-update", { host });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "host",
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to import from USB",
title: this.supervisor.localize(
"system.host.failed_to_import_from_usb"
),
text: extractApiErrorMessage(err),
});
}
}
private async _loadData(): Promise<void> {
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
if (atLeastVersion(this.hass.config.version, 2021, 2, 4)) {
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "network",
});
} else {
const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
}
}
static get styles(): CSSResult[] {

View File

@@ -19,7 +19,6 @@ import {
HassioStats,
} from "../../../src/data/hassio/common";
import {
fetchHassioSupervisorInfo,
reloadSupervisor,
restartSupervisor,
setSupervisorOption,
@@ -38,54 +37,24 @@ import { documentationUrl } from "../../../src/util/documentation-url";
import "../components/supervisor-metric";
import { hassioStyle } from "../resources/hassio-style";
const UNSUPPORTED_REASON = {
container: {
title: "Containers known to cause issues",
url: "/more-info/unsupported/container",
},
dbus: { title: "DBUS", url: "/more-info/unsupported/dbus" },
docker_configuration: {
title: "Docker Configuration",
url: "/more-info/unsupported/docker_configuration",
},
docker_version: {
title: "Docker Version",
url: "/more-info/unsupported/docker_version",
},
job_conditions: {
title: "Ignored job conditions",
url: "/more-info/unsupported/job_conditions",
},
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
network_manager: {
title: "Network Manager",
url: "/more-info/unsupported/network_manager",
},
os: { title: "Operating System", url: "/more-info/unsupported/os" },
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
const UNSUPPORTED_REASON_URL = {
container: "/more-info/unsupported/container",
dbus: "/more-info/unsupported/dbus",
docker_configuration: "/more-info/unsupported/docker_configuration",
docker_version: "/more-info/unsupported/docker_version",
job_conditions: "/more-info/unsupported/job_conditions",
lxc: "/more-info/unsupported/lxc",
network_manager: "/more-info/unsupported/network_manager",
os: "/more-info/unsupported/os",
privileged: "/more-info/unsupported/privileged",
systemd: "/more-info/unsupported/systemd",
};
const UNHEALTHY_REASON = {
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
supervisor: {
title: "Supervisor was not able to update",
url: "/more-info/unhealthy/supervisor",
},
setup: {
title: "Setup of the Supervisor failed",
url: "/more-info/unhealthy/setup",
},
docker: {
title: "The Docker environment is not working properly",
url: "/more-info/unhealthy/docker",
},
const UNHEALTHY_REASON_URL = {
privileged: "/more-info/unsupported/privileged",
supervisor: "/more-info/unhealthy/supervisor",
setup: "/more-info/unhealthy/setup",
docker: "/more-info/unhealthy/docker",
};
@customElement("hassio-supervisor-info")
@@ -99,11 +68,11 @@ class HassioSupervisorInfo extends LitElement {
protected render(): TemplateResult | void {
const metrics = [
{
description: "Supervisor CPU Usage",
description: this.supervisor.localize("system.supervisor.cpu_usage"),
value: this._metrics?.cpu_percent,
},
{
description: "Supervisor RAM Usage",
description: this.supervisor.localize("system.supervisor.ram_usage"),
value: this._metrics?.memory_percent,
tooltip: `${bytesToString(this._metrics?.memory_usage)}/${bytesToString(
this._metrics?.memory_limit
@@ -116,7 +85,7 @@ class HassioSupervisorInfo extends LitElement {
<div>
<ha-settings-row>
<span slot="heading">
Version
${this.supervisor.localize("common.version")}
</span>
<span slot="description">
supervisor-${this.supervisor.supervisor.version}
@@ -124,7 +93,7 @@ class HassioSupervisorInfo extends LitElement {
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Newest Version
${this.supervisor.localize("common.newest_version")}
</span>
<span slot="description">
supervisor-${this.supervisor.supervisor.version_latest}
@@ -132,17 +101,19 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.supervisor.update_available
? html`
<ha-progress-button
title="Update the supervisor"
.title=${this.supervisor.localize(
"system.supervisor.update_supervisor"
)}
@click=${this._supervisorUpdate}
>
Update
${this.supervisor.localize("common.update")}
</ha-progress-button>
`
: ""}
</ha-settings-row>
<ha-settings-row>
<span slot="heading">
Channel
${this.supervisor.localize("system.supervisor.channel")}
</span>
<span slot="description">
${this.supervisor.supervisor.channel}
@@ -151,18 +122,26 @@ class HassioSupervisorInfo extends LitElement {
? html`
<ha-progress-button
@click=${this._toggleBeta}
title="Get stable updates for Home Assistant, supervisor and host"
.title=${this.supervisor.localize(
"system.supervisor.leave_beta_description"
)}
>
Leave beta channel
${this.supervisor.localize(
"system.supervisor.leave_beta_action"
)}
</ha-progress-button>
`
: this.supervisor.supervisor.channel === "stable"
? html`
<ha-progress-button
@click=${this._toggleBeta}
title="Get beta updates for Home Assistant (RCs), supervisor and host"
.title=${this.supervisor.localize(
"system.supervisor.join_beta_description"
)}
>
Join beta channel
${this.supervisor.localize(
"system.supervisor.join_beta_action"
)}
</ha-progress-button>
`
: ""}
@@ -171,16 +150,20 @@ class HassioSupervisorInfo extends LitElement {
${this.supervisor.supervisor.supported
? html` <ha-settings-row three-line>
<span slot="heading">
Share Diagnostics
${this.supervisor.localize(
"system.supervisor.share_diagnostics"
)}
</span>
<div slot="description" class="diagnostics-description">
Share crash reports and diagnostic information.
${this.supervisor.localize(
"system.supervisor.share_diagnostics_description"
)}
<button
class="link"
title="Show more information about this"
.title=${this.supervisor.localize("common.show_more")}
@click=${this._diagnosticsInformationDialog}
>
Learn more
${this.supervisor.localize("common.learn_more")}
</button>
</div>
<ha-switch
@@ -190,10 +173,12 @@ class HassioSupervisorInfo extends LitElement {
></ha-switch>
</ha-settings-row>`
: html`<div class="error">
You are running an unsupported installation.
${this.supervisor.localize(
"system.supervisor.unsupported_title"
)}
<button
class="link"
title="Learn more about how you can make your system compliant"
.title=${this.supervisor.localize("common.learn_more")}
@click=${this._unsupportedDialog}
>
Learn more
@@ -201,10 +186,12 @@ class HassioSupervisorInfo extends LitElement {
</div>`}
${!this.supervisor.supervisor.healthy
? html`<div class="error">
Your installation is running in an unhealthy state.
${this.supervisor.localize(
"system.supervisor.unhealthy_title"
)}
<button
class="link"
title="Learn more about why your system is marked as unhealthy"
.title=${this.supervisor.localize("common.learn_more")}
@click=${this._unhealthyDialog}
>
Learn more
@@ -228,16 +215,26 @@ class HassioSupervisorInfo extends LitElement {
<div class="card-actions">
<ha-progress-button
@click=${this._supervisorReload}
title="Reload parts of the Supervisor"
.title=${this.supervisor.localize(
"system.supervisor.reload_supervisor"
)}
>
Reload Supervisor
${this.supervisor.localize("system.supervisor.reload_supervisor")}
</ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._supervisorRestart}
title="Restart the Supervisor"
.title=${this.supervisor.localize(
"common.restart_name",
"name",
"Supervisor"
)}
>
Restart Supervisor
${this.supervisor.localize(
"common.restart_name",
"name",
"Supervisor"
)}
</ha-progress-button>
</div>
</ha-card>
@@ -258,23 +255,23 @@ class HassioSupervisorInfo extends LitElement {
if (this.supervisor.supervisor.channel === "stable") {
const confirmed = await showConfirmationDialog(this, {
title: "WARNING",
text: html` Beta releases are for testers and early adopters and can
contain unstable code changes.
title: this.supervisor.localize("system.supervisor.warning"),
text: html`${this.supervisor.localize("system.supervisor.beta_warning")}
<br />
<b>
Make sure you have backups of your data before you activate this
feature.
${this.supervisor.localize("system.supervisor.beta_backup")}
</b>
<br /><br />
This includes beta releases for:
${this.supervisor.localize("system.supervisor.beta_release_items")}
<li>Home Assistant Core</li>
<li>Home Assistant Supervisor</li>
<li>Home Assistant Operating System</li>
<br />
Do you want to join the beta channel?`,
confirmText: "join beta",
dismissText: "no",
${this.supervisor.localize("system.supervisor.join_beta_action")}`,
confirmText: this.supervisor.localize(
"system.supervisor.beta_join_confirm"
),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -292,7 +289,9 @@ class HassioSupervisorInfo extends LitElement {
await this._reloadSupervisor();
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
title: this.supervisor.localize(
"system.supervisor.failed_to_set_option"
),
text: extractApiErrorMessage(err),
});
} finally {
@@ -308,7 +307,7 @@ class HassioSupervisorInfo extends LitElement {
await this._reloadSupervisor();
} catch (err) {
showAlertDialog(this, {
title: "Failed to reload the supervisor",
title: this.supervisor.localize("system.supervisor.failed_to_reload"),
text: extractApiErrorMessage(err),
});
} finally {
@@ -318,8 +317,9 @@ class HassioSupervisorInfo extends LitElement {
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
const supervisor = await fetchHassioSupervisorInfo(this.hass);
fireEvent(this, "supervisor-update", { supervisor });
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
}
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
@@ -327,10 +327,18 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Restart the Supervisor",
text: "Are you sure you want to restart the Supervisor",
confirmText: "restart",
dismissText: "cancel",
title: this.supervisor.localize(
"confirm.restart.title",
"name",
"Supervisor"
),
text: this.supervisor.localize(
"confirm.restart.text",
"name",
"Supervisor"
),
confirmText: this.supervisor.localize("common.restart"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -342,7 +350,11 @@ class HassioSupervisorInfo extends LitElement {
await restartSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to restart the supervisor",
title: this.supervisor.localize(
"common.failed_to_restart_name",
"name",
"Supervisor"
),
text: extractApiErrorMessage(err),
});
} finally {
@@ -355,10 +367,16 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: "Update Supervisor",
text: `Are you sure you want to update supervisor to version ${this.supervisor.supervisor.version_latest}?`,
confirmText: "update",
dismissText: "cancel",
title: this.supervisor.localize("confirm.update", "name", "Supervisor"),
text: this.supervisor.localize(
"confirm.text",
"name",
"Supervisor",
"version",
this.supervisor.supervisor.version_latest
),
confirmText: this.supervisor.localize("common.update"),
dismissText: this.supervisor.localize("common.cancel"),
});
if (!confirmed) {
@@ -368,9 +386,16 @@ class HassioSupervisorInfo extends LitElement {
try {
await updateSupervisor(this.hass);
fireEvent(this, "supervisor-colllection-refresh", {
colllection: "supervisor",
});
} catch (err) {
showAlertDialog(this, {
title: "Failed to update the supervisor",
title: this.supervisor.localize(
"common.failed_to_update_name",
"name",
"Supervisor"
),
text: extractApiErrorMessage(err),
});
} finally {
@@ -380,40 +405,41 @@ class HassioSupervisorInfo extends LitElement {
private async _diagnosticsInformationDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Help Improve Home Assistant",
text: html`Would you want to automatically share crash reports and
diagnostic information when the supervisor encounters unexpected errors?
<br /><br />
This will allow us to fix the problems, the information is only
accessible to the Home Assistant Core team and will not be shared with
others.
<br /><br />
The data does not include any private/sensitive information and you can
disable this in settings at any time you want.`,
title: this.supervisor.localize(
"system.supervisor.share_diagonstics_title"
),
text: this.supervisor.localize(
"system.supervisor.share_diagonstics_description",
"line_break",
html`<br /><br />`
),
});
}
private async _unsupportedDialog(): Promise<void> {
await showAlertDialog(this, {
title: "You are running an unsupported installation",
text: html`Below is a list of issues found with your installation, click
on the links to learn how you can resolve the issues. <br /><br />
title: this.supervisor.localize("system.supervisor.unsupported_title"),
text: html`${this.supervisor.localize(
"system.supervisor.unsupported_description"
)} <br /><br />
<ul>
${this.supervisor.resolution.unsupported.map(
(issue) => html`
(reason) => html`
<li>
${UNSUPPORTED_REASON[issue]
${UNSUPPORTED_REASON_URL[reason]
? html`<a
href="${documentationUrl(
this.hass,
UNSUPPORTED_REASON[issue].url
UNSUPPORTED_REASON_URL[reason]
)}"
target="_blank"
rel="noreferrer"
>
${UNSUPPORTED_REASON[issue].title}
${this.supervisor.localize(
`system.supervisor.unsupported_reason.${reason}`
) || reason}
</a>`
: issue}
: reason}
</li>
`
)}
@@ -423,26 +449,28 @@ class HassioSupervisorInfo extends LitElement {
private async _unhealthyDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Your installation is unhealthy",
text: html`Running an unhealthy installation will cause issues. Below is a
list of issues found with your installation, click on the links to learn
how you can resolve the issues. <br /><br />
title: this.supervisor.localize("system.supervisor.unhealthy_title"),
text: html`${this.supervisor.localize(
"system.supervisor.unhealthy_description"
)} <br /><br />
<ul>
${this.supervisor.resolution.unhealthy.map(
(issue) => html`
(reason) => html`
<li>
${UNHEALTHY_REASON[issue]
${UNHEALTHY_REASON_URL[reason]
? html`<a
href="${documentationUrl(
this.hass,
UNHEALTHY_REASON[issue].url
UNHEALTHY_REASON_URL[reason]
)}"
target="_blank"
rel="noreferrer"
>
${UNHEALTHY_REASON[issue].title}
${this.supervisor.localize(
`system.supervisor.unhealthy_reason.${reason}`
) || reason}
</a>`
: issue}
: reason}
</li>
`
)}
@@ -458,7 +486,9 @@ class HassioSupervisorInfo extends LitElement {
await setSupervisorOption(this.hass, data);
} catch (err) {
showAlertDialog(this, {
title: "Failed to set supervisor option",
title: this.supervisor.localize(
"system.supervisor.failed_to_set_option"
),
text: extractApiErrorMessage(err),
});
}

View File

@@ -16,6 +16,7 @@ import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-card";
import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { fetchHassioLogs } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-loading-screen";
import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types";
@@ -58,6 +59,8 @@ const logProviders: LogProvider[] = [
class HassioSupervisorLog extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _error?: string;
@internalProperty() private _selectedLogProvider = "supervisor";
@@ -76,7 +79,7 @@ class HassioSupervisorLog extends LitElement {
${this.hass.userData?.showAdvanced
? html`
<paper-dropdown-menu
label="Log Provider"
.label=${this.supervisor.localize("system.log.log_provider")}
@iron-select=${this._setLogProvider}
>
<paper-listbox
@@ -86,9 +89,9 @@ class HassioSupervisorLog extends LitElement {
>
${logProviders.map((provider) => {
return html`
<paper-item provider=${provider.key}
>${provider.name}</paper-item
>
<paper-item provider=${provider.key}>
${provider.name}
</paper-item>
`;
})}
</paper-listbox>
@@ -98,14 +101,13 @@ class HassioSupervisorLog extends LitElement {
<div class="card-content" id="content">
${this._content
? html`<hassio-ansi-to-html
.content=${this._content}
></hassio-ansi-to-html>`
? html`<hassio-ansi-to-html .content=${this._content}>
</hassio-ansi-to-html>`
: html`<hass-loading-screen no-toolbar></hass-loading-screen>`}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._refresh}>
Refresh
${this.supervisor.localize("common.refresh")}
</ha-progress-button>
</div>
</ha-card>
@@ -134,9 +136,13 @@ class HassioSupervisorLog extends LitElement {
this._selectedLogProvider
);
} catch (err) {
this._error = `Failed to get supervisor logs, ${extractApiErrorMessage(
err
)}`;
this._error = this.supervisor.localize(
"system.log.get_logs",
"provider",
this._selectedLogProvider,
"error",
extractApiErrorMessage(err)
);
}
}

View File

@@ -32,13 +32,16 @@ class HassioSystem extends LitElement {
return html`
<hass-tabs-subpage
.hass=${this.hass}
.localizeFunc=${this.supervisor.localize}
.narrow=${this.narrow}
hassio
main-page
.route=${this.route}
.tabs=${supervisorTabs}
main-page
supervisor
>
<span slot="header">System</span>
<span slot="header">
${this.supervisor.localize("panel.system")}
</span>
<div class="content">
<div class="card-group">
<hassio-core-info
@@ -54,7 +57,10 @@ class HassioSystem extends LitElement {
.supervisor=${this.supervisor}
></hassio-host-info>
</div>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>
<hassio-supervisor-log
.hass=${this.hass}
.supervisor=${this.supervisor}
></hassio-supervisor-log>
</div>
</hass-tabs-subpage>
`;

7
hassio/src/util/addon.ts Normal file
View File

@@ -0,0 +1,7 @@
import memoizeOne from "memoize-one";
import { SupervisorArch } from "../../../src/data/supervisor/supervisor";
export const addonArchIsSupported = memoizeOne(
(supported_archs: SupervisorArch[], addon_archs: SupervisorArch[]) =>
addon_archs.some((arch) => supported_archs.includes(arch))
);

View File

@@ -22,6 +22,17 @@
"author": "Paulus Schoutsen <Paulus@PaulusSchoutsen.nl> (http://paulusschoutsen.nl)",
"license": "Apache-2.0",
"dependencies": {
"@braintree/sanitize-url": "^5.0.0",
"@codemirror/commands": "^0.17.0",
"@codemirror/gutter": "^0.17.0",
"@codemirror/highlight": "^0.17.0",
"@codemirror/history": "^0.17.0",
"@codemirror/legacy-modes": "^0.17.0",
"@codemirror/search": "^0.17.0",
"@codemirror/state": "^0.17.0",
"@codemirror/stream-parser": "^0.17.0",
"@codemirror/text": "^0.17.0",
"@codemirror/view": "^0.17.0",
"@formatjs/intl-getcanonicallocales": "^1.4.6",
"@formatjs/intl-pluralrules": "^3.4.10",
"@fullcalendar/common": "5.1.0",
@@ -45,8 +56,8 @@
"@material/mwc-tab": "^0.20.0",
"@material/mwc-tab-bar": "^0.20.0",
"@material/top-app-bar": "=9.0.0-canary.1c156d69d.0",
"@mdi/js": "5.6.55",
"@mdi/svg": "5.6.55",
"@mdi/js": "5.9.55",
"@mdi/svg": "5.9.55",
"@polymer/app-layout": "^3.0.2",
"@polymer/app-route": "^3.0.2",
"@polymer/app-storage": "^3.0.2",
@@ -100,7 +111,7 @@
"fuse.js": "^6.0.0",
"google-timezones-json": "^1.0.2",
"hls.js": "^0.13.2",
"home-assistant-js-websocket": "^5.4.1",
"home-assistant-js-websocket": "^5.9.0",
"idb-keyval": "^3.2.0",
"intl-messageformat": "^8.3.9",
"js-yaml": "^3.13.1",
@@ -109,7 +120,7 @@
"lit-element": "^2.4.0",
"lit-html": "^1.3.0",
"lit-virtualizer": "^0.4.2",
"marked": "^1.1.1",
"marked": "2.0.0",
"mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2",
"node-vibrant": "3.2.1-alpha.1",
@@ -160,7 +171,7 @@
"@types/js-yaml": "^3.12.1",
"@types/leaflet": "^1.4.3",
"@types/leaflet-draw": "^1.0.1",
"@types/marked": "^1.1.0",
"@types/marked": "^1.2.2",
"@types/memoize-one": "4.1.0",
"@types/mocha": "^7.0.2",
"@types/resize-observer-browser": "^0.1.3",
@@ -176,7 +187,7 @@
"eslint": "^6.8.0",
"eslint-config-airbnb-typescript": "^7.2.1",
"eslint-config-prettier": "^6.10.1",
"eslint-import-resolver-webpack": "^0.12.2",
"eslint-import-resolver-webpack": "^0.13.0",
"eslint-plugin-disable": "^2.0.1",
"eslint-plugin-import": "^2.20.2",
"eslint-plugin-lit": "^1.2.0",
@@ -212,16 +223,16 @@
"sinon": "^7.3.1",
"source-map-url": "^0.4.0",
"systemjs": "^6.3.2",
"terser-webpack-plugin": "^5.0.0",
"terser-webpack-plugin": "^5.1.1",
"ts-lit-plugin": "^1.2.1",
"ts-mocha": "^7.0.0",
"typescript": "^4.0.3",
"vinyl-buffer": "^1.0.1",
"vinyl-source-stream": "^2.0.0",
"webpack": "5.1.3",
"webpack-cli": "4.1.0",
"webpack-dev-server": "^3.11.0",
"webpack-manifest-plugin": "3.0.0-rc.0",
"webpack": "^5.24.1",
"webpack-cli": "^4.5.0",
"webpack-dev-server": "^3.11.2",
"webpack-manifest-plugin": "^3.0.0",
"workbox-build": "^5.1.3"
},
"_comment": "Polymer fixed to 3.1 because 3.2 throws on logbook page",

View File

@@ -12,5 +12,5 @@ yarn install
script/build_frontend
rm -rf dist
python3 setup.py sdist
python3 setup.py -q sdist
python3 -m twine upload dist/* --skip-existing

View File

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

View File

@@ -1,11 +1,20 @@
export const atLeastVersion = (
version: string,
major: number,
minor: number
minor: number,
patch?: number
): boolean => {
const [haMajor, haMinor] = version.split(".", 2);
const [haMajor, haMinor, haPatch] = version.split(".", 3);
return (
Number(haMajor) > major ||
(Number(haMajor) === major && Number(haMinor) >= minor)
(Number(haMajor) === major &&
(patch === undefined
? Number(haMinor) >= minor
: Number(haMinor) > minor)) ||
(patch !== undefined &&
Number(haMajor) === major &&
Number(haMinor) === minor &&
Number(haPatch) >= patch)
);
};

View File

@@ -8,12 +8,19 @@ export const batteryIcon = (
const battery = Number(batteryState.state);
const battery_charging =
batteryChargingState && batteryChargingState.state === "on";
let icon = "hass:battery";
if (isNaN(battery)) {
return "hass:battery-unknown";
if (batteryState.state === "off") {
icon += "-full";
} else if (batteryState.state === "on") {
icon += "-alert";
} else {
icon += "-unknown";
}
return icon;
}
let icon = "hass:battery";
const batteryRound = Math.round(battery / 10) * 10;
if (battery_charging && battery > 10) {
icon += `-charging-${batteryRound}`;

View File

@@ -15,7 +15,7 @@ export const iconColorCSS = css`
ha-icon[data-domain="media_player"][data-state="on"],
ha-icon[data-domain="media_player"][data-state="paused"],
ha-icon[data-domain="media_player"][data-state="playing"],
ha-icon[data-domain="script"][data-state="running"],
ha-icon[data-domain="script"][data-state="on"],
ha-icon[data-domain="sun"][data-state="above_horizon"],
ha-icon[data-domain="switch"][data-state="on"],
ha-icon[data-domain="timer"][data-state="active"],

View File

@@ -6,3 +6,16 @@ export const extractSearchParamsObject = (): Record<string, string> => {
}
return query;
};
export const extractSearchParam = (param: string): string | null => {
const urlParams = new URLSearchParams(window.location.search);
return urlParams.get(param);
};
export const createSearchParam = (params: Record<string, string>): string => {
const urlParams = new URLSearchParams();
Object.entries(params).forEach(([key, value]) => {
urlParams.append(key, value);
});
return urlParams.toString();
};

View File

@@ -1,16 +1,12 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
@@ -38,7 +34,8 @@ import {
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types";
import "../ha-svg-icon";
import type { HaComboBox } from "../ha-combo-box";
import "../ha-combo-box";
interface Device {
name: string;
@@ -112,10 +109,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean })
private _opened?: boolean;
@property({ type: Boolean }) public disabled?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
@internalProperty() private _opened?: boolean;
@query("ha-combo-box", true) private _comboBox!: HaComboBox;
private _init = false;
@@ -244,15 +242,11 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
);
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
});
this._comboBox?.open();
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
this._comboBox?.focus();
}
public hassSubscribe(): UnsubscribeFunc[] {
@@ -292,70 +286,29 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
return html``;
}
return html`
<vaadin-combo-box-light
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
item-value-path="id"
item-id-path="id"
item-label-path="name"
.value=${this._value}
.renderer=${rowRenderer}
@opened-changed=${this._openedChanged}
@value-changed=${this._deviceChanged}
>
<paper-input
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.device-picker.device")
: this.label}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.device-picker.clear"
)}
slot="suffix"
class="clear-button"
@click=${this._clearValue}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
<mwc-icon-button
.label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
slot="suffix"
class="toggle-button"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</paper-input>
</vaadin-combo-box-light>
></ha-combo-box>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
this._setValue("");
}
private get _value() {
return this.value || "";
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _deviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
@@ -363,6 +316,10 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
}
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {

View File

@@ -115,7 +115,7 @@ export class StateBadge extends LitElement {
// eslint-disable-next-line
console.warn(errorMessage);
}
// lowest brighntess will be around 50% (that's pretty dark)
// lowest brightness will be around 50% (that's pretty dark)
iconStyle.filter = `brightness(${(brightness + 245) / 5}%)`;
}
}

View File

@@ -0,0 +1,148 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { isComponentLoaded } from "../common/config/is_component_loaded";
import { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare";
import { HassioAddonInfo } from "../data/hassio/addon";
import { fetchHassioSupervisorInfo } from "../data/hassio/supervisor";
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import { HaComboBox } from "./ha-combo-box";
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: HassioAddonInfo }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.slug]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent = model.item.slug;
};
@customElement("ha-addon-picker")
class HaAddonPicker extends LitElement {
public hass!: HomeAssistant;
@property() public label?: string;
@property() public value = "";
@internalProperty() private _addons?: HassioAddonInfo[];
@property({ type: Boolean }) public disabled = false;
@query("ha-combo-box") private _comboBox!: HaComboBox;
public open() {
this._comboBox?.open();
}
public focus() {
this._comboBox?.focus();
}
protected firstUpdated() {
this._getAddons();
}
protected render(): TemplateResult {
if (!this._addons) {
return html``;
}
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.addon-picker.addon")
: this.label}
.value=${this._value}
.renderer=${rowRenderer}
.items=${this._addons}
item-value-path="slug"
item-id-path="slug"
item-label-path="name"
@value-changed=${this._addonChanged}
></ha-combo-box>
`;
}
private async _getAddons() {
try {
if (isComponentLoaded(this.hass, "hassio")) {
const supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
this._addons = supervisorInfo.addons.sort((a, b) =>
compare(a.name, b.name)
);
} else {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.no_supervisor.description"
),
});
}
} catch (error) {
showAlertDialog(this, {
title: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.title"
),
text: this.hass.localize(
"ui.componencts.addon-picker.error.fetch_addons.description"
),
});
}
}
private get _value() {
return this.value || "";
}
private _addonChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this._value) {
this._setValue(newValue);
}
}
private _setValue(value: string) {
this.value = value;
setTimeout(() => {
fireEvent(this, "value-changed", { value });
fireEvent(this, "change");
}, 0);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-addon-picker": HaAddonPicker;
}
}

View File

@@ -117,6 +117,8 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@property({ type: Boolean }) public disabled?: boolean;
@internalProperty() private _areas?: AreaRegistryEntry[];
@internalProperty() private _devices?: DeviceRegistryEntry[];
@@ -138,7 +140,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
this._devices = devices;
}),
subscribeEntityRegistry(this.hass.connection!, (entities) => {
this._entities = entities;
this._entities = entities.filter((entity) => entity.area_id);
}),
];
}
@@ -191,11 +193,14 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
deviceEntityLookup[entity.device_id].push(entity);
}
inputDevices = devices;
inputEntities = entities.filter((entity) => entity.area_id);
} else if (deviceFilter) {
inputDevices = devices;
} else if (entityFilter) {
inputEntities = entities.filter((entity) => entity.area_id);
inputEntities = entities;
} else {
if (deviceFilter) {
inputDevices = devices;
}
if (entityFilter) {
inputEntities = entities;
}
}
if (includeDomains) {
@@ -339,6 +344,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-label-path="name"
.value=${this._value}
.renderer=${rowRenderer}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged}
@value-changed=${this._areaChanged}
>
@@ -349,6 +355,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"

View File

@@ -1,4 +1,5 @@
import { Editor } from "codemirror";
import type { StreamLanguage } from "@codemirror/stream-parser";
import type { EditorView, KeyBinding, ViewUpdate } from "@codemirror/view";
import {
customElement,
internalProperty,
@@ -15,32 +16,40 @@ declare global {
}
}
const modeTag = Symbol("mode");
const readOnlyTag = Symbol("readOnly");
const saveKeyBinding: KeyBinding = {
key: "Mod-s",
run: (view: EditorView) => {
fireEvent(view.dom, "editor-save");
return true;
},
};
@customElement("ha-code-editor")
export class HaCodeEditor extends UpdatingElement {
public codemirror?: Editor;
public codemirror?: EditorView;
@property() public mode?: string;
@property() public mode = "yaml";
@property({ type: Boolean }) public autofocus = false;
@property({ type: Boolean }) public readOnly = false;
@property() public rtl = false;
@property() public error = false;
@internalProperty() private _value = "";
@internalProperty() private _langs?: Record<string, StreamLanguage<unknown>>;
public set value(value: string) {
this._value = value;
}
public get value(): string {
return this.codemirror ? this.codemirror.getValue() : this._value;
}
public get hasComments(): boolean {
return !!this.shadowRoot!.querySelector("span.cm-comment");
return this.codemirror ? this.codemirror.state.doc.toString() : this._value;
}
public connectedCallback() {
@@ -48,7 +57,6 @@ export class HaCodeEditor extends UpdatingElement {
if (!this.codemirror) {
return;
}
this.codemirror.refresh();
if (this.autofocus !== false) {
this.codemirror.focus();
}
@@ -62,17 +70,27 @@ export class HaCodeEditor extends UpdatingElement {
}
if (changedProps.has("mode")) {
this.codemirror.setOption("mode", this.mode);
this.codemirror.dispatch({
reconfigure: {
[modeTag]: this._mode,
},
});
}
if (changedProps.has("autofocus")) {
this.codemirror.setOption("autofocus", this.autofocus !== false);
if (changedProps.has("readOnly")) {
this.codemirror.dispatch({
reconfigure: {
[readOnlyTag]: !this.readOnly,
},
});
}
if (changedProps.has("_value") && this._value !== this.value) {
this.codemirror.setValue(this._value);
}
if (changedProps.has("rtl")) {
this.codemirror.setOption("gutters", this._calcGutters());
this._setScrollBarDirection();
this.codemirror.dispatch({
changes: {
from: 0,
to: this.codemirror.state.doc.length,
insert: this._value,
},
});
}
if (changedProps.has("error")) {
this.classList.toggle("error-state", this.error);
@@ -85,159 +103,66 @@ export class HaCodeEditor extends UpdatingElement {
this._load();
}
private get _mode() {
return this._langs![this.mode];
}
private async _load(): Promise<void> {
const loaded = await loadCodeMirror();
const codeMirror = loaded.codeMirror;
this._langs = loaded.langs;
const shadowRoot = this.attachShadow({ mode: "open" });
shadowRoot!.innerHTML = `
<style>
${loaded.codeMirrorCss}
.CodeMirror {
height: var(--code-mirror-height, auto);
direction: var(--code-mirror-direction, ltr);
font-family: var(--code-font-family, monospace);
}
.CodeMirror-scroll {
max-height: var(--code-mirror-max-height, --code-mirror-height);
}
:host(.error-state) .CodeMirror-gutters {
shadowRoot!.innerHTML = `<style>
:host(.error-state) div.cm-wrap .cm-gutters {
border-color: var(--error-state-color, red);
}
.CodeMirror-focused .CodeMirror-gutters {
border-right: 2px solid var(--paper-input-container-focus-color, var(--primary-color));
}
.CodeMirror-linenumber {
color: var(--paper-dialog-color, var(--secondary-text-color));
}
.rtl .CodeMirror-vscrollbar {
right: auto;
left: 0px;
}
.rtl-gutter {
width: 20px;
}
.CodeMirror-gutters {
border-right: 1px solid var(--paper-input-container-color, var(--secondary-text-color));
background-color: var(--paper-dialog-background-color, var(--primary-background-color));
transition: 0.2s ease border-right;
}
.cm-s-default.CodeMirror {
background-color: var(--code-editor-background-color, var(--card-background-color));
color: var(--primary-text-color);
}
.cm-s-default .CodeMirror-cursor {
border-left: 1px solid var(--secondary-text-color);
}
.cm-s-default div.CodeMirror-selected, .cm-s-default.CodeMirror-focused div.CodeMirror-selected {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .CodeMirror-line::selection,
.cm-s-default .CodeMirror-line>span::selection,
.cm-s-default .CodeMirror-line>span>span::selection {
background: rgba(var(--rgb-primary-color), 0.2);
}
.cm-s-default .cm-keyword {
color: var(--codemirror-keyword, #6262FF);
}
.cm-s-default .cm-operator {
color: var(--codemirror-operator, #cda869);
}
.cm-s-default .cm-variable-2 {
color: var(--codemirror-variable-2, #690);
}
.cm-s-default .cm-builtin {
color: var(--codemirror-builtin, #9B7536);
}
.cm-s-default .cm-atom {
color: var(--codemirror-atom, #F90);
}
.cm-s-default .cm-number {
color: var(--codemirror-number, #ca7841);
}
.cm-s-default .cm-def {
color: var(--codemirror-def, #8DA6CE);
}
.cm-s-default .cm-string {
color: var(--codemirror-string, #07a);
}
.cm-s-default .cm-string-2 {
color: var(--codemirror-string-2, #bd6b18);
}
.cm-s-default .cm-comment {
color: var(--codemirror-comment, #777);
}
.cm-s-default .cm-variable {
color: var(--codemirror-variable, #07a);
}
.cm-s-default .cm-tag {
color: var(--codemirror-tag, #997643);
}
.cm-s-default .cm-meta {
color: var(--codemirror-meta, var(--primary-text-color));
}
.cm-s-default .cm-attribute {
color: var(--codemirror-attribute, #d6bb6d);
}
.cm-s-default .cm-property {
color: var(--codemirror-property, #905);
}
.cm-s-default .cm-qualifier {
color: var(--codemirror-qualifier, #690);
}
.cm-s-default .cm-variable-3 {
color: var(--codemirror-variable-3, #07a);
}
.cm-s-default .cm-type {
color: var(--codemirror-type, #07a);
}
</style>`;
this.codemirror = codeMirror(shadowRoot, {
value: this._value,
lineNumbers: true,
tabSize: 2,
mode: this.mode,
autofocus: this.autofocus !== false,
viewportMargin: Infinity,
readOnly: this.readOnly,
extraKeys: {
Tab: "indentMore",
"Shift-Tab": "indentLess",
},
gutters: this._calcGutters(),
const container = document.createElement("span");
shadowRoot.appendChild(container);
this.codemirror = new loaded.EditorView({
state: loaded.EditorState.create({
doc: this._value,
extensions: [
loaded.lineNumbers(),
loaded.history(),
loaded.highlightSelectionMatches(),
loaded.keymap.of([
...loaded.defaultKeymap,
...loaded.searchKeymap,
...loaded.historyKeymap,
...loaded.tabKeyBindings,
saveKeyBinding,
] as KeyBinding[]),
loaded.tagExtension(modeTag, this._mode),
loaded.theme,
loaded.Prec.fallback(loaded.highlightStyle),
loaded.tagExtension(
readOnlyTag,
loaded.EditorView.editable.of(!this.readOnly)
),
loaded.EditorView.updateListener.of((update) =>
this._onUpdate(update)
),
],
}),
root: shadowRoot,
parent: container,
});
this._setScrollBarDirection();
this.codemirror!.on("changes", () => this._onChange());
}
private _blockKeyboardShortcuts() {
this.addEventListener("keydown", (ev) => ev.stopPropagation());
}
private _onChange(): void {
private _onUpdate(update: ViewUpdate): void {
if (!update.docChanged) {
return;
}
const newValue = this.value;
if (newValue === this._value) {
return;
@@ -245,16 +170,6 @@ export class HaCodeEditor extends UpdatingElement {
this._value = newValue;
fireEvent(this, "value-changed", { value: this._value });
}
private _calcGutters(): string[] {
return this.rtl ? ["rtl-gutter", "CodeMirror-linenumbers"] : [];
}
private _setScrollBarDirection(): void {
if (this.codemirror) {
this.codemirror.getWrapperElement().classList.toggle("rtl", this.rtl);
}
}
}
declare global {

View File

@@ -1,116 +0,0 @@
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import { EventsMixin } from "../mixins/events-mixin";
import "./ha-icon-button";
class HaComboBox extends EventsMixin(PolymerElement) {
static get template() {
return html`
<style>
paper-input > ha-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
[hidden] {
display: none;
}
</style>
<vaadin-combo-box-light
items="[[_items]]"
item-value-path="[[itemValuePath]]"
item-label-path="[[itemLabelPath]]"
value="{{value}}"
opened="{{opened}}"
allow-custom-value="[[allowCustomValue]]"
on-change="_fireChanged"
>
<paper-input
autofocus="[[autofocus]]"
label="[[label]]"
class="input"
value="[[value]]"
>
<ha-icon-button
slot="suffix"
class="clear-button"
icon="hass:close"
hidden$="[[!value]]"
>Clear</ha-icon-button
>
<ha-icon-button
slot="suffix"
class="toggle-button"
icon="[[_computeToggleIcon(opened)]]"
hidden$="[[!items.length]]"
>Toggle</ha-icon-button
>
</paper-input>
<template>
<style>
paper-item {
margin: -5px -10px;
padding: 0;
}
</style>
<paper-item>[[_computeItemLabel(item, itemLabelPath)]]</paper-item>
</template>
</vaadin-combo-box-light>
`;
}
static get properties() {
return {
allowCustomValue: Boolean,
items: {
type: Object,
observer: "_itemsChanged",
},
_items: Object,
itemLabelPath: String,
itemValuePath: String,
autofocus: Boolean,
label: String,
opened: {
type: Boolean,
value: false,
observer: "_openedChanged",
},
value: {
type: String,
notify: true,
},
};
}
_openedChanged(newVal) {
if (!newVal) {
this._items = this.items;
}
}
_itemsChanged(newVal) {
if (!this.opened) {
this._items = newVal;
}
}
_computeToggleIcon(opened) {
return opened ? "hass:menu-up" : "hass:menu-down";
}
_computeItemLabel(item, itemLabelPath) {
return itemLabelPath ? item[itemLabelPath] : item;
}
_fireChanged(ev) {
ev.stopPropagation();
this.fire("change");
}
}
customElements.define("ha-combo-box", HaComboBox);

View File

@@ -0,0 +1,181 @@
import "@material/mwc-icon-button/mwc-icon-button";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body";
import "@polymer/paper-listbox/paper-listbox";
import "@vaadin/vaadin-combo-box/theme/material/vaadin-combo-box-light";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import "./ha-svg-icon";
const defaultRowRenderer = (
root: HTMLElement,
_owner,
model: { item: any }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -5px -10px;
padding: 0;
}
</style>
<paper-item></paper-item>
`;
}
root.querySelector("paper-item")!.textContent = model.item;
};
@customElement("ha-combo-box")
export class HaComboBox extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public label?: string;
@property() public value?: string;
@property() public items?: [];
@property() public filteredItems?: [];
@property({ attribute: "allow-custom-value", type: Boolean })
public allowCustomValue?: boolean;
@property({ attribute: "item-value-path" }) public itemValuePath?: string;
@property({ attribute: "item-label-path" }) public itemLabelPath?: string;
@property({ attribute: "item-id-path" }) public itemIdPath?: string;
@property() public renderer?: (
root: HTMLElement,
owner: HTMLElement,
model: { item: any }
) => void;
@property({ type: Boolean }) public disabled?: boolean;
@internalProperty() private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
public open() {
this.updateComplete.then(() => {
(this._comboBox as any)?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
}
protected render(): TemplateResult {
return html`
<vaadin-combo-box-light
.itemValuePath=${this.itemValuePath}
.itemIdPath=${this.itemIdPath}
.itemLabelPath=${this.itemLabelPath}
.value=${this.value}
.items=${this.items}
.filteredItems=${this.filteredItems}
.renderer=${this.renderer || defaultRowRenderer}
.allowCustomValue=${this.allowCustomValue}
.disabled=${this.disabled}
@opened-changed=${this._openedChanged}
@filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged}
>
<paper-input
.label=${this.label}
.disabled=${this.disabled}
class="input"
autocapitalize="none"
autocomplete="off"
autocorrect="off"
spellcheck="false"
>
${this.value
? html`
<mwc-icon-button
.label=${this.hass.localize("ui.components.combo-box.clear")}
slot="suffix"
class="clear-button"
@click=${this._clearValue}
>
<ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button>
`
: ""}
<mwc-icon-button
.label=${this.hass.localize("ui.components.combo-box.show")}
slot="suffix"
class="toggle-button"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</paper-input>
</vaadin-combo-box-light>
`;
}
private _clearValue(ev: Event) {
ev.stopPropagation();
fireEvent(this, "value-changed", { value: undefined });
}
private _openedChanged(ev: PolymerChangedEvent<boolean>) {
this._opened = ev.detail.value;
// @ts-ignore
fireEvent(this, ev.type, ev.detail);
}
private _filterChanged(ev: PolymerChangedEvent<boolean>) {
// @ts-ignore
fireEvent(this, ev.type, ev.detail);
}
private _valueChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (newValue !== this.value) {
fireEvent(this, "value-changed", { value: newValue });
}
}
static get styles(): CSSResult {
return css`
paper-input > mwc-icon-button {
--mdc-icon-button-size: 24px;
padding: 2px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-combo-box": HaComboBox;
}
}

View File

@@ -1,6 +1,9 @@
import { mdiEye, mdiEyeOff } from "@mdi/js";
import "@polymer/paper-input/paper-input";
import type { PaperInputElement } from "@polymer/paper-input/paper-input";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
@@ -10,12 +13,13 @@ import {
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "../ha-icon-button";
import "../ha-svg-icon";
import type {
HaFormElement,
HaFormStringData,
HaFormStringSchema,
} from "./ha-form";
import "@material/mwc-icon-button/mwc-icon-button";
@customElement("ha-form-string")
export class HaFormString extends LitElement implements HaFormElement {
@@ -48,16 +52,17 @@ export class HaFormString extends LitElement implements HaFormElement {
.autoValidate=${this.schema.required}
@value-changed=${this._valueChanged}
>
<ha-icon-button
<mwc-icon-button
toggles
slot="suffix"
.icon=${this._unmaskedPassword ? "hass:eye-off" : "hass:eye"}
id="iconButton"
title="Click to toggle between masked and clear password"
@click=${this._toggleUnmaskedPassword}
tabindex="-1"
>
</ha-icon-button>
><ha-svg-icon
.path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
></ha-svg-icon>
</mwc-icon-button>
</paper-input>
`
: html`
@@ -98,6 +103,15 @@ export class HaFormString extends LitElement implements HaFormElement {
}
return "text";
}
static get styles(): CSSResult {
return css`
mwc-icon-button {
--mdc-icon-button-size: 24px;
color: var(--secondary-text-color);
}
`;
}
}
declare global {

View File

@@ -202,9 +202,8 @@ export class HaForm extends LitElement implements HaFormElement {
ev.stopPropagation();
const schema = (ev.target as HaFormElement).schema as HaFormSchema;
const data = this.data as HaFormDataContainer;
data[schema.name] = ev.detail.value;
fireEvent(this, "value-changed", {
value: { ...data },
value: { ...data, [schema.name]: ev.detail.value },
});
}

View File

@@ -21,8 +21,11 @@ export class HaActionSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean, reflect: true }) public disabled = false;
protected render() {
return html`<ha-automation-action
.disabled=${this.disabled}
.actions=${this.value || []}
.hass=${this.hass}
></ha-automation-action>`;
@@ -34,6 +37,10 @@ export class HaActionSelector extends LitElement {
display: block;
margin-bottom: 16px;
}
:host([disabled]) ha-automation-action {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
`;
}
}

View File

@@ -0,0 +1,30 @@
import { customElement, html, LitElement, property } from "lit-element";
import { AddonSelector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "../ha-addon-picker";
@customElement("ha-selector-addon")
export class HaAddonSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AddonSelector;
@property() public value?: any;
@property() public label?: string;
protected render() {
return html`<ha-addon-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
allow-custom-entity
></ha-addon-picker>`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-addon": HaAddonSelector;
}
}

View File

@@ -24,6 +24,8 @@ export class HaAreaSelector extends LitElement {
@internalProperty() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
@@ -50,6 +52,7 @@ export class HaAreaSelector extends LitElement {
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-area-picker>`;
}

View File

@@ -19,11 +19,14 @@ export class HaBooleanSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
.disabled=${this.disabled}
></ha-switch>
</ha-formfield>`;
}

View File

@@ -23,10 +23,12 @@ export class HaDeviceSelector extends LitElement {
@internalProperty() public _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device.integration) {
if (oldSelector !== this.selector && this.selector.device?.integration) {
this._loadConfigEntries();
}
}
@@ -44,24 +46,25 @@ export class HaDeviceSelector extends LitElement {
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
.disabled=${this.disabled}
allow-custom-entity
></ha-device-picker>`;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.device.manufacturer &&
this.selector.device?.manufacturer &&
device.manufacturer !== this.selector.device.manufacturer
) {
return false;
}
if (
this.selector.device.model &&
this.selector.device?.model &&
device.model !== this.selector.device.model
) {
return false;
}
if (this.selector.device.integration) {
if (this.selector.device?.integration) {
if (
this._configEntries &&
!this._configEntries.some((entry) =>

View File

@@ -25,12 +25,15 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${(entity) => this._filterEntities(entity)}
.disabled=${this.disabled}
allow-custom-entity
></ha-entity-picker>`;
}
@@ -51,12 +54,12 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
}
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.entity.domain) {
if (this.selector.entity?.domain) {
if (computeStateDomain(entity) !== this.selector.entity.domain) {
return false;
}
}
if (this.selector.entity.device_class) {
if (this.selector.entity?.device_class) {
if (
!entity.attributes.device_class ||
entity.attributes.device_class !== this.selector.entity.device_class
@@ -64,7 +67,7 @@ export class HaEntitySelector extends SubscribeMixin(LitElement) {
return false;
}
}
if (this.selector.entity.integration) {
if (this.selector.entity?.integration) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==

View File

@@ -21,8 +21,12 @@ export class HaNumberSelector extends LitElement {
@property() public value?: number;
@property() public placeholder?: number;
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`${this.label}
${this.selector.number.mode === "slider"
@@ -31,6 +35,7 @@ export class HaNumberSelector extends LitElement {
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step}
.disabled=${this.disabled}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
@@ -42,12 +47,14 @@ export class HaNumberSelector extends LitElement {
.label=${this.selector.number.mode === "slider"
? undefined
: this.label}
.placeholder=${this.placeholder}
.noLabelFloat=${this.selector.number.mode === "slider"}
class=${classMap({ single: this.selector.number.mode === "box" })}
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.value=${this.value}
.step=${this.selector.number.step}
.disabled=${this.disabled}
type="number"
auto-validate
@value-changed=${this._handleInputChange}
@@ -65,16 +72,21 @@ export class HaNumberSelector extends LitElement {
}
private _handleInputChange(ev) {
const value = ev.detail.value;
if (this._value === value) {
ev.stopPropagation();
const value =
ev.detail.value === "" || isNaN(ev.detail.value)
? undefined
: Number(ev.detail.value);
if (this.value === value) {
return;
}
fireEvent(this, "value-changed", { value });
}
private _handleSliderChange(ev) {
const value = ev.target.value;
if (this._value === value) {
ev.stopPropagation();
const value = Number(ev.target.value);
if (this.value === value) {
return;
}
fireEvent(this, "value-changed", { value });

View File

@@ -11,8 +11,14 @@ export class HaObjectSelector extends LitElement {
@property() public label?: string;
@property() public placeholder?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-yaml-editor
.disabled=${this.disabled}
.placeholder=${this.placeholder}
.defaultValue=${this.value}
@value-changed=${this._handleChange}
></ha-yaml-editor>`;

View File

@@ -0,0 +1,78 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import { SelectSelector } from "../../data/selector";
import "../ha-paper-dropdown-menu";
@customElement("ha-selector-select")
export class HaSelectSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: SelectSelector;
@property() public value?: string;
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
return html`<ha-paper-dropdown-menu
.disabled=${this.disabled}
.label=${this.label}
>
<paper-listbox
slot="dropdown-content"
attr-for-selected="item-value"
.selected=${this.value}
@selected-item-changed=${this._valueChanged}
>
${this.selector.select.options.map(
(item: string) => html`
<paper-item .itemValue=${item}>
${item}
</paper-item>
`
)}
</paper-listbox>
</ha-paper-dropdown-menu>`;
}
private _valueChanged(ev) {
if (this.disabled || !ev.detail.value) {
return;
}
fireEvent(this, "value-changed", {
value: ev.detail.value.itemValue,
});
}
static get styles(): CSSResult {
return css`
ha-paper-dropdown-menu {
width: 100%;
min-width: 200px;
display: block;
}
paper-listbox {
min-width: 200px;
}
paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-select": HaSelectSelector;
}
}

View File

@@ -3,7 +3,11 @@ import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import "@polymer/paper-input/paper-input";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import {
HassEntity,
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import {
css,
CSSResult,
@@ -20,7 +24,6 @@ import {
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { TargetSelector } from "../../data/selector";
import { Target } from "../../data/target";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { HomeAssistant } from "../../types";
import "../ha-target-picker";
@@ -31,7 +34,7 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@property() public selector!: TargetSelector;
@property() public value?: Target;
@property() public value?: HassServiceTarget;
@property() public label?: string;
@@ -39,6 +42,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
@internalProperty() private _configEntries?: ConfigEntry[];
@property({ type: Boolean }) public disabled = false;
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
@@ -59,7 +64,8 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
const oldSelector = changedProperties.get("selector");
if (
oldSelector !== this.selector &&
this.selector.target.device?.integration
(this.selector.target.device?.integration ||
this.selector.target.entity?.integration)
) {
this._loadConfigEntries();
}
@@ -80,15 +86,20 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
.includeDomains=${this.selector.target.entity?.domain
? [this.selector.target.entity.domain]
: undefined}
.disabled=${this.disabled}
></ha-target-picker>`;
}
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.target.entity?.integration) {
if (
this.selector.target.entity?.integration ||
this.selector.target.device?.integration
) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==
this.selector.target.entity.integration
(this.selector.target.entity?.integration ||
this.selector.target.device?.integration)
) {
return false;
}
@@ -118,7 +129,10 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
) {
return false;
}
if (this.selector.target.device?.integration) {
if (
this.selector.target.device?.integration ||
this.selector.target.entity?.integration
) {
if (
!this._configEntries?.some((entry) =>
device.config_entries.includes(entry.entry_id)
@@ -132,14 +146,16 @@ export class HaTargetSelector extends SubscribeMixin(LitElement) {
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.target.device?.integration
(entry) =>
entry.domain ===
(this.selector.target.device?.integration ||
this.selector.target.entity?.integration)
);
}
static get styles(): CSSResult {
return css`
ha-target-picker {
margin: 0 -8px;
display: block;
}
`;

View File

@@ -13,14 +13,20 @@ export class HaTextSelector extends LitElement {
@property() public label?: string;
@property() public placeholder?: string;
@property() public selector!: StringSelector;
@property({ type: Boolean }) public disabled = false;
protected render() {
if (this.selector.text?.multiline) {
return html`<paper-textarea
.label=${this.label}
.value="${this.value}"
@value-changed="${this._handleChange}"
.placeholder=${this.placeholder}
.value=${this.value}
.disabled=${this.disabled}
@value-changed=${this._handleChange}
autocapitalize="none"
autocomplete="off"
spellcheck="false"
@@ -29,6 +35,8 @@ export class HaTextSelector extends LitElement {
return html`<paper-input
required
.value=${this.value}
.placeholder=${this.placeholder}
.disabled=${this.disabled}
@value-changed=${this._handleChange}
.label=${this.label}
></paper-input>`;

View File

@@ -17,6 +17,8 @@ export class HaTimeSelector extends LitElement {
@property() public label?: string;
@property({ type: Boolean }) public disabled = false;
protected render() {
const parts = this.value?.split(":") || [];
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
@@ -29,6 +31,7 @@ export class HaTimeSelector extends LitElement {
.sec=${parts[2] ?? "00"}
.format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
.disabled=${this.disabled}
@change=${this._timeChanged}
@am-pm-changed=${this._timeChanged}
hide-label

View File

@@ -3,6 +3,7 @@ import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { Selector } from "../../data/selector";
import { HomeAssistant } from "../../types";
import "./ha-selector-action";
import "./ha-selector-addon";
import "./ha-selector-area";
import "./ha-selector-boolean";
import "./ha-selector-device";
@@ -12,6 +13,7 @@ import "./ha-selector-target";
import "./ha-selector-time";
import "./ha-selector-object";
import "./ha-selector-text";
import "./ha-selector-select";
@customElement("ha-selector")
export class HaSelector extends LitElement {
@@ -23,6 +25,10 @@ export class HaSelector extends LitElement {
@property() public label?: string;
@property() public placeholder?: any;
@property({ type: Boolean }) public disabled = false;
public focus() {
const input = this.shadowRoot!.getElementById("selector");
if (!input) {
@@ -42,6 +48,8 @@ export class HaSelector extends LitElement {
selector: this.selector,
value: this.value,
label: this.label,
placeholder: this.placeholder,
disabled: this.disabled,
id: "selector",
})}
`;

View File

@@ -0,0 +1,407 @@
import { HassService, HassServiceTarget } from "home-assistant-js-websocket";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
PropertyValues,
query,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { computeDomain } from "../common/entity/compute_domain";
import { computeObjectId } from "../common/entity/compute_object_id";
import { ENTITY_COMPONENT_DOMAINS } from "../data/entity";
import { Selector } from "../data/selector";
import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types";
import "./ha-selector/ha-selector";
import "./ha-service-picker";
import "./ha-settings-row";
import "./ha-yaml-editor";
import "./ha-checkbox";
import type { HaYamlEditor } from "./ha-yaml-editor";
interface ExtHassService extends Omit<HassService, "fields"> {
fields: {
key: string;
name?: string;
description: string;
required?: boolean;
advanced?: boolean;
default?: any;
example?: any;
selector?: Selector;
}[];
}
@customElement("ha-service-control")
export class HaServiceControl extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public value?: {
service: string;
target?: HassServiceTarget;
data?: Record<string, any>;
};
@property({ reflect: true, type: Boolean }) public narrow!: boolean;
@property({ type: Boolean }) public showAdvanced?: boolean;
@internalProperty() private _serviceData?: ExtHassService;
@internalProperty() private _checkedKeys = new Set();
@query("ha-yaml-editor") private _yamlEditor?: HaYamlEditor;
protected updated(changedProperties: PropertyValues) {
if (!changedProperties.has("value")) {
return;
}
const oldValue = changedProperties.get("value") as
| undefined
| this["value"];
if (oldValue?.service !== this.value?.service) {
this._checkedKeys = new Set();
}
this._serviceData = this.value?.service
? this._getServiceInfo(this.value.service)
: undefined;
if (
this._serviceData &&
"target" in this._serviceData &&
(this.value?.data?.entity_id ||
this.value?.data?.area_id ||
this.value?.data?.device_id)
) {
const target = {
...this.value.target,
};
if (this.value.data.entity_id && !this.value.target?.entity_id) {
target.entity_id = this.value.data.entity_id;
}
if (this.value.data.area_id && !this.value.target?.area_id) {
target.area_id = this.value.data.area_id;
}
if (this.value.data.device_id && !this.value.target?.device_id) {
target.device_id = this.value.data.device_id;
}
this.value = {
...this.value,
target,
data: { ...this.value.data },
};
delete this.value.data!.entity_id;
delete this.value.data!.device_id;
delete this.value.data!.area_id;
}
if (this.value?.data) {
const yamlEditor = this._yamlEditor;
if (yamlEditor && yamlEditor.value !== this.value.data) {
yamlEditor.setValue(this.value.data);
}
}
}
private _domainFilter = memoizeOne((service: string) => {
const domain = computeDomain(service);
return ENTITY_COMPONENT_DOMAINS.includes(domain) ? [domain] : null;
});
private _getServiceInfo = memoizeOne((service: string):
| ExtHassService
| undefined => {
if (!service) {
return undefined;
}
const domain = computeDomain(service);
const serviceName = computeObjectId(service);
const serviceDomains = this.hass.services;
if (!(domain in serviceDomains)) {
return undefined;
}
if (!(serviceName in serviceDomains[domain])) {
return undefined;
}
const fields = Object.entries(
serviceDomains[domain][serviceName].fields
).map(([key, value]) => {
return {
key,
...value,
selector: value.selector as Selector | undefined,
};
});
return {
...serviceDomains[domain][serviceName],
fields,
};
});
protected render() {
const legacy =
this._serviceData?.fields.length &&
!this._serviceData.fields.some((field) => field.selector);
const entityId =
legacy &&
this._serviceData?.fields.find((field) => field.key === "entity_id");
const hasOptional = Boolean(
!legacy &&
this._serviceData?.fields.some(
(field) => field.selector && !field.required
)
);
return html`<ha-service-picker
.hass=${this.hass}
.value=${this.value?.service}
@value-changed=${this._serviceChanged}
></ha-service-picker>
<p>${this._serviceData?.description}</p>
${this._serviceData && "target" in this._serviceData
? html`<ha-settings-row .narrow=${this.narrow}>
${hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""}
<span slot="heading"
>${this.hass.localize(
"ui.components.service-control.target"
)}</span
>
<span slot="description"
>${this.hass.localize(
"ui.components.service-control.target_description"
)}</span
><ha-selector
.hass=${this.hass}
.selector=${this._serviceData.target
? { target: this._serviceData.target }
: {
target: {
entity: { domain: computeDomain(this.value!.service) },
},
}}
@value-changed=${this._targetChanged}
.value=${this.value?.target}
></ha-selector
></ha-settings-row>`
: entityId
? html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value?.data?.entity_id}
.label=${entityId.description}
.includeDomains=${this._domainFilter(this.value!.service)}
@value-changed=${this._entityPicked}
allow-custom-entity
></ha-entity-picker>`
: ""}
${legacy
? html`<ha-yaml-editor
.label=${this.hass.localize(
"ui.components.service-control.service_data"
)}
.name=${"data"}
.defaultValue=${this.value?.data}
@value-changed=${this._dataChanged}
></ha-yaml-editor>`
: this._serviceData?.fields.map((dataField) =>
dataField.selector && (!dataField.advanced || this.showAdvanced)
? html`<ha-settings-row .narrow=${this.narrow}>
${dataField.required
? hasOptional
? html`<div slot="prefix" class="checkbox-spacer"></div>`
: ""
: html`<ha-checkbox
.key=${dataField.key}
.checked=${this._checkedKeys.has(dataField.key) ||
(this.value?.data &&
this.value.data[dataField.key] !== undefined)}
@change=${this._checkboxChanged}
slot="prefix"
></ha-checkbox>`}
<span slot="heading">${dataField.name || dataField.key}</span>
<span slot="description">${dataField?.description}</span
><ha-selector
.disabled=${!dataField.required &&
!this._checkedKeys.has(dataField.key) &&
(!this.value?.data ||
this.value.data[dataField.key] === undefined)}
.hass=${this.hass}
.selector=${dataField.selector}
.key=${dataField.key}
@value-changed=${this._serviceDataChanged}
.value=${this.value?.data &&
this.value.data[dataField.key] !== undefined
? this.value.data[dataField.key]
: dataField.default}
></ha-selector
></ha-settings-row>`
: ""
)} `;
}
private _checkboxChanged(ev) {
const checked = ev.currentTarget.checked;
const key = ev.currentTarget.key;
if (checked) {
this._checkedKeys.add(key);
} else {
this._checkedKeys.delete(key);
const data = { ...this.value?.data };
delete data[key];
fireEvent(this, "value-changed", {
value: {
...this.value,
data,
},
});
}
this.requestUpdate("_checkedKeys");
}
private _serviceChanged(ev: PolymerChangedEvent<string>) {
ev.stopPropagation();
if (ev.detail.value === this.value?.service) {
return;
}
fireEvent(this, "value-changed", {
value: { service: ev.detail.value || "" },
});
}
private _entityPicked(ev: CustomEvent) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this.value?.data?.entity_id === newValue) {
return;
}
let value;
if (!newValue && this.value?.data) {
value = { ...this.value };
delete value.data.entity_id;
} else {
value = {
...this.value,
data: { ...this.value?.data, entity_id: ev.detail.value },
};
}
fireEvent(this, "value-changed", {
value,
});
}
private _targetChanged(ev: CustomEvent) {
ev.stopPropagation();
const newValue = ev.detail.value;
if (this.value?.target === newValue) {
return;
}
let value;
if (!newValue) {
value = { ...this.value };
delete value.target;
} else {
value = { ...this.value, target: ev.detail.value };
}
fireEvent(this, "value-changed", {
value,
});
}
private _serviceDataChanged(ev: CustomEvent) {
ev.stopPropagation();
const key = (ev.currentTarget as any).key;
const value = ev.detail.value;
if (this.value?.data && this.value.data[key] === value) {
return;
}
const data = { ...this.value?.data, [key]: value };
if (value === "" || value === undefined) {
delete data[key];
}
fireEvent(this, "value-changed", {
value: {
...this.value,
data,
},
});
}
private _dataChanged(ev: CustomEvent) {
ev.stopPropagation();
if (!ev.detail.isValid) {
return;
}
fireEvent(this, "value-changed", {
value: {
...this.value,
data: ev.detail.value,
},
});
}
static get styles(): CSSResult {
return css`
ha-settings-row {
padding: var(--service-control-padding, 0 16px);
}
ha-settings-row {
--paper-time-input-justify-content: flex-end;
border-top: var(
--service-control-items-border-top,
1px solid var(--divider-color)
);
}
ha-service-picker,
ha-entity-picker,
ha-yaml-editor {
display: block;
margin: var(--service-control-padding, 0 16px);
}
ha-yaml-editor {
padding: 16px 0;
}
p {
margin: var(--service-control-padding, 0 16px);
padding: 16px 0;
}
:host(:not([narrow])) ha-settings-row paper-input {
width: 60%;
}
:host(:not([narrow])) ha-settings-row ha-selector {
width: 60%;
}
.checkbox-spacer {
width: 32px;
}
ha-checkbox {
margin-left: -16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-service-control": HaServiceControl;
}
}

View File

@@ -1,60 +0,0 @@
import { html } from "@polymer/polymer/lib/utils/html-tag";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import LocalizeMixin from "../mixins/localize-mixin";
import "./ha-combo-box";
/*
* @appliesMixin LocalizeMixin
*/
class HaServicePicker extends LocalizeMixin(PolymerElement) {
static get template() {
return html`
<ha-combo-box
label="[[localize('ui.components.service-picker.service')]]"
items="[[_services]]"
value="{{value}}"
allow-custom-value=""
></ha-combo-box>
`;
}
static get properties() {
return {
hass: {
type: Object,
observer: "_hassChanged",
},
_services: Array,
value: {
type: String,
notify: true,
},
};
}
_hassChanged(hass, oldHass) {
if (!hass) {
this._services = [];
return;
}
if (oldHass && hass.services === oldHass.services) {
return;
}
const result = [];
Object.keys(hass.services)
.sort()
.forEach((domain) => {
const services = Object.keys(hass.services[domain]).sort();
for (let i = 0; i < services.length; i++) {
result.push(`${domain}.${services[i]}`);
}
});
this._services = result;
}
}
customElements.define("ha-service-picker", HaServicePicker);

View File

@@ -0,0 +1,135 @@
import { html, internalProperty, LitElement, property } from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { LocalizeFunc } from "../common/translations/localize";
import { domainToName } from "../data/integration";
import { HomeAssistant } from "../types";
import "./ha-combo-box";
const rowRenderer = (
root: HTMLElement,
_owner,
model: { item: { service: string; name: string } }
) => {
if (!root.firstElementChild) {
root.innerHTML = `
<style>
paper-item {
margin: -10px 0;
padding: 0;
}
</style>
<paper-item>
<paper-item-body two-line="">
<div class='name'>[[item.name]]</div>
<div secondary>[[item.service]]</div>
</paper-item-body>
</paper-item>
`;
}
root.querySelector(".name")!.textContent = model.item.name;
root.querySelector("[secondary]")!.textContent =
model.item.name === model.item.service ? "" : model.item.service;
};
class HaServicePicker extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public value?: string;
@internalProperty() private _filter?: string;
protected render() {
return html`
<ha-combo-box
.hass=${this.hass}
.label=${this.hass.localize("ui.components.service-picker.service")}
.filteredItems=${this._filteredServices(
this.hass.localize,
this.hass.services,
this._filter
)}
.value=${this.value}
.renderer=${rowRenderer}
item-value-path="service"
item-label-path="name"
allow-custom-value
@filter-changed=${this._filterChanged}
@value-changed=${this._valueChanged}
></ha-combo-box>
`;
}
private _services = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"]
): {
service: string;
name: string;
}[] => {
if (!services) {
return [];
}
const result: { service: string; name: string }[] = [];
Object.keys(services)
.sort()
.forEach((domain) => {
const services_keys = Object.keys(services[domain]).sort();
for (const service of services_keys) {
result.push({
service: `${domain}.${service}`,
name: `${domainToName(localize, domain)}: ${
services[domain][service].name || service
}`,
});
}
});
return result;
}
);
private _filteredServices = memoizeOne(
(
localize: LocalizeFunc,
services: HomeAssistant["services"],
filter?: string
) => {
if (!services) {
return [];
}
const processedServices = this._services(localize, services);
if (!filter) {
return processedServices;
}
return processedServices.filter(
(service) =>
service.service.toLowerCase().includes(filter) ||
service.name?.toLowerCase().includes(filter)
);
}
);
private _filterChanged(ev: CustomEvent): void {
this._filter = ev.detail.value.toLowerCase();
}
private _valueChanged(ev) {
this.value = ev.detail.value;
fireEvent(this, "change");
fireEvent(this, "value-changed", { value: this.value });
}
}
customElements.define("ha-service-picker", HaServicePicker);
declare global {
interface HTMLElementTagNameMap {
"ha-service-picker": HaServicePicker;
}
}

View File

@@ -6,7 +6,7 @@ import {
html,
LitElement,
property,
SVGTemplateResult,
TemplateResult,
} from "lit-element";
@customElement("ha-settings-row")
@@ -16,15 +16,18 @@ export class HaSettingsRow extends LitElement {
@property({ type: Boolean, attribute: "three-line" })
public threeLine = false;
protected render(): SVGTemplateResult {
protected render(): TemplateResult {
return html`
<paper-item-body
?two-line=${!this.threeLine}
?three-line=${this.threeLine}
>
<slot name="heading"></slot>
<div secondary><slot name="description"></slot></div>
</paper-item-body>
<div class="prefix-wrap">
<slot name="prefix"></slot>
<paper-item-body
?two-line=${!this.threeLine}
?three-line=${this.threeLine}
>
<slot name="heading"></slot>
<div secondary><slot name="description"></slot></div>
</paper-item-body>
</div>
<slot></slot>
`;
}
@@ -45,6 +48,7 @@ export class HaSettingsRow extends LitElement {
min-height: calc(
var(--paper-item-body-two-line-min-height, 72px) - 16px
);
flex: 1;
}
:host([narrow]) {
align-items: normal;
@@ -58,6 +62,13 @@ export class HaSettingsRow extends LitElement {
div[secondary] {
white-space: normal;
}
.prefix-wrap {
display: contents;
}
:host([narrow]) .prefix-wrap {
display: flex;
align-items: center;
}
`;
}
}

View File

@@ -79,6 +79,14 @@ class HaSlider extends PaperSliderClass {
return subTemplate;
}
_setImmediateValue(newImmediateValue) {
super._setImmediateValue(
this.step >= 1
? Math.round(newImmediateValue)
: Math.round(newImmediateValue * 100) / 100
);
}
_calcStep(value) {
if (!this.step) {
return parseFloat(value);

View File

@@ -10,7 +10,10 @@ import {
mdiUnfoldMoreVertical,
} from "@mdi/js";
import "@polymer/paper-tooltip/paper-tooltip";
import { UnsubscribeFunc } from "home-assistant-js-websocket";
import {
HassServiceTarget,
UnsubscribeFunc,
} from "home-assistant-js-websocket";
import {
css,
CSSResult,
@@ -41,7 +44,6 @@ import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../data/entity_registry";
import { Target } from "../data/target";
import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { HomeAssistant } from "../types";
import "./device/ha-device-picker";
@@ -56,7 +58,7 @@ import "./ha-svg-icon";
export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public value?: Target;
@property() public value?: HassServiceTarget;
@property() public label?: string;
@@ -82,6 +84,8 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
@property() public entityFilter?: HaEntityPickerEntityFilterFunc;
@property({ type: Boolean, reflect: true }) public disabled = false;
@internalProperty() private _areas?: { [areaId: string]: AreaRegistryEntry };
@internalProperty() private _devices?: {
@@ -436,7 +440,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
type: string,
id: string
): this["value"] {
const newVal = ensureArray(value![type])!.filter((val) => val !== id);
const newVal = ensureArray(value![type])!.filter(
(val) => String(val) !== id
);
if (newVal.length) {
return {
...value,
@@ -530,6 +536,9 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
.items {
z-index: 2;
}
.mdc-chip-set {
padding: 4px 0;
}
.mdc-chip.add {
color: rgba(0, 0, 0, 0.87);
}
@@ -594,6 +603,10 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
paper-tooltip.expand {
min-width: 200px;
}
:host([disabled]) .mdc-chip {
opacity: var(--light-disabled-opacity);
pointer-events: none;
}
`;
}
}

View File

@@ -5,20 +5,10 @@ import {
internalProperty,
LitElement,
property,
query,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../common/dom/fire_event";
import { afterNextRender } from "../common/util/render-status";
import "./ha-code-editor";
import type { HaCodeEditor } from "./ha-code-editor";
declare global {
// for fire event
interface HASSDomEvents {
"editor-refreshed": undefined;
}
}
const isEmpty = (obj: Record<string, unknown>): boolean => {
if (typeof obj !== "object") {
@@ -44,22 +34,14 @@ export class HaYamlEditor extends LitElement {
@internalProperty() private _yaml = "";
@query("ha-code-editor", true) private _editor?: HaCodeEditor;
public setValue(value): void {
try {
this._yaml = value && !isEmpty(value) ? safeDump(value) : "";
} catch (err) {
// eslint-disable-next-line no-console
console.error(err);
console.error(err, value);
alert(`There was an error converting to YAML: ${err}`);
}
afterNextRender(() => {
if (this._editor?.codemirror) {
this._editor.codemirror.refresh();
}
afterNextRender(() => fireEvent(this, "editor-refreshed"));
});
}
protected firstUpdated(): void {
@@ -73,7 +55,7 @@ export class HaYamlEditor extends LitElement {
return html``;
}
return html`
${this.label ? html` <p>${this.label}</p> ` : ""}
${this.label ? html`<p>${this.label}</p>` : ""}
<ha-code-editor
.value=${this._yaml}
mode="yaml"
@@ -85,13 +67,13 @@ export class HaYamlEditor extends LitElement {
private _onChange(ev: CustomEvent): void {
ev.stopPropagation();
const value = ev.detail.value;
this._yaml = ev.detail.value;
let parsed;
let isValid = true;
if (value) {
if (this._yaml) {
try {
parsed = safeLoad(value);
parsed = safeLoad(this._yaml);
} catch (err) {
// Invalid YAML
isValid = false;
@@ -107,7 +89,7 @@ export class HaYamlEditor extends LitElement {
}
get yaml() {
return this._editor?.value;
return this._yaml;
}
}

View File

@@ -6,6 +6,7 @@ import {
html,
LitElement,
property,
PropertyValues,
TemplateResult,
} from "lit-element";
import "./state-history-chart-line";
@@ -83,6 +84,10 @@ class StateHistoryCharts extends LitElement {
`;
}
protected shouldUpdate(changedProps: PropertyValues): boolean {
return !(changedProps.size === 1 && changedProps.has("hass"));
}
private _isHistoryEmpty(): boolean {
const historyDataEmpty =
!this.historyData ||

View File

@@ -205,9 +205,13 @@ export type Condition =
| DeviceCondition
| LogicalCondition;
export const triggerAutomation = (hass: HomeAssistant, entityId: string) => {
export const triggerAutomationActions = (
hass: HomeAssistant,
entityId: string
) => {
hass.callService("automation", "trigger", {
entity_id: entityId,
skip_condition: true,
});
};

View File

@@ -9,6 +9,7 @@ export interface ConfigEntry {
connection_class: string;
supports_options: boolean;
supports_unload: boolean;
disabled_by: string | null;
}
export interface ConfigEntryMutableParams {
@@ -43,6 +44,27 @@ export const reloadConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
require_restart: boolean;
}>("POST", `config/config_entries/entry/${configEntryId}/reload`);
export const disableConfigEntry = (
hass: HomeAssistant,
configEntryId: string
) =>
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: "user",
});
export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
hass.callWS<{
require_restart: boolean;
}>({
type: "config_entries/disable",
entry_id: configEntryId,
disabled_by: null,
});
export const getConfigEntrySystemOptions = (
hass: HomeAssistant,
configEntryId: string

View File

@@ -65,16 +65,18 @@ export const deleteConfigFlow = (hass: HomeAssistant, flowId: string) =>
export const getConfigFlowHandlers = (hass: HomeAssistant) =>
hass.callApi<string[]>("GET", "config/config_entries/flow_handlers");
const fetchConfigFlowInProgress = (conn) =>
export const fetchConfigFlowInProgress = (
conn: Connection
): Promise<DataEntryFlowProgress[]> =>
conn.sendMessagePromise({
type: "config_entries/flow/progress",
});
const subscribeConfigFlowInProgressUpdates = (conn, store) =>
const subscribeConfigFlowInProgressUpdates = (conn: Connection, store) =>
conn.subscribeEvents(
debounce(
() =>
fetchConfigFlowInProgress(conn).then((flows) =>
fetchConfigFlowInProgress(conn).then((flows: DataEntryFlowProgress[]) =>
store.setState(flows, true)
),
500,

View File

@@ -1,21 +1,40 @@
import { atLeastVersion } from "../../common/config/version";
import { HaFormSchema } from "../../components/ha-form/ha-form";
import { HomeAssistant } from "../../types";
import { SupervisorArch } from "../supervisor/supervisor";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export type AddonStage = "stable" | "experimental" | "deprecated";
export type AddonAppArmour = "disable" | "default" | "profile";
export type AddonRole = "default" | "homeassistant" | "manager" | "admin";
export type AddonStartup =
| "initialize"
| "system"
| "services"
| "application"
| "once";
export type AddonState = "started" | "stopped" | null;
export type AddonRepository = "core" | "local" | string;
interface AddonTranslations {
[key: string]: Record<string, Record<string, Record<string, string>>>;
}
export interface HassioAddonInfo {
advanced: boolean;
available: boolean;
build: boolean;
description: string;
detached: boolean;
homeassistant: string;
icon: boolean;
installed: boolean;
logo: boolean;
name: string;
repository: "core" | "local" | string;
repository: AddonRepository;
slug: string;
stage: "stable" | "experimental" | "deprecated";
state: "started" | "stopped" | null;
stage: AddonStage;
state: AddonState;
update_available: boolean;
url: string | null;
version_latest: string;
@@ -23,8 +42,8 @@ export interface HassioAddonInfo {
}
export interface HassioAddonDetails extends HassioAddonInfo {
apparmor: "disable" | "default" | "profile";
arch: "armhf" | "aarch64" | "i386" | "amd64";
apparmor: AddonAppArmour;
arch: SupervisorArch[];
audio_input: null | string;
audio_output: null | string;
audio: boolean;
@@ -41,10 +60,9 @@ export interface HassioAddonDetails extends HassioAddonInfo {
full_access: boolean;
gpio: boolean;
hassio_api: boolean;
hassio_role: "default" | "homeassistant" | "manager" | "admin";
hassio_role: AddonRole;
hostname: string;
homeassistant_api: boolean;
homeassistant: string;
host_dbus: boolean;
host_ipc: boolean;
host_network: boolean;
@@ -63,11 +81,12 @@ export interface HassioAddonDetails extends HassioAddonInfo {
privileged: any;
protected: boolean;
rating: "1-6";
schema: HaFormSchema[];
schema: HaFormSchema[] | null;
services_role: string[];
slug: string;
startup: "initialize" | "system" | "services" | "application" | "once";
startup: AddonStartup;
stdin: boolean;
translations: AddonTranslations;
watchdog: null | boolean;
webui: null | string;
}
@@ -101,10 +120,28 @@ export interface HassioAddonSetOptionParams {
}
export const reloadHassioAddons = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/addons/reload",
method: "post",
});
return;
}
await hass.callApi<HassioResponse<void>>("POST", `hassio/addons/reload`);
};
export const fetchHassioAddonsInfo = async (hass: HomeAssistant) => {
export const fetchHassioAddonsInfo = async (
hass: HomeAssistant
): Promise<HassioAddonsInfo> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/addons",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioAddonsInfo>>("GET", `hassio/addons`)
);
@@ -113,7 +150,15 @@ export const fetchHassioAddonsInfo = async (hass: HomeAssistant) => {
export const fetchHassioAddonInfo = async (
hass: HomeAssistant,
slug: string
) => {
): Promise<HassioAddonDetails> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/info`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioAddonDetails>>(
"GET",
@@ -148,6 +193,16 @@ export const setHassioAddonOption = async (
slug: string,
data: HassioAddonSetOptionParams
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/options`,
method: "post",
data,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/options`,
@@ -158,21 +213,64 @@ export const setHassioAddonOption = async (
export const validateHassioAddonOption = async (
hass: HomeAssistant,
slug: string
) => {
return await hass.callApi<
HassioResponse<{ message: string; valid: boolean }>
>("POST", `hassio/addons/${slug}/options/validate`);
): Promise<{ message: string; valid: boolean }> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/options/validate`,
method: "post",
});
}
return (
await hass.callApi<HassioResponse<{ message: string; valid: boolean }>>(
"POST",
`hassio/addons/${slug}/options/validate`
)
).data;
};
export const startHassioAddon = async (hass: HomeAssistant, slug: string) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/start`,
method: "post",
timeout: null,
});
}
return hass.callApi<string>("POST", `hassio/addons/${slug}/start`);
};
export const stopHassioAddon = async (hass: HomeAssistant, slug: string) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/stop`,
method: "post",
timeout: null,
});
}
return hass.callApi<string>("POST", `hassio/addons/${slug}/stop`);
};
export const setHassioAddonSecurity = async (
hass: HomeAssistant,
slug: string,
data: HassioAddonSetSecurityParams
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/security`,
method: "post",
data,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/security`,
@@ -180,15 +278,61 @@ export const setHassioAddonSecurity = async (
);
};
export const installHassioAddon = async (hass: HomeAssistant, slug: string) => {
return hass.callApi<HassioResponse<void>>(
export const installHassioAddon = async (
hass: HomeAssistant,
slug: string
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/install`,
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/install`
);
};
export const restartHassioAddon = async (hass: HomeAssistant, slug: string) => {
return hass.callApi<HassioResponse<void>>(
export const updateHassioAddon = async (
hass: HomeAssistant,
slug: string
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/store/addons/${slug}/update`,
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/update`
);
};
export const restartHassioAddon = async (
hass: HomeAssistant,
slug: string
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/restart`,
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/restart`
);
@@ -198,6 +342,16 @@ export const uninstallHassioAddon = async (
hass: HomeAssistant,
slug: string
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/addons/${slug}/uninstall`,
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/addons/${slug}/uninstall`

View File

@@ -1,3 +1,4 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
export interface HassioResponse<T> {
@@ -33,6 +34,14 @@ export const fetchHassioStats = async (
hass: HomeAssistant,
container: string
): Promise<HassioStats> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/${container}/stats`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioStats>>(
"GET",

View File

@@ -1,3 +1,4 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
@@ -5,7 +6,17 @@ interface HassioDockerRegistries {
[key: string]: { username: string; password?: string };
}
export const fetchHassioDockerRegistries = async (hass: HomeAssistant) => {
export const fetchHassioDockerRegistries = async (
hass: HomeAssistant
): Promise<HassioDockerRegistries> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/docker/registries`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
"GET",
@@ -18,6 +29,16 @@ export const addHassioDockerRegistry = async (
hass: HomeAssistant,
data: HassioDockerRegistries
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/docker/registries`,
method: "post",
data,
});
return;
}
await hass.callApi<HassioResponse<HassioDockerRegistries>>(
"POST",
"hassio/docker/registries",
@@ -29,6 +50,15 @@ export const removeHassioDockerRegistry = async (
hass: HomeAssistant,
registry: string
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/docker/registries/${registry}`,
method: "delete",
});
return;
}
await hass.callApi<HassioResponse<void>>(
"DELETE",
`hassio/docker/registries/${registry}`

View File

@@ -1,3 +1,4 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
@@ -21,7 +22,17 @@ export interface HassioHardwareInfo {
audio: Record<string, unknown>;
}
export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => {
export const fetchHassioHardwareAudio = async (
hass: HomeAssistant
): Promise<HassioHardwareAudioList> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/hardware/audio`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHardwareAudioList>>(
"GET",
@@ -30,7 +41,17 @@ export const fetchHassioHardwareAudio = async (hass: HomeAssistant) => {
);
};
export const fetchHassioHardwareInfo = async (hass: HomeAssistant) => {
export const fetchHassioHardwareInfo = async (
hass: HomeAssistant
): Promise<HassioHardwareInfo> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/hardware/info`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHardwareInfo>>(
"GET",

View File

@@ -1,3 +1,4 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
@@ -5,6 +6,7 @@ export type HassioHostInfo = {
chassis: string;
cpe: string;
deployment: string;
disk_life_time: number | "";
disk_free: number;
disk_total: number;
disk_used: number;
@@ -22,7 +24,17 @@ export interface HassioHassOSInfo {
version: string | null;
}
export const fetchHassioHostInfo = async (hass: HomeAssistant) => {
export const fetchHassioHostInfo = async (
hass: HomeAssistant
): Promise<HassioHostInfo> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/host/info",
method: "get",
});
}
const response = await hass.callApi<HassioResponse<HassioHostInfo>>(
"GET",
"hassio/host/info"
@@ -30,7 +42,17 @@ export const fetchHassioHostInfo = async (hass: HomeAssistant) => {
return hassioApiResultExtractor(response);
};
export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => {
export const fetchHassioHassOsInfo = async (
hass: HomeAssistant
): Promise<HassioHassOSInfo> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/os/info",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHassOSInfo>>(
"GET",
@@ -40,22 +62,67 @@ export const fetchHassioHassOsInfo = async (hass: HomeAssistant) => {
};
export const rebootHost = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/host/reboot",
method: "post",
timeout: null,
});
}
return hass.callApi<HassioResponse<void>>("POST", "hassio/host/reboot");
};
export const shutdownHost = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/host/shutdown",
method: "post",
timeout: null,
});
}
return hass.callApi<HassioResponse<void>>("POST", "hassio/host/shutdown");
};
export const updateOS = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/os/update",
method: "post",
timeout: null,
});
}
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/update");
};
export const configSyncOS = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "os/config/sync",
method: "post",
timeout: null,
});
}
return hass.callApi<HassioResponse<void>>("POST", "hassio/os/config/sync");
};
export const changeHostOptions = async (hass: HomeAssistant, options: any) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/host/options",
method: "post",
data: options,
});
}
return hass.callApi<HassioResponse<void>>(
"POST",
"hassio/host/options",

Some files were not shown because too many files have changed in this diff Show More