Compare commits

..

159 Commits

Author SHA1 Message Date
Ludeeus
2a4cebf724 Block update on error and not running state 2021-03-01 23:12:13 +00: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
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
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
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
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
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
246 changed files with 13062 additions and 20980 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

@@ -369,14 +369,13 @@ gulp.task(
const newData = {};
Object.entries(data).forEach(([key, value]) => {
// Filter out translations without native name.
if (data[key].nativeName) {
newData[key] = data[key];
if (value.nativeName) {
newData[key] = value;
} else {
console.warn(
`Skipping language ${key}. Native name was not translated.`
);
}
if (data[key]) newData[key] = value;
});
return newData;
})

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

@@ -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,46 +50,28 @@ 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`
@@ -159,6 +140,31 @@ 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!}
></hassio-addon-repository>
`
: html``;
});
}
);
private _handleAction(ev: CustomEvent<ActionDetail>) {
switch (ev.detail.index) {
case 0:
@@ -181,7 +187,7 @@ class HassioAddonStore extends LitElement {
private async _manageRepositories() {
showRepositoriesDialog(this, {
repos: this._repos!,
repos: this.supervisor.addon.repositories,
loadData: () => this._loadData(),
});
}
@@ -191,18 +197,10 @@ class HassioAddonStore extends LitElement {
}
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

@@ -26,16 +26,15 @@ class HassioAddonConfigDashboard extends LitElement {
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}

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 {
@@ -48,6 +52,8 @@ class HassioAddonConfig extends LitElement {
@internalProperty() private _canShowSchema = false;
@internalProperty() private _showOptional = false;
@internalProperty() private _error?: string;
@internalProperty() private _options?: Record<string, unknown>;
@@ -56,7 +62,21 @@ class HassioAddonConfig extends LitElement {
@query("ha-yaml-editor") private _editor?: HaYamlEditor;
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>
@@ -78,11 +98,16 @@ class HassioAddonConfig extends LitElement {
</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}
.schema=${this._showOptional
? this.addon.schema!
: this._filteredShchema(
this.addon.options,
this.addon.schema!
)}
></ha-form>`
: html` <ha-yaml-editor
@value-changed=${this._configChanged}
@@ -94,7 +119,19 @@ class HassioAddonConfig extends LitElement {
? ""
: html` <div class="errors">Invalid YAML</div> `}
</div>
<div class="card-actions">
${hasHiddenOptions
? html`<ha-formfield
class="show-additional"
label="Show unused optional configuration options"
>
<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}
@@ -108,7 +145,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 +181,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> {
@@ -202,8 +241,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,
@@ -271,6 +311,13 @@ class HassioAddonConfig extends LitElement {
margin-block: 0px;
font-weight: normal;
}
.card-actions.right {
justify-content: flex-end;
}
.show-additional {
padding: 16px;
}
`,
];
}

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,8 +67,14 @@ 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[] = [
@@ -106,6 +124,7 @@ class HassioAddonDashboard extends LitElement {
.route=${route}
.narrow=${this.narrow}
.hass=${this.hass}
.supervisor=${this.supervisor}
.addon=${this.addon}
></hassio-addon-router>
</hass-tabs-subpage>
@@ -152,30 +171,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,
@@ -137,11 +144,22 @@ 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",
@@ -169,22 +187,32 @@ class HassioAddonInfo extends LitElement {
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 add-on is not compatible with the processor of
your device or the operating system you have installed
on your device.
</p>
`
: html`
<p class="warning">
You are running Home Assistant
${this.supervisor.core.version}, to update to this
version of the add-on you need at least version
${addonStoreInfo.homeassistant} of Home Assistant
</p>
`
: ""}
</div>
<div class="card-actions">
<ha-call-api-button
.hass=${this.hass}
.disabled=${!this.addon.available}
path="hassio/addons/${this.addon.slug}/update"
>
<mwc-button @click=${this._updateClicked}>
Update
</ha-call-api-button>
</mwc-button>
${this.addon.changelog
? html`
<mwc-button @click=${this._openChangelog}>
@@ -534,87 +562,102 @@ 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 add-on is not compatible with the processor of your
device or the operating system you have installed on your
device.
</p>
`
: html`
<p class="warning">
You are running Home Assistant
${this.supervisor.core.version}, to install this add-on you
need at least version ${addonStoreInfo!.homeassistant} of
Home Assistant
</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>
<div>
${this.addon.version
? this._computeIsRunning
? html`
<ha-progress-button
class="warning"
@click=${this._stopClicked}
>
Stop
</ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._restartClicked}
>
Restart
</ha-progress-button>
`
: html`
<ha-progress-button @click=${this._startClicked}>
Start
</ha-progress-button>
`
: html`
<ha-progress-button
.disabled=${!this.addon.available}
@click=${this._installClicked}
>
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>
Open web UI
</mwc-button>
</a>
`
: ""}
${this._computeShowIngressUI
? html`
<mwc-button @click=${this._openIngress}>
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}
>
Uninstall
</ha-progress-button>
${this.addon.build
? html`
<ha-call-api-button
class="warning"
.hass=${this.hass}
.path="hassio/addons/${this.addon.slug}/rebuild"
>
Rebuild
</ha-call-api-button>
`
: ""}`
: ""}
</div>
</div>
</ha-card>
@@ -848,6 +891,55 @@ class HassioAddonInfo extends LitElement {
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: "Failed to stop addon",
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: "Failed to restart addon",
text: extractApiErrorMessage(err),
});
}
button.progress = false;
}
private async _updateClicked(): Promise<void> {
showDialogSupervisorAddonUpdate(this, {
addon: this.addon,
supervisor: this.supervisor,
});
}
private async _startClicked(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
@@ -856,10 +948,10 @@ 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],
text: validate.message.split(" Got ")[0],
confirm: () => this._openConfiguration(),
confirmText: "Go to configuration",
dismissText: "Cancel",
@@ -879,6 +971,12 @@ 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",
@@ -994,9 +1092,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 +1114,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 +1151,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

@@ -10,6 +10,7 @@ import {
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-card";
import "../../../src/components/ha-svg-icon";
@@ -30,6 +31,7 @@ import {
} 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";
@customElement("hassio-update")
@@ -64,6 +66,7 @@ export class HassioUpdate extends LitElement {
<div class="card-group">
${this._renderUpdateCard(
"Home Assistant Core",
"core",
this.supervisor.core,
"hassio/homeassistant/update",
`https://${
@@ -72,6 +75,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 +83,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 +96,7 @@ export class HassioUpdate extends LitElement {
private _renderUpdateCard(
name: string,
key: string,
object: HassioHomeAssistantInfo | HassioSupervisorInfo | HassioHassOSInfo,
apiPath: string,
releaseNotesUrl: string
@@ -116,6 +122,7 @@ export class HassioUpdate extends LitElement {
<ha-progress-button
.apiPath=${apiPath}
.name=${name}
.key=${key}
.version=${object.version_latest}
@click=${this._confirmUpdate}
>
@@ -128,6 +135,10 @@ export class HassioUpdate extends LitElement {
private async _confirmUpdate(ev): Promise<void> {
const item = ev.currentTarget;
if (item.key === "core") {
showDialogSupervisorCoreUpdate(this, { supervisor: this.supervisor });
return;
}
item.progress = true;
const confirmed = await showConfirmationDialog(this, {
title: `Update ${item.name}`,
@@ -142,10 +153,17 @@ export class HassioUpdate extends LitElement {
}
try {
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",
text: extractApiErrorMessage(err),

View File

@@ -0,0 +1,190 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
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 { Supervisor } from "../../../../src/data/supervisor/supervisor";
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 {
@property({ attribute: false }) public supervisor!: Supervisor;
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;
this.supervisor = params.supervisor;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._opened = false;
this._error = undefined;
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
.disabled=${this._error !== undefined ||
this.supervisor.info.state !== "running"}
@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,19 @@
import { fireEvent } from "../../../../src/common/dom/fire_event";
import { HassioAddonDetails } from "../../../../src/data/hassio/addon";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorAddonUpdateParams {
addon: HassioAddonDetails;
supervisor: Supervisor;
}
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,182 @@
import "@material/mwc-button/mwc-button";
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
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 { updateCore } from "../../../../src/data/supervisor/core";
import { Supervisor } from "../../../../src/data/supervisor/supervisor";
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 {
@property({ attribute: false }) public supervisor!: Supervisor;
public hass!: HomeAssistant;
@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.supervisor = params.supervisor;
await this.updateComplete;
}
public closeDialog(): void {
this._action = null;
this._createSnapshot = true;
this._opened = false;
this._error = undefined;
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.supervisor.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
.disabled=${this._error !== undefined ||
this.supervisor.info.state !== "running"}
@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.supervisor.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.supervisor.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 { Supervisor } from "../../../../src/data/supervisor/supervisor";
export interface SupervisorDialogSupervisorCoreUpdateParams {
supervisor: Supervisor;
}
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

@@ -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

@@ -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 { supervisorCollection } 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";
@@ -71,8 +73,17 @@ export class HassioMain extends SupervisorBaseElement {
protected render() {
if (!this.supervisor || !this.hass) {
return html``;
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-router
.hass=${this.hass}

View File

@@ -0,0 +1,128 @@
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";
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() 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 = html`This redirect is not supported by your Home Assistant
instance. Check the
<a
target="_blank"
rel="noreferrer noopener"
href="https://my.home-assistant.io/faq.html#supported-pages"
>My Home Assistant FAQ</a
>
for the supported redirects and the version they where introduced.`;
return;
}
let url: string;
try {
url = this._createRedirectUrl(redirect);
} catch (err) {
this._error = "An unknown error occured";
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

@@ -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

@@ -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";
@@ -211,7 +212,13 @@ class HassioSnapshots extends LitElement {
: undefined}
</div>
<div class="card-actions">
<ha-progress-button @click=${this._createSnapshot}>
<ha-progress-button
@click=${this._createSnapshot}
title="${this.supervisor.info.state !== "running"
? `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`
: ""}"
.disabled=${this.supervisor.info.state !== "running"}
>
Create
</ha-progress-button>
</div>
@@ -325,6 +332,12 @@ class HassioSnapshots extends LitElement {
}
private async _createSnapshot(ev: CustomEvent): Promise<void> {
if (this.supervisor.info.state !== "running") {
await showAlertDialog(this, {
title: "Could not create snapshot",
text: `Creating a snapshot is not possible right now because the system is in ${this.supervisor.info.state} state.`,
});
}
const button = ev.currentTarget as any;
button.progress = true;
@@ -386,6 +399,7 @@ class HassioSnapshots extends LitElement {
private _snapshotClicked(ev) {
showHassioSnapshotDialog(this, {
slug: ev.currentTarget!.snapshot.slug,
supervisor: this.supervisor,
onDelete: () => this._updateSnapshots(),
});
}
@@ -395,6 +409,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,13 @@
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 { fetchHassioAddonsInfo } from "../../src/data/hassio/addon";
import { HassioResponse } from "../../src/data/hassio/common";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
@@ -10,13 +19,21 @@ 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";
declare global {
interface HASSDomEvents {
"supervisor-update": Partial<Supervisor>;
"supervisor-colllection-refresh": { colllection: SupervisorObject };
}
}
@@ -25,6 +42,20 @@ export class SupervisorBaseElement extends urlSyncMixin(
) {
@property({ attribute: false }) public supervisor?: Supervisor;
@internalProperty() private _unsubs: Record<string, UnsubscribeFunc> = {};
@internalProperty() private _collections: Record<
string,
Collection<unknown>
> = {};
public disconnectedCallback() {
super.disconnectedCallback();
Object.keys(this._unsubs).forEach((unsub) => {
this._unsubs[unsub]();
});
}
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor!, ...obj };
}
@@ -32,13 +63,59 @@ export class SupervisorBaseElement extends urlSyncMixin(
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._initSupervisor();
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
}
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> {
this.addEventListener(
"supervisor-colllection-refresh",
this._handleSupervisorStoreRefreshEvent
);
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]
);
}
});
if (this.supervisor === undefined) {
Object.keys(this._collections).forEach((collection) =>
this._updateSupervisor({
[collection]: this._collections[collection].state,
})
);
}
return;
}
const [
addon,
supervisor,
host,
core,
@@ -46,7 +123,9 @@ export class SupervisorBaseElement extends urlSyncMixin(
os,
network,
resolution,
store,
] = await Promise.all([
fetchHassioAddonsInfo(this.hass),
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
@@ -54,9 +133,11 @@ export class SupervisorBaseElement extends urlSyncMixin(
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
fetchSupervisorStore(this.hass),
]);
this.supervisor = {
addon,
supervisor,
host,
core,
@@ -64,6 +145,11 @@ export class SupervisorBaseElement extends urlSyncMixin(
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")
@@ -139,41 +140,19 @@ 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: "Failed to restart Home Assistant Core",
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, { supervisor: this.supervisor });
}
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,
@@ -150,6 +150,18 @@ 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">
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`
@@ -328,11 +340,14 @@ 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: "Failed to update",
text: extractApiErrorMessage(err),
});
}
}
button.progress = false;
}
@@ -356,8 +371,9 @@ class HassioHostInfo extends LitElement {
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",
@@ -370,8 +386,9 @@ 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",
@@ -381,8 +398,14 @@ class HassioHostInfo extends LitElement {
}
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,
@@ -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> {
@@ -368,6 +368,9 @@ 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",

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="20210301.0",
description="The Home Assistant frontend",
url="https://github.com/home-assistant/home-assistant-polymer",
author="The Home Assistant Authors",

View File

@@ -1,11 +1,19 @@
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,36 @@
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;
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 +38,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 +56,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,10 +77,10 @@ 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;
watchdog: null | boolean;
webui: null | string;
@@ -101,10 +115,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 +145,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 +188,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 +208,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 +273,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 +337,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",

View File

@@ -1,26 +1,50 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { HassioResponse } from "./common";
import { CreateSessionResponse } from "./supervisor";
export const createHassioSession = async (hass: HomeAssistant) => {
const response = await hass.callApi<HassioResponse<CreateSessionResponse>>(
"POST",
"hassio/ingress/session"
);
document.cookie = `ingress_session=${
response.data.session
};path=/api/hassio_ingress/;SameSite=Strict${
function setIngressCookie(session: string): string {
document.cookie = `ingress_session=${session};path=/api/hassio_ingress/;SameSite=Strict${
location.protocol === "https:" ? ";Secure" : ""
}`;
return response.data.session;
return session;
}
export const createHassioSession = async (
hass: HomeAssistant
): Promise<string> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
const wsResponse: { session: string } = await hass.callWS({
type: "supervisor/api",
endpoint: "/ingress/session",
method: "post",
});
return setIngressCookie(wsResponse.session);
}
const restResponse: { data: { session: string } } = await hass.callApi<
HassioResponse<CreateSessionResponse>
>("POST", "hassio/ingress/session");
return setIngressCookie(restResponse.data.session);
};
export const validateHassioSession = async (
hass: HomeAssistant,
session: string
) =>
await hass.callApi<HassioResponse<null>>(
): Promise<void> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/ingress/validate_session",
method: "post",
data: { session },
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
"hassio/ingress/validate_session",
{ session }
);
};

View File

@@ -1,3 +1,4 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
@@ -51,7 +52,17 @@ export interface NetworkInfo {
docker: DockerNetwork;
}
export const fetchNetworkInfo = async (hass: HomeAssistant) => {
export const fetchNetworkInfo = async (
hass: HomeAssistant
): Promise<NetworkInfo> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/network/info",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<NetworkInfo>>(
"GET",
@@ -65,6 +76,17 @@ export const updateNetworkInterface = async (
network_interface: string,
options: Partial<NetworkInterface>
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: `/network/interface/${network_interface}/update`,
method: "post",
data: options,
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<NetworkInfo>>(
"POST",
`hassio/network/interface/${network_interface}/update`,
@@ -75,7 +97,16 @@ export const updateNetworkInterface = async (
export const accesspointScan = async (
hass: HomeAssistant,
network_interface: string
) => {
): Promise<AccessPoints> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/network/interface/${network_interface}/accesspoints`,
method: "get",
timeout: null,
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<AccessPoints>>(
"GET",

View File

@@ -1,3 +1,4 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
@@ -8,7 +9,17 @@ export interface HassioResolution {
suggestions: string[];
}
export const fetchHassioResolution = async (hass: HomeAssistant) => {
export const fetchHassioResolution = async (
hass: HomeAssistant
): Promise<HassioResolution> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/resolution/info",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioResolution>>(
"GET",

View File

@@ -1,3 +1,4 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { hassioApiResultExtractor, HassioResponse } from "./common";
@@ -28,12 +29,24 @@ export interface HassioFullSnapshotCreateParams {
}
export interface HassioPartialSnapshotCreateParams {
name: string;
folders: string[];
addons: string[];
folders?: string[];
addons?: string[];
password?: string;
homeassistant?: boolean;
}
export const fetchHassioSnapshots = async (hass: HomeAssistant) => {
export const fetchHassioSnapshots = async (
hass: HomeAssistant
): Promise<HassioSnapshot[]> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
const data: { snapshots: HassioSnapshot[] } = await hass.callWS({
type: "supervisor/api",
endpoint: `/snapshots`,
method: "get",
});
return data.snapshots;
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<{ snapshots: HassioSnapshot[] }>>(
"GET",
@@ -45,8 +58,15 @@ export const fetchHassioSnapshots = async (hass: HomeAssistant) => {
export const fetchHassioSnapshotInfo = async (
hass: HomeAssistant,
snapshot: string
) => {
): Promise<HassioSnapshotDetail> => {
if (hass) {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: `/snapshots/${snapshot}/info`,
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioSnapshotDetail>>(
"GET",
@@ -63,6 +83,15 @@ export const fetchHassioSnapshotInfo = async (
};
export const reloadHassioSnapshots = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/snapshots/reload",
method: "post",
});
return;
}
await hass.callApi<HassioResponse<void>>("POST", `hassio/snapshots/reload`);
};
@@ -70,6 +99,15 @@ export const createHassioFullSnapshot = async (
hass: HomeAssistant,
data: HassioFullSnapshotCreateParams
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/snapshots/new/full",
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/snapshots/new/full`,
@@ -79,8 +117,19 @@ export const createHassioFullSnapshot = async (
export const createHassioPartialSnapshot = async (
hass: HomeAssistant,
data: HassioFullSnapshotCreateParams
data: HassioPartialSnapshotCreateParams
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/snapshots/new/partial",
method: "post",
timeout: null,
data,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
`hassio/snapshots/new/partial`,

View File

@@ -1,9 +1,11 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant, PanelInfo } from "../../types";
import { SupervisorArch } from "../supervisor/supervisor";
import { HassioAddonInfo, HassioAddonRepository } from "./addon";
import { hassioApiResultExtractor, HassioResponse } from "./common";
export type HassioHomeAssistantInfo = {
arch: string;
arch: SupervisorArch;
audio_input: string | null;
audio_output: string | null;
boot: boolean;
@@ -22,7 +24,7 @@ export type HassioHomeAssistantInfo = {
export type HassioSupervisorInfo = {
addons: HassioAddonInfo[];
addons_repositories: HassioAddonRepository[];
arch: string;
arch: SupervisorArch;
channel: string;
debug: boolean;
debug_block: boolean;
@@ -39,7 +41,7 @@ export type HassioSupervisorInfo = {
};
export type HassioInfo = {
arch: string;
arch: SupervisorArch;
channel: string;
docker: string;
features: string[];
@@ -48,10 +50,19 @@ export type HassioInfo = {
hostname: string;
logging: string;
machine: string;
state:
| "initialize"
| "setup"
| "startup"
| "running"
| "freeze"
| "shutdown"
| "stopping"
| "close";
operating_system: string;
supervisor: string;
supported: boolean;
supported_arch: string[];
supported_arch: SupervisorArch[];
timezone: string;
};
@@ -73,18 +84,57 @@ export interface SupervisorOptions {
}
export const reloadSupervisor = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/supervisor/reload",
method: "post",
});
return;
}
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/reload`);
};
export const restartSupervisor = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/supervisor/restart",
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/restart`);
};
export const updateSupervisor = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/supervisor/update",
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>("POST", `hassio/supervisor/update`);
};
export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
export const fetchHassioHomeAssistantInfo = async (
hass: HomeAssistant
): Promise<HassioHomeAssistantInfo> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/core/info",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioHomeAssistantInfo>>(
"GET",
@@ -93,7 +143,17 @@ export const fetchHassioHomeAssistantInfo = async (hass: HomeAssistant) => {
);
};
export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => {
export const fetchHassioSupervisorInfo = async (
hass: HomeAssistant
): Promise<HassioSupervisorInfo> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/supervisor/info",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioSupervisorInfo>>(
"GET",
@@ -102,7 +162,17 @@ export const fetchHassioSupervisorInfo = async (hass: HomeAssistant) => {
);
};
export const fetchHassioInfo = async (hass: HomeAssistant) => {
export const fetchHassioInfo = async (
hass: HomeAssistant
): Promise<HassioInfo> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/info",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<HassioInfo>>("GET", "hassio/info")
);
@@ -119,6 +189,16 @@ export const setSupervisorOption = async (
hass: HomeAssistant,
data: SupervisorOptions
) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/supervisor/options",
method: "post",
data,
});
return;
}
await hass.callApi<HassioResponse<void>>(
"POST",
"hassio/supervisor/options",

View File

@@ -4,7 +4,6 @@ import { computeStateDomain } from "../common/entity/compute_state_domain";
import { computeStateName } from "../common/entity/compute_state_name";
import { LocalizeFunc } from "../common/translations/localize";
import { HomeAssistant } from "../types";
import { UNAVAILABLE_STATES } from "./entity";
const DOMAINS_USE_LAST_UPDATED = ["climate", "humidifier", "water_heater"];
const LINE_ATTRIBUTES_TO_KEEP = [
@@ -201,23 +200,6 @@ const processLineChartEntities = (
};
};
const isNumerical = (states: HassEntity[]): boolean => {
if (states.every((state) => UNAVAILABLE_STATES.includes(state.state))) {
return false;
}
if (
states.some(
(state) =>
isNaN(parseFloat(state.state)) &&
!UNAVAILABLE_STATES.includes(state.state)
)
) {
return false;
}
return true;
};
export const computeHistory = (
hass: HomeAssistant,
stateHistory: HassEntity[][],
@@ -249,8 +231,6 @@ export const computeHistory = (
unit = hass.config.unit_system.temperature;
} else if (computeStateDomain(stateInfo[0]) === "humidifier") {
unit = "%";
} else if (isNumerical(stateInfo)) {
unit = " ";
}
if (!unit) {

View File

@@ -216,7 +216,6 @@ export const getLogbookMessage = (
case "cold":
case "gas":
case "heat":
case "colightld":
case "moisture":
case "motion":
case "occupancy":
@@ -246,9 +245,17 @@ export const getLogbookMessage = (
}
case "cover":
return state === "open"
? hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`)
: hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
switch (state) {
case "open":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_opened`);
case "opening":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.is_opening`);
case "closing":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.is_closing`);
case "closed":
return hass.localize(`${LOGBOOK_LOCALIZE_PATH}.was_closed`);
}
break;
case "lock":
if (state === "unlocked") {

View File

@@ -2,6 +2,7 @@ import {
Connection,
getCollection,
HassEventBase,
HassServiceTarget,
} from "home-assistant-js-websocket";
import { HASSDomEvent } from "../common/dom/fire_event";
import { HuiErrorCard } from "../panels/lovelace/cards/hui-error-card";
@@ -120,8 +121,8 @@ export interface ToggleActionConfig extends BaseActionConfig {
export interface CallServiceActionConfig extends BaseActionConfig {
action: "call-service";
service: string;
target?: HassServiceTarget;
service_data?: {
entity_id?: string | [string];
[key: string]: any;
};
}

View File

@@ -1,6 +1,7 @@
import {
HassEntityAttributeBase,
HassEntityBase,
HassServiceTarget,
} from "home-assistant-js-websocket";
import { computeObjectId } from "../common/entity/compute_object_id";
import { navigate } from "../common/navigate";
@@ -36,6 +37,7 @@ export interface EventAction {
export interface ServiceAction {
service: string;
entity_id?: string;
target?: HassServiceTarget;
data?: Record<string, any>;
}
@@ -115,7 +117,7 @@ export const triggerScript = (
variables?: Record<string, unknown>
) => hass.callService("script", computeObjectId(entityId), variables);
export const canExcecute = (state: ScriptEntity) => {
export const canRun = (state: ScriptEntity) => {
if (state.state === "off") {
return true;
}

View File

@@ -1,4 +1,5 @@
export type Selector =
| AddonSelector
| EntitySelector
| DeviceSelector
| AreaSelector
@@ -8,8 +9,8 @@ export type Selector =
| TimeSelector
| ActionSelector
| StringSelector
| ObjectSelector;
| ObjectSelector
| SelectSelector;
export interface EntitySelector {
entity: {
integration?: string;
@@ -30,6 +31,13 @@ export interface DeviceSelector {
};
}
export interface AddonSelector {
addon: {
name?: string;
slug?: string;
};
}
export interface AreaSelector {
area: {
entity?: {
@@ -95,3 +103,9 @@ export interface ObjectSelector {
// eslint-disable-next-line @typescript-eslint/ban-types
object: {};
}
export interface SelectSelector {
select: {
options: string[];
};
}

View File

@@ -1,3 +1,4 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { HassioResponse } from "../hassio/common";
@@ -6,5 +7,15 @@ export const restartCore = async (hass: HomeAssistant) => {
};
export const updateCore = async (hass: HomeAssistant) => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
await hass.callWS({
type: "supervisor/api",
endpoint: "/core/update",
method: "post",
timeout: null,
});
return;
}
await hass.callApi<HassioResponse<void>>("POST", `hassio/core/update`);
};

View File

@@ -0,0 +1,51 @@
import { atLeastVersion } from "../../common/config/version";
import { HomeAssistant } from "../../types";
import { AddonRepository, AddonStage } from "../hassio/addon";
import { hassioApiResultExtractor, HassioResponse } from "../hassio/common";
export interface StoreAddon {
advanced: boolean;
available: boolean;
build: boolean;
description: string;
homeassistant: string | null;
icon: boolean;
installed: boolean;
logo: boolean;
name: string;
repository: AddonRepository;
slug: string;
stage: AddonStage;
update_available: boolean;
url: string;
version: string | null;
version_latest: string;
}
interface StoreRepository {
maintainer: string;
name: string;
slug: string;
source: string;
url: string;
}
export interface SupervisorStore {
addons: StoreAddon[];
repositories: StoreRepository[];
}
export const fetchSupervisorStore = async (
hass: HomeAssistant
): Promise<SupervisorStore> => {
if (atLeastVersion(hass.config.version, 2021, 2, 4)) {
return await hass.callWS({
type: "supervisor/api",
endpoint: "/store",
method: "get",
});
}
return hassioApiResultExtractor(
await hass.callApi<HassioResponse<SupervisorStore>>("GET", `hassio/store`)
);
};

View File

@@ -1,3 +1,7 @@
import { Connection, getCollection } from "home-assistant-js-websocket";
import { Store } from "home-assistant-js-websocket/dist/store";
import { HomeAssistant } from "../../types";
import { HassioAddonsInfo } from "../hassio/addon";
import { HassioHassOSInfo, HassioHostInfo } from "../hassio/host";
import { NetworkInfo } from "../hassio/network";
import { HassioResolution } from "../hassio/resolution";
@@ -6,6 +10,50 @@ import {
HassioInfo,
HassioSupervisorInfo,
} from "../hassio/supervisor";
import { SupervisorStore } from "./store";
export const supervisorWSbaseCommand = {
type: "supervisor/api",
method: "GET",
};
export const supervisorCollection = {
host: "/host/info",
supervisor: "/supervisor/info",
info: "/info",
core: "/core/info",
network: "/network/info",
resolution: "/resolution/info",
os: "/os/info",
addon: "/addons",
store: "/store",
};
export type SupervisorArch = "armhf" | "armv7" | "aarch64" | "i386" | "amd64";
export type SupervisorObject =
| "host"
| "supervisor"
| "info"
| "core"
| "network"
| "resolution"
| "os"
| "addon"
| "store";
interface supervisorApiRequest {
endpoint: string;
method?: "get" | "post" | "delete" | "put";
force_rest?: boolean;
data?: any;
}
export interface SupervisorEvent {
event: string;
update_key?: SupervisorObject;
data?: any;
[key: string]: any;
}
export interface Supervisor {
host: HassioHostInfo;
@@ -15,4 +63,76 @@ export interface Supervisor {
network: NetworkInfo;
resolution: HassioResolution;
os: HassioHassOSInfo;
addon: HassioAddonsInfo;
store: SupervisorStore;
}
export const supervisorApiWsRequest = <T>(
conn: Connection,
request: supervisorApiRequest
): Promise<T> =>
conn.sendMessagePromise<T>({ ...supervisorWSbaseCommand, ...request });
async function processEvent(
conn: Connection,
store: Store<any>,
event: SupervisorEvent,
key: string
) {
if (event.event !== "supervisor-update" || event.update_key !== key) {
return;
}
if (Object.keys(event.data).length === 0) {
const data = await supervisorApiWsRequest<any>(conn, {
endpoint: supervisorCollection[key],
});
store.setState(data);
return;
}
const state = store.state;
if (state === undefined) {
return;
}
store.setState({
...state,
...event.data,
});
}
const subscribeSupervisorEventUpdates = (
conn: Connection,
store: Store<unknown>,
key: string
) =>
conn.subscribeMessage(
(event) => processEvent(conn, store, event as SupervisorEvent, key),
{
type: "supervisor/subscribe",
}
);
export const getSupervisorEventCollection = (
conn: Connection,
key: string,
endpoint: string
) =>
getCollection(
conn,
`_supervisor${key}Event`,
() => supervisorApiWsRequest(conn, { endpoint }),
(connection, store) =>
subscribeSupervisorEventUpdates(connection, store, key)
);
export const subscribeSupervisorEvents = (
hass: HomeAssistant,
onChange: (event) => void,
key: string,
endpoint: string
) =>
getSupervisorEventCollection(hass.connection, key, endpoint).subscribe(
onChange
);

View File

@@ -1,5 +0,0 @@
export interface Target {
entity_id?: string[];
device_id?: string[];
area_id?: string[];
}

View File

@@ -89,6 +89,11 @@ export const reconfigureNode = (
ieee: ieeeAddress,
});
export const refreshTopology = (hass: HomeAssistant): Promise<void> =>
hass.callWS({
type: "zha/topology/update",
});
export const fetchAttributesForCluster = (
hass: HomeAssistant,
ieeeAddress: string,

View File

@@ -22,7 +22,9 @@ import {
AreaRegistryEntry,
subscribeAreaRegistry,
} from "../../data/area_registry";
import { fetchConfigFlowInProgress } from "../../data/config_flow";
import type {
DataEntryFlowProgress,
DataEntryFlowProgressedEvent,
DataEntryFlowStep,
} from "../../data/data_entry_flow";
@@ -32,6 +34,7 @@ import {
} from "../../data/device_registry";
import { haStyleDialog } from "../../resources/styles";
import type { HomeAssistant } from "../../types";
import { showAlertDialog } from "../generic/show-dialog-box";
import { DataEntryFlowDialogParams } from "./show-dialog-data-entry-flow";
import "./step-flow-abort";
import "./step-flow-create-entry";
@@ -40,6 +43,7 @@ import "./step-flow-form";
import "./step-flow-loading";
import "./step-flow-pick-handler";
import "./step-flow-progress";
import "./step-flow-pick-flow";
let instance = 0;
@@ -75,6 +79,10 @@ class DataEntryFlowDialog extends LitElement {
@internalProperty() private _handlers?: string[];
@internalProperty() private _handler?: string;
@internalProperty() private _flowsInProgress?: DataEntryFlowProgress[];
private _unsubAreas?: UnsubscribeFunc;
private _unsubDevices?: UnsubscribeFunc;
@@ -83,48 +91,93 @@ class DataEntryFlowDialog extends LitElement {
this._params = params;
this._instance = instance++;
if (params.startFlowHandler) {
this._checkFlowsInProgress(params.startFlowHandler);
return;
}
if (params.continueFlowId) {
this._loading = true;
const curInstance = this._instance;
let step: DataEntryFlowStep;
try {
step = await params.flowConfig.fetchFlow(
this.hass,
params.continueFlowId
);
} catch (err) {
this._step = undefined;
this._params = undefined;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
),
});
return;
}
// Happens if second showDialog called
if (curInstance !== this._instance) {
return;
}
this._processStep(step);
this._loading = false;
return;
}
// Create a new config flow. Show picker
if (!params.continueFlowId && !params.startFlowHandler) {
if (!params.flowConfig.getFlowHandlers) {
throw new Error("No getFlowHandlers defined in flow config");
}
this._step = null;
// We only load the handlers once
if (this._handlers === undefined) {
this._loading = true;
try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally {
this._loading = false;
}
}
await this.updateComplete;
return;
if (!params.flowConfig.getFlowHandlers) {
throw new Error("No getFlowHandlers defined in flow config");
}
this._step = null;
this._loading = true;
const curInstance = this._instance;
const step = await (params.continueFlowId
? params.flowConfig.fetchFlow(this.hass, params.continueFlowId)
: params.flowConfig.createFlow(this.hass, params.startFlowHandler!));
// Happens if second showDialog called
if (curInstance !== this._instance) {
return;
// We only load the handlers once
if (this._handlers === undefined) {
this._loading = true;
try {
this._handlers = await params.flowConfig.getFlowHandlers(this.hass);
} finally {
this._loading = false;
}
}
this._processStep(step);
this._loading = false;
await this.updateComplete;
}
public closeDialog() {
if (this._step) {
this._flowDone();
} else if (this._step === null) {
// Flow aborted during picking flow
this._step = undefined;
this._params = undefined;
if (!this._params) {
return;
}
const flowFinished = Boolean(
this._step && ["create_entry", "abort"].includes(this._step.type)
);
// If we created this flow, delete it now.
if (this._step && !flowFinished && !this._params.continueFlowId) {
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
}
if (this._step !== null && this._params.dialogClosedCallback) {
this._params.dialogClosedCallback({
flowFinished,
});
}
this._step = undefined;
this._params = undefined;
this._devices = undefined;
this._flowsInProgress = undefined;
this._handler = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
fireEvent(this, "dialog-closed", { dialog: this.localName });
}
@@ -144,7 +197,9 @@ class DataEntryFlowDialog extends LitElement {
>
<div>
${this._loading ||
(this._step === null && this._handlers === undefined)
(this._step === null &&
this._handlers === undefined &&
this._handler === undefined)
? html`
<step-flow-loading
.label=${this.hass.localize(
@@ -166,15 +221,22 @@ class DataEntryFlowDialog extends LitElement {
?rtl=${computeRTL(this.hass)}
></ha-icon-button>
${this._step === null
? // Show handler picker
html`
<step-flow-pick-handler
? this._handler
? html`<step-flow-pick-flow
.flowConfig=${this._params.flowConfig}
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
></step-flow-pick-handler>
`
.handler=${this._handler}
.flowsInProgress=${this._flowsInProgress}
></step-flow-pick-flow>`
: // Show handler picker
html`
<step-flow-pick-handler
.hass=${this.hass}
.handlers=${this._handlers}
.showAdvanced=${this._params.showAdvanced}
@handler-picked=${this._handlerPicked}
></step-flow-pick-handler>
`
: this._step.type === "form"
? html`
<step-flow-form
@@ -279,6 +341,43 @@ class DataEntryFlowDialog extends LitElement {
});
}
private async _checkFlowsInProgress(handler: string) {
this._loading = true;
const flowsInProgress = (
await fetchConfigFlowInProgress(this.hass.connection)
).filter((flow) => flow.handler === handler);
if (!flowsInProgress.length) {
let step: DataEntryFlowStep;
try {
step = await this._params!.flowConfig.createFlow(this.hass, handler);
} catch (err) {
this._step = undefined;
this._params = undefined;
showAlertDialog(this, {
title: this.hass.localize(
"ui.panel.config.integrations.config_flow.error"
),
text: this.hass.localize(
"ui.panel.config.integrations.config_flow.could_not_load"
),
});
return;
}
this._processStep(step);
} else {
this._step = null;
this._handler = handler;
this._flowsInProgress = flowsInProgress;
}
this._loading = false;
}
private _handlerPicked(ev) {
this._checkFlowsInProgress(ev.detail.handler);
}
private async _processStep(
step: DataEntryFlowStep | undefined | Promise<DataEntryFlowStep>
): Promise<void> {
@@ -293,7 +392,7 @@ class DataEntryFlowDialog extends LitElement {
}
if (step === undefined) {
this._flowDone();
this.closeDialog();
return;
}
this._step = undefined;
@@ -301,38 +400,6 @@ class DataEntryFlowDialog extends LitElement {
this._step = step;
}
private _flowDone(): void {
if (!this._params) {
return;
}
const flowFinished = Boolean(
this._step && ["create_entry", "abort"].includes(this._step.type)
);
// If we created this flow, delete it now.
if (this._step && !flowFinished && !this._params.continueFlowId) {
this._params.flowConfig.deleteFlow(this.hass, this._step.flow_id);
}
if (this._params.dialogClosedCallback) {
this._params.dialogClosedCallback({
flowFinished,
});
}
this._step = undefined;
this._params = undefined;
this._devices = undefined;
if (this._unsubAreas) {
this._unsubAreas();
this._unsubAreas = undefined;
}
if (this._unsubDevices) {
this._unsubDevices();
this._unsubDevices = undefined;
}
}
static get styles(): CSSResultArray {
return [
haStyleDialog,

View File

@@ -0,0 +1,130 @@
import "@polymer/paper-item/paper-icon-item";
import "@polymer/paper-item";
import "@polymer/paper-item/paper-item-body";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import "../../components/ha-icon-next";
import { localizeConfigFlowTitle } from "../../data/config_flow";
import { DataEntryFlowProgress } from "../../data/data_entry_flow";
import { domainToName } from "../../data/integration";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
@customElement("step-flow-pick-flow")
class StepFlowPickFlow extends LitElement {
public flowConfig!: FlowConfig;
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false })
public flowsInProgress!: DataEntryFlowProgress[];
@property() public handler!: string;
protected render(): TemplateResult {
return html`
<h2>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.pick_flow_step.title"
)}
</h2>
<div>
${this.flowsInProgress.map(
(flow) => html` <paper-icon-item
@click=${this._flowInProgressPicked}
.flow=${flow}
>
<img
slot="item-icon"
loading="lazy"
src=${brandsUrl(flow.handler, "icon", true)}
referrerpolicy="no-referrer"
/>
<paper-item-body>
${localizeConfigFlowTitle(this.hass.localize, flow)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-icon-item>`
)}
<paper-item @click=${this._startNewFlowPicked} .handler=${this.handler}>
<paper-item-body>
${this.hass.localize(
"ui.panel.config.integrations.config_flow.pick_flow_step.new_flow",
"integration",
domainToName(this.hass.localize, this.handler)
)}
</paper-item-body>
<ha-icon-next></ha-icon-next>
</paper-item>
</div>
`;
}
private _startNewFlowPicked(ev) {
this._startFlow(ev.currentTarget.handler);
}
private _startFlow(handler: string) {
fireEvent(this, "flow-update", {
stepPromise: this.flowConfig.createFlow(this.hass, handler),
});
}
private _flowInProgressPicked(ev) {
const flow: DataEntryFlowProgress = ev.currentTarget.flow;
fireEvent(this, "flow-update", {
stepPromise: this.flowConfig.fetchFlow(this.hass, flow.flow_id),
});
}
static get styles(): CSSResult[] {
return [
configFlowContentStyles,
css`
img {
width: 40px;
height: 40px;
}
ha-icon-next {
margin-right: 8px;
}
div {
overflow: auto;
max-height: 600px;
margin: 16px 0;
}
h2 {
padding-right: 66px;
}
@media all and (max-height: 900px) {
div {
max-height: calc(100vh - 134px);
}
}
paper-icon-item,
paper-item {
cursor: pointer;
margin-bottom: 4px;
}
`,
];
}
}
declare global {
interface HTMLElementTagNameMap {
"step-flow-pick-flow": StepFlowPickFlow;
}
}

View File

@@ -22,7 +22,6 @@ import { domainToName } from "../../data/integration";
import { HomeAssistant } from "../../types";
import { brandsUrl } from "../../util/brands-url";
import { documentationUrl } from "../../util/documentation-url";
import { FlowConfig } from "./show-dialog-data-entry-flow";
import { configFlowContentStyles } from "./styles";
interface HandlerObj {
@@ -30,17 +29,24 @@ interface HandlerObj {
slug: string;
}
declare global {
// for fire event
interface HASSDomEvents {
"handler-picked": {
handler: string;
};
}
}
@customElement("step-flow-pick-handler")
class StepFlowPickHandler extends LitElement {
public flowConfig!: FlowConfig;
@property({ attribute: false }) public hass!: HomeAssistant;
@property() public handlers!: string[];
@property() public showAdvanced?: boolean;
@internalProperty() private filter?: string;
@internalProperty() private _filter?: string;
private _width?: number;
@@ -74,7 +80,7 @@ class StepFlowPickHandler extends LitElement {
protected render(): TemplateResult {
const handlers = this._getHandlers(
this.handlers,
this.filter,
this._filter,
this.hass.localize
);
@@ -82,7 +88,7 @@ class StepFlowPickHandler extends LitElement {
<h2>${this.hass.localize("ui.panel.config.integrations.new")}</h2>
<search-input
autofocus
.filter=${this.filter}
.filter=${this._filter}
@value-changed=${this._filterChanged}
.label=${this.hass.localize("ui.panel.config.integrations.search")}
></search-input>
@@ -164,15 +170,12 @@ class StepFlowPickHandler extends LitElement {
}
private async _filterChanged(e) {
this.filter = e.detail.value;
this._filter = e.detail.value;
}
private async _handlerPicked(ev) {
fireEvent(this, "flow-update", {
stepPromise: this.flowConfig.createFlow(
this.hass,
ev.currentTarget.handler.slug
),
fireEvent(this, "handler-picked", {
handler: ev.currentTarget.handler.slug,
});
}
@@ -195,6 +198,9 @@ class StepFlowPickHandler extends LitElement {
overflow: auto;
max-height: 600px;
}
h2 {
padding-right: 66px;
}
@media all and (max-height: 900px) {
div {
max-height: calc(100vh - 134px);

View File

@@ -10,7 +10,7 @@ import {
TemplateResult,
} from "lit-element";
import "../../../components/ha-relative-time";
import { triggerAutomation } from "../../../data/automation";
import { triggerAutomationActions } from "../../../data/automation";
import { UNAVAILABLE_STATES } from "../../../data/entity";
import { HomeAssistant } from "../../../types";
@@ -36,7 +36,7 @@ class MoreInfoAutomation extends LitElement {
<div class="actions">
<mwc-button
@click=${this.handleAction}
@click=${this._runActions}
.disabled=${UNAVAILABLE_STATES.includes(this.stateObj!.state)}
>
${this.hass.localize("ui.card.automation.trigger")}
@@ -45,8 +45,8 @@ class MoreInfoAutomation extends LitElement {
`;
}
private handleAction() {
triggerAutomation(this.hass, this.stateObj!.entity_id);
private _runActions() {
triggerAutomationActions(this.hass, this.stateObj!.entity_id);
}
static get styles(): CSSResult {

View File

@@ -52,6 +52,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
caption="[[localize('ui.card.fan.speed')]]"
min="0"
max="100"
step="[[computePercentageStepSize(stateObj)]]"
value="{{percentageSliderValue}}"
on-change="percentageChanged"
pin=""
@@ -113,7 +114,7 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
<ha-attributes
state-obj="[[stateObj]]"
extra-filters="speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
extra-filters="percentage_step,speed,preset_mode,preset_modes,speed_list,percentage,oscillating,direction"
></ha-attributes>
`;
}
@@ -154,6 +155,13 @@ class MoreInfoFan extends LocalizeMixin(EventsMixin(PolymerElement)) {
}
}
computePercentageStepSize(stateObj) {
if (stateObj.attributes.percentage_step) {
return stateObj.attributes.percentage_step;
}
return 1;
}
computeClassNames(stateObj) {
return (
"more-info-fan " +

View File

@@ -380,22 +380,24 @@ export class QuickBar extends LitElement {
QuickBarNavigationItem,
"action"
>[] {
return Object.keys(this.hass.panels).map((panelKey) => {
const panel = this.hass.panels[panelKey];
const translationKey = getPanelNameTranslationKey(panel);
return Object.keys(this.hass.panels)
.filter((panelKey) => panelKey !== "_my_redirect")
.map((panelKey) => {
const panel = this.hass.panels[panelKey];
const translationKey = getPanelNameTranslationKey(panel);
const text = this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
"panel",
this.hass.localize(translationKey) || panel.title || panel.url_path
);
const text = this.hass.localize(
"ui.dialogs.quick-bar.commands.navigation.navigate_to",
"panel",
this.hass.localize(translationKey) || panel.title || panel.url_path
);
return {
text,
icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON,
path: `/${panel.url_path}`,
};
});
return {
text,
icon: getPanelIcon(panel) || DEFAULT_NAVIGATION_ICON,
path: `/${panel.url_path}`,
};
});
}
private _generateNavigationConfigSectionCommands(): Partial<

View File

@@ -48,10 +48,19 @@ const authProm = isExternal
const connProm = async (auth) => {
try {
const conn = await createConnection({ auth });
// Clear url if we have been able to establish a connection
// Clear auth data from url if we have been able to establish a connection
if (location.search.includes("auth_callback=1")) {
history.replaceState(null, "", location.pathname);
const searchParams = new URLSearchParams(location.search);
// https://github.com/home-assistant/home-assistant-js-websocket/blob/master/lib/auth.ts
// Remove all data from QueryCallbackData type
searchParams.delete("auth_callback");
searchParams.delete("code");
searchParams.delete("state");
history.replaceState(
null,
"",
`${location.pathname}?${searchParams.toString()}`
);
}
return { auth, conn };

View File

@@ -15,7 +15,8 @@ export const demoConfig: HassConfig = {
time_zone: "America/Los_Angeles",
config_dir: "/config",
version: "DEMO",
whitelist_external_dirs: [],
allowlist_external_dirs: [],
allowlist_external_urls: [],
config_source: "storage",
safe_mode: false,
state: STATE_RUNNING,

View File

@@ -1,4 +1,4 @@
<meta name='viewport' content='width=device-width, viewport-fit=cover'>
<meta name='viewport' content='width=device-width, user-scalable=no, viewport-fit=cover'>
<style>
body {
font-family: Roboto, sans-serif;

View File

@@ -70,10 +70,14 @@ class HassErrorScreen extends LitElement {
color: var(--primary-text-color);
height: calc(100% - var(--header-height));
display: flex;
padding: 16px;
align-items: center;
justify-content: center;
flex-direction: column;
}
a {
color: var(--primary-color);
}
`,
];
}

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