Compare commits

..

144 Commits

Author SHA1 Message Date
Bram Kragten
5409752817 20201212.0 (#7952)
* [ci skip] Translation update

* Add link to the community forums to find more blueprints (#7947)

* Add link to the community forums to find more blueprints

* Apply suggestions from code review

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

Co-authored-by: Paulus Schoutsen <balloob@gmail.com>

* Fix `ha-relative-time` usage for tags and sun (#7944)

* Bumped version to 20201212.0

Co-authored-by: HomeAssistant Azure <hello@home-assistant.io>
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
Co-authored-by: Philip Allgaier <mail@spacegaier.de>
2020-12-12 20:57:28 +01:00
Bram Kragten
a70e6c49a1 Merge pull request #7940 from home-assistant/dev
20201210.0
2020-12-10 16:56:08 +01:00
Bram Kragten
3d83d5f4b5 Bumped version to 20201210.0 2020-12-10 15:57:13 +01:00
Philip Allgaier
f9dece0743 Add copy YAML (automation & script) fallback without navigator.clipboard (#7900) 2020-12-10 15:55:57 +01:00
Bram Kragten
ac0871d0e8 Use correct device name in target picker (#7939) 2020-12-10 15:12:33 +01:00
HomeAssistant Azure
ffc19e591d [ci skip] Translation update 2020-12-10 00:32:24 +00:00
HomeAssistant Azure
c53380ca3d [ci skip] Translation update 2020-12-09 00:32:38 +00:00
Bram Kragten
7c74a2026a Correct tabs helpers page (#7928)
Fixes https://github.com/home-assistant/frontend/issues/7926
2020-12-08 11:47:30 +01:00
HomeAssistant Azure
adaed438d9 [ci skip] Translation update 2020-12-08 00:32:29 +00:00
uvjustin
baf38305cb Remove use of named groups in regexp (#7921) 2020-12-07 15:18:27 +01:00
HomeAssistant Azure
8254712521 [ci skip] Translation update 2020-12-07 00:33:14 +00:00
HomeAssistant Azure
53214781e3 [ci skip] Translation update 2020-12-06 00:32:39 +00:00
HomeAssistant Azure
88cbbbdf65 [ci skip] Translation update 2020-12-05 00:33:30 +00:00
Bram Kragten
c10dca9c7b Merge pull request #7910 from home-assistant/dev 2020-12-04 21:46:54 +01:00
Bram Kragten
7f2ebb4bde Bumped version to 20201204.0 2020-12-04 21:33:37 +01:00
Bram Kragten
f1abb60e4a Add message when no matches for selectors, clear config when deleted (#7906) 2020-12-04 20:49:24 +01:00
Bram Kragten
e014c7aff6 Entity is not required for button card (#7909)
Fixes https://github.com/home-assistant/frontend/issues/7908
2020-12-04 12:02:58 -06:00
Bram Kragten
b79c03433e Don't update device picker while open (#7903) 2020-12-04 12:05:01 +01:00
HomeAssistant Azure
34eb4d974d [ci skip] Translation update 2020-12-04 00:32:26 +00:00
Zack Barett
3264be3c5e Move main function to events on hui-view for custom views to use (#7861) 2020-12-03 18:16:55 -06:00
Bram Kragten
655f4f75fb Change import blueprint fab icon (#7894) 2020-12-03 22:41:13 +01:00
Bram Kragten
4383f31696 Translation logic tweaks (#7901)
* Translation logic tweaks

* Comments
2020-12-03 22:29:52 +01:00
Paulus Schoutsen
99eb15d15e Improve blueprint translations (#7892) 2020-12-03 18:08:14 +01:00
Bram Kragten
2682c6e150 Merge pull request #7891 from home-assistant/dev 2020-12-03 17:22:08 +01:00
Bram Kragten
3a5d854e6d Bumped version to 20201203.0 2020-12-03 17:00:56 +01:00
Bram Kragten
1e90c6387c More BP tweaks (#7884) 2020-12-03 09:59:43 -06:00
Philip Allgaier
2cca25f4d0 Navigate back to area overview after area deletion (#7890) 2020-12-03 16:52:44 +01:00
Bram Kragten
565724d201 Fix translation race condition? (#7885) 2020-12-03 16:52:09 +01:00
Bram Kragten
3e4955becd UI tweaks for BP (#7883)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-12-03 14:18:17 +01:00
Philip Allgaier
7b560c727f Adjust text since entities can now be in areas too (#7880) 2020-12-03 14:05:16 +01:00
HomeAssistant Azure
35abd9dfdb [ci skip] Translation update 2020-12-03 00:32:38 +00:00
Bram Kragten
b7ccf3e0e5 Merge pull request #7875 from home-assistant/dev 2020-12-02 19:51:36 +01:00
Bram Kragten
0d9ab8fdd0 Bumped version to 20201202.0 2020-12-02 19:31:43 +01:00
Bram Kragten
303f9290a8 Add more device disabled ui (#7874) 2020-12-02 19:31:06 +01:00
Bram Kragten
e0c4dc08a1 Tooltip tweak target picker (#7870) 2020-12-02 11:21:51 -06:00
Bram Kragten
8c655883fe Add device disabled reason (#7873) 2020-12-02 18:20:29 +01:00
Bram Kragten
ba90785115 Fix card picker cards (#7871) 2020-12-02 17:46:30 +01:00
Bram Kragten
7b392b626b Hardcode history card stub entity to sun.sun (#7760) 2020-12-02 10:16:46 -06:00
Zack Barett
8e4ceb7d48 Fix View Background Colors (#7823) 2020-12-02 17:08:18 +01:00
Philip Allgaier
2ab1c6e9a9 Make media player card work as square + add to gallery + icon pos tweaks (#7727) 2020-12-02 16:57:19 +01:00
Adam Ernst
dbdced0971 Improve descriptions for Configuration entries (#7606) 2020-12-02 16:36:21 +01:00
Philip Allgaier
5e481880bd Allow empty entities array with entity-filter (#7854) 2020-12-02 16:34:59 +01:00
Kendell R
faec063f34 Add --masonry-view-card-margin (#7395) 2020-12-02 16:22:20 +01:00
Zack Barett
bbea38d227 Lovelace Card Editor: Preview Background (#7678) 2020-12-02 16:19:01 +01:00
Kendell R
a0ef60de49 Make card picker border follow standards (#7162) 2020-12-02 16:14:46 +01:00
Philip Allgaier
3313572606 Improve password change card (#7756)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-12-02 16:00:49 +01:00
Philip Allgaier
c4f850cb14 Cleanup created user if person creation is cancelled (#7865) 2020-12-02 15:45:24 +01:00
Erik Montnemery
3bdab738c6 Support disabling devices (#7715)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-12-02 15:40:35 +01:00
Philip Allgaier
faaef31b9f Show proper error message if username already used (#7866) 2020-12-02 15:35:38 +01:00
Philip Allgaier
ca7b8b8b4c Add option to deactivate a user (#7757) 2020-12-02 15:33:11 +01:00
Philip Allgaier
9ca84e0694 Ensure "false" is set as default for "continue_on_timeout" (#7862) 2020-12-02 15:22:17 +01:00
Philip Allgaier
daaf2b1796 Ensure push notification description reacts to language change (#7856) 2020-12-02 12:11:47 +01:00
Bram Kragten
25f7cbea5a Add target selector (#7864)
Co-authored-by: Paulus Schoutsen <balloob@gmail.com>
2020-12-02 12:10:31 +01:00
Josh McCarty
c485ea9d7b Add number formatting to counter entity state display (#7868) 2020-12-02 11:51:56 +01:00
HomeAssistant Azure
295390c8e9 [ci skip] Translation update 2020-12-02 00:32:30 +00:00
Bram Kragten
3ebf816ce2 Fix height of ha-icon-input (#7767) 2020-12-01 22:51:15 +01:00
HomeAssistant Azure
0e362b851b [ci skip] Translation update 2020-12-01 00:32:47 +00:00
David F. Mulcahey
8d7ba19a08 Add ZHA fab for adding device when filtered by ZHA (#7848) 2020-11-30 11:05:54 +01:00
Paulus Schoutsen
08f4aa9d10 Use balloons as default pic for header. (#7850) 2020-11-30 10:59:04 +01:00
HomeAssistant Azure
98175d5c72 [ci skip] Translation update 2020-11-30 00:32:50 +00:00
Ian Richardson
7d4cad90bc Show attribute value when 0 (#7842)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-29 22:37:13 +01:00
David F. Mulcahey
335354d962 Enhance ZHA device pairing feedback (#7843)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-29 22:33:46 +01:00
Bram Kragten
fe31d15d27 Add UI for setting an area on entity level (#7837) 2020-11-29 22:00:51 +01:00
Marc Randolph
7ceb6eb50d Apply existing theme variables to unthemed items (#7844) 2020-11-29 16:13:55 +01:00
HomeAssistant Azure
4c4db46aa8 [ci skip] Translation update 2020-11-29 00:32:38 +00:00
Bram Kragten
b5724ed343 Make fab blue (#7839) 2020-11-28 17:22:42 +01:00
HomeAssistant Azure
cae94175fe [ci skip] Translation update 2020-11-28 00:32:35 +00:00
Kendell R
0494a9d410 Update skeleton height for compact header (#7827)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-27 19:23:41 +01:00
Philip Allgaier
c261b5c1ce Display HA username + flags in user config (#7705) 2020-11-27 09:41:47 +01:00
HomeAssistant Azure
c89e17ac00 [ci skip] Translation update 2020-11-27 00:32:35 +00:00
Bram Kragten
50e7410002 Merge pull request #7833 from home-assistant/dev 2020-11-27 00:16:56 +01:00
Bram Kragten
c5b0ebf76d Update ha-config-dashboard.ts 2020-11-27 00:01:34 +01:00
Bram Kragten
1d08978d6c Merge branch 'master' into dev 2020-11-26 23:59:34 +01:00
Bram Kragten
fc78b6c933 Bumped version to 20201126.0 2020-11-26 23:58:13 +01:00
Philip Allgaier
480a5718fc Remove unusable Polymer mixin from LitElement (vacuum entity row) (#7831) 2020-11-26 23:57:37 +01:00
David F. Mulcahey
f093bd115c Add network visualization to the ZHA config panel (#7802)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-26 23:56:50 +01:00
David F. Mulcahey
8a86beff14 Remove unused ZHA translations and fix ZHA fab labels (#7832) 2020-11-26 23:56:22 +01:00
Philip Allgaier
6020890384 Add state-info ellipsis + fix height (#7740) 2020-11-26 23:54:30 +01:00
Philip Allgaier
124aa947e2 Show pointer cursor for input + scene entity rows (#7830) 2020-11-26 20:32:15 +01:00
Bram Kragten
e1add14453 Add UI for new selectors (#7822) 2020-11-26 18:38:01 +01:00
Philip Allgaier
e3293837a8 Blueprints: Added missing labels & tooltips, optimize input display + text tweaks (#7748) 2020-11-26 18:14:57 +01:00
HomeAssistant Azure
5cb2743780 [ci skip] Translation update 2020-11-26 00:32:25 +00:00
Joakim Sørensen
6f0c79ec25 Introduce supervisor object (#7800)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-25 18:22:05 +01:00
Paulus Schoutsen
7de7d1d926 Fix init page message color (#7780)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-25 15:22:22 +01:00
Bram Kragten
89175f8e85 Add description and device class (#7816)
https://github.com/home-assistant/core/pull/43321
2020-11-25 15:10:08 +01:00
Paulus Schoutsen
fc48c59eb0 Clean up some types (#7801)
Co-authored-by: Zack Barett <arnett.zackary@gmail.com>
2020-11-25 12:31:51 +01:00
Paulus Schoutsen
51332bc7e7 Add frontend url (#7797) 2020-11-25 12:26:11 +01:00
Josh McCarty
7403405d12 Additional number formatting (#7763) 2020-11-25 11:37:58 +01:00
Paulus Schoutsen
1d13947e71 Use Record type (#7798) 2020-11-25 10:40:32 +01:00
HomeAssistant Azure
f6cb1ffe20 [ci skip] Translation update 2020-11-25 00:32:19 +00:00
Paulus Schoutsen
6d92b5651a Bump @web/dev-server (#7792) 2020-11-24 12:04:22 +01:00
Bram Kragten
3ea5bb2a6c Fix filtering hidden columns entity table (#7786) 2020-11-24 12:03:06 +01:00
Joakim Sørensen
1d367eca69 Add ignored job_conditions to list of unsupported reasons (#7790) 2020-11-24 11:48:19 +01:00
HomeAssistant Azure
d4bf3a2ec3 [ci skip] Translation update 2020-11-24 00:32:27 +00:00
Joakim Sørensen
0ef8881660 Add unhealthy dialog and SU restart button (#7781) 2020-11-23 17:24:26 +01:00
Joakim Sørensen
d7d1121f7d Force enable interface if configured (#7785) 2020-11-23 17:22:23 +01:00
Joakim Sørensen
7f089c309f Change order and wording snapshot actions (#7677)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-23 17:08:10 +01:00
Philip Allgaier
4dcc0bb66c Reduce "wasted" screen space (#7655) 2020-11-23 13:44:47 +01:00
Paulus Schoutsen
0049be7feb Allow developing with @web/dev-server (#7782) 2020-11-23 13:05:18 +01:00
Paulus Schoutsen
39ff641be9 Close webpack compiler on prod build (#7779)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-23 12:48:21 +01:00
Paulus Schoutsen
e2fed24995 Clean up node-vibrant (#7777) 2020-11-23 12:47:47 +01:00
Philip Allgaier
c0aa353f83 Load missing component titles for ha-related-items (#7771) 2020-11-23 10:48:32 +01:00
Ian Richardson
d8521be63d use correct condition for display of tilt controls (#7769)
Co-authored-by: Philip Allgaier <philip.allgaier@gmx.de>
2020-11-23 10:45:53 +01:00
Paulus Schoutsen
6d4569c89d Drop webpackChunkName (#7778) 2020-11-23 10:39:40 +01:00
Paulus Schoutsen
cd07553b59 Upgrade Rollup babel plugin (#7773) 2020-11-23 10:21:45 +01:00
HomeAssistant Azure
641bfcc9f7 [ci skip] Translation update 2020-11-23 00:32:47 +00:00
HomeAssistant Azure
6c01371958 [ci skip] Translation update 2020-11-22 00:33:21 +00:00
Paulus Schoutsen
7b00260b1a Remove duplicate translation key (#7766) 2020-11-21 23:33:19 +01:00
Paulus Schoutsen
875142373e Remove unused files and automate translation fragments (#7764) 2020-11-21 22:44:44 +01:00
Philip Allgaier
ba505b15ef Ensure new grid card options don't break editor (#7762) 2020-11-21 21:40:27 +01:00
Donnie
17d227b142 Stop appending 'Configuration' to the end of every config page in navigation commands. Fix spinner regression (#7753) 2020-11-21 08:31:40 -08:00
Philip Allgaier
e7e192ffe3 Align gallery code + fix icon in entity-icon demo (#7754) 2020-11-21 14:28:25 +01:00
Philip Allgaier
c53ec6e12d Ensure more consistent lovelace config errors (#7755) 2020-11-21 14:08:43 +01:00
Philip Allgaier
aad6492a6a Show error if no entity specified for history card (#7752) 2020-11-21 13:50:56 +01:00
HomeAssistant Azure
fd5b125c2d [ci skip] Translation update 2020-11-21 00:32:15 +00:00
Paulus Schoutsen
5acee76c70 Show validation errors in UI (#7725) 2020-11-20 15:37:56 +01:00
Ian Richardson
10916fa82a Convert ha-relative-time to TypeScript/LitElement (#7709)
Co-authored-by: Bram Kragten <mail@bramkragten.nl>
2020-11-20 15:17:34 +01:00
Bram Kragten
f69951a523 Fix automation editor (#7746) 2020-11-20 15:16:27 +01:00
Philip Allgaier
38ba85e89d Make gallery config visible in dark mode + fix config alignment (#7741) 2020-11-20 15:13:19 +01:00
Patrick Decat
97023921b8 Add missing "gate" cover device_class (#7744) 2020-11-20 15:10:25 +01:00
Bram Kragten
f835810f0a Add entity and device selectors (#7735) 2020-11-20 13:26:03 +01:00
HomeAssistant Azure
46f5589530 [ci skip] Translation update 2020-11-20 00:32:27 +00:00
Thomas Lovén
ff9840c8ef Use mwc-tabs in conditional card editor (#7716) 2020-11-19 23:06:06 +01:00
Philip Allgaier
0c197558a1 Close notification drawer when last entry is dismissed (#7724) 2020-11-19 22:53:21 +01:00
Joakim Sørensen
c409ba149d Supervisor network changes (#7676) 2020-11-19 22:51:29 +01:00
Joakim Sørensen
0b896ddfb1 Check snapshot size before upload (#7733) 2020-11-19 22:49:29 +01:00
Philip Allgaier
45721eb4fe Adjust FAB for blueprint overview + align translations (#7730) 2020-11-19 22:48:34 +01:00
Bram Kragten
1289bd03b2 Remove conference banner (#7731) 2020-11-19 10:46:52 +01:00
HomeAssistant Azure
c1ba8ba6b8 [ci skip] Translation update 2020-11-19 00:32:32 +00:00
Bram Kragten
4973d8f629 WIP initial Blueprint UI (#7695)
* WIP initial Blueprint UI

* Review comments

* localize
2020-11-18 12:19:59 +01:00
Philip Allgaier
3aff4c96c4 Add Georgian language (Kartuli) (#7721) 2020-11-18 10:39:35 +01:00
Paulus Schoutsen
4005bc8985 Clarify title 2020-11-18 08:46:42 +00:00
Paulus Schoutsen
62e9792c39 Add note about building the frontend 2020-11-18 08:44:45 +00:00
HomeAssistant Azure
33183cc595 [ci skip] Translation update 2020-11-18 00:32:28 +00:00
Josh McCarty
394d552856 Use ha-dialog for dialog-system-log-details, add close icon button (#7513) 2020-11-17 22:29:20 +01:00
Bram Kragten
aa4f0929e0 Bumped version to 20201111.2 2020-11-17 22:24:30 +01:00
Thomas Lovén
f99b9215e3 Fix date picker row alignment (#7704) 2020-11-17 22:23:21 +01:00
Paulus Schoutsen
c51d621fee Allow dismissing (#7712) 2020-11-17 22:22:39 +01:00
Paulus Schoutsen
7499892bc2 Don't support safari 10 in minifaction for latest support (#7719) 2020-11-17 22:00:28 +01:00
Bram Kragten
3fb35871c7 Bumped version to 20201111.1 2020-11-16 22:02:28 +01:00
Donnie
d6d20cd704 Quick Bar should only capitalize letters in Command Palette (#7671) 2020-11-16 22:02:02 +01:00
Bram Kragten
9cc6a6b885 Fix number format in state-card-display (#7703) 2020-11-16 22:01:48 +01:00
Philip Allgaier
ee0be7b6d0 Add missing 1px to prevent slider pin cutoff (#7657) 2020-11-16 22:01:32 +01:00
Bram Kragten
7e6153ba7d Merge pull request #7654 from home-assistant/dev 2020-11-11 15:40:04 +01:00
414 changed files with 15530 additions and 4701 deletions

View File

@@ -1,6 +0,0 @@
jshint:
enabled: false
eslint:
enabled: true
config_file: .eslintrc-hound.json

39
build-scripts/README.md Normal file
View File

@@ -0,0 +1,39 @@
# Bundling Home Assistant Frontend
The Home Assistant build pipeline contains various steps to prepare a build.
- Generating icon files to be included
- Generating translation files to be included
- Converting TypeScript, CSS and JSON files to JavaScript
- Bundling
- Minifying the files
- Generating the HTML entrypoint files
- Generating the service worker
- Compressing the files
## Converting files
Currently in Home Assistant we use a bundler to convert TypeScript, CSS and JSON files to JavaScript files that the browser understands.
We currently rely on Webpack but also have experimental Rollup support. Both of these programs bundle the converted files in both production and development.
For development, bundling is optional. We just want to get the right files in the browser.
Responsibilities of the converter during development:
- Convert TypeScript to JavaScript
- Convert CSS to JavaScript that sets the content as the default export
- Convert JSON to JavaScript that sets the content as the default export
- Make sure import, dynamic import and web worker references work
- Add extensions where missing
- Resolve absolute package imports
- Filter out specific imports/packages
- Replace constants with values
In production, the following responsibilities are added:
- Minify HTML
- Bundle multiple imports so that the browser can fetch less files
- Generate a second version that is ES5 compatible
Configuration for all these steps are specified in [bundle.js](bundle.js).

View File

@@ -44,7 +44,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({
}); });
module.exports.terserOptions = (latestBuild) => ({ module.exports.terserOptions = (latestBuild) => ({
safari10: true, safari10: !latestBuild,
ecma: latestBuild ? undefined : 5, ecma: latestBuild ? undefined : 5,
output: { comments: false }, output: { comments: false },
}); });
@@ -117,7 +117,7 @@ BundleConfig {
*/ */
module.exports.config = { module.exports.config = {
app({ isProdBuild, latestBuild, isStatsBuild }) { app({ isProdBuild, latestBuild, isStatsBuild, isWDS }) {
return { return {
entry: { entry: {
service_worker: "./src/entrypoints/service_worker.ts", service_worker: "./src/entrypoints/service_worker.ts",
@@ -132,6 +132,7 @@ module.exports.config = {
isProdBuild, isProdBuild,
latestBuild, latestBuild,
isStatsBuild, isStatsBuild,
isWDS,
}; };
}, },

View File

@@ -6,6 +6,9 @@ module.exports = {
useRollup() { useRollup() {
return process.env.ROLLUP === "1"; return process.env.ROLLUP === "1";
}, },
useWDS() {
return process.env.WDS === "1";
},
isProdBuild() { isProdBuild() {
return ( return (
process.env.NODE_ENV === "production" || module.exports.isStatsBuild() process.env.NODE_ENV === "production" || module.exports.isStatsBuild()

View File

@@ -12,6 +12,7 @@ require("./webpack.js");
require("./service-worker.js"); require("./service-worker.js");
require("./entry-html.js"); require("./entry-html.js");
require("./rollup.js"); require("./rollup.js");
require("./wds.js");
gulp.task( gulp.task(
"develop-app", "develop-app",
@@ -28,7 +29,11 @@ gulp.task(
"build-translations" "build-translations"
), ),
"copy-static-app", "copy-static-app",
env.useRollup() ? "rollup-watch-app" : "webpack-watch-app" env.useWDS()
? "wds-watch-app"
: env.useRollup()
? "rollup-watch-app"
: "webpack-watch-app"
) )
); );

View File

@@ -19,6 +19,7 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => {
return compiled({ return compiled({
...data, ...data,
useRollup: env.useRollup(), useRollup: env.useRollup(),
useWDS: env.useWDS(),
renderTemplate, renderTemplate,
}); });
}; };
@@ -90,10 +91,23 @@ gulp.task("gen-pages-prod", (done) => {
}); });
gulp.task("gen-index-app-dev", (done) => { gulp.task("gen-index-app-dev", (done) => {
let latestAppJS, latestCoreJS, latestCustomPanelJS;
if (env.useWDS()) {
latestAppJS = "http://localhost:8000/src/entrypoints/app.ts";
latestCoreJS = "http://localhost:8000/src/entrypoints/core.ts";
latestCustomPanelJS =
"http://localhost:8000/src/entrypoints/custom-panel.ts";
} else {
latestAppJS = "/frontend_latest/app.js";
latestCoreJS = "/frontend_latest/core.js";
latestCustomPanelJS = "/frontend_latest/custom-panel.js";
}
const content = renderTemplate("index", { const content = renderTemplate("index", {
latestAppJS: "/frontend_latest/app.js", latestAppJS,
latestCoreJS: "/frontend_latest/core.js", latestCoreJS,
latestCustomPanelJS: "/frontend_latest/custom-panel.js", latestCustomPanelJS,
es5AppJS: "/frontend_es5/app.js", es5AppJS: "/frontend_es5/app.js",
es5CoreJS: "/frontend_es5/core.js", es5CoreJS: "/frontend_es5/core.js",

View File

@@ -33,21 +33,10 @@ String.prototype.rsplit = function (sep, maxsplit) {
: split; : split;
}; };
// Panel translations which should be split from the core translations. These // Panel translations which should be split from the core translations.
// should mirror the fragment definitions in polymer.json, so that we load const TRANSLATION_FRAGMENTS = Object.keys(
// additional resources at equivalent points. require("../../src/translations/en.json").ui.panel
const TRANSLATION_FRAGMENTS = [ );
"config",
"history",
"logbook",
"mailbox",
"profile",
"shopping-list",
"page-authorize",
"page-demo",
"page-onboarding",
"developer-tools",
];
function recursiveFlatten(prefix, data) { function recursiveFlatten(prefix, data) {
let output = {}; let output = {};

11
build-scripts/gulp/wds.js Normal file
View File

@@ -0,0 +1,11 @@
// Tasks to run Rollup
const gulp = require("gulp");
const { startDevServer } = require("@web/dev-server");
gulp.task("wds-watch-app", () => {
startDevServer({
config: {
watch: true,
},
});
});

View File

@@ -47,7 +47,7 @@ const runDevServer = ({
); );
}); });
const handler = (done) => (err, stats) => { const doneHandler = (done) => (err, stats) => {
if (err) { if (err) {
log.error(err.stack || err); log.error(err.stack || err);
if (err.details) { if (err.details) {
@@ -67,11 +67,20 @@ const handler = (done) => (err, stats) => {
} }
}; };
const prodBuild = (conf) =>
new Promise((resolve) => {
webpack(
conf,
// Resolve promise when done. Because we pass a callback, webpack closes itself
doneHandler(resolve)
);
});
gulp.task("webpack-watch-app", () => { gulp.task("webpack-watch-app", () => {
// we are not calling done, so this command will run forever // This command will run forever because we don't close compiler
webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch( webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch(
{ ignored: /build-translations/ }, { ignored: /build-translations/ },
handler() doneHandler()
); );
gulp.watch( gulp.watch(
path.join(paths.translations_src, "en.json"), path.join(paths.translations_src, "en.json"),
@@ -79,15 +88,12 @@ gulp.task("webpack-watch-app", () => {
); );
}); });
gulp.task( gulp.task("webpack-prod-app", () =>
"webpack-prod-app", prodBuild(
() => bothBuilds(createAppConfig, {
new Promise((resolve) => isProdBuild: true,
webpack( })
bothBuilds(createAppConfig, { isProdBuild: true }), )
handler(resolve)
)
)
); );
gulp.task("webpack-dev-server-demo", () => { gulp.task("webpack-dev-server-demo", () => {
@@ -98,17 +104,12 @@ gulp.task("webpack-dev-server-demo", () => {
}); });
}); });
gulp.task( gulp.task("webpack-prod-demo", () =>
"webpack-prod-demo", prodBuild(
() => bothBuilds(createDemoConfig, {
new Promise((resolve) => isProdBuild: true,
webpack( })
bothBuilds(createDemoConfig, { )
isProdBuild: true,
}),
handler(resolve)
)
)
); );
gulp.task("webpack-dev-server-cast", () => { gulp.task("webpack-dev-server-cast", () => {
@@ -121,41 +122,30 @@ gulp.task("webpack-dev-server-cast", () => {
}); });
}); });
gulp.task( gulp.task("webpack-prod-cast", () =>
"webpack-prod-cast", prodBuild(
() => bothBuilds(createCastConfig, {
new Promise((resolve) => isProdBuild: true,
webpack( })
bothBuilds(createCastConfig, { )
isProdBuild: true,
}),
handler(resolve)
)
)
); );
gulp.task("webpack-watch-hassio", () => { gulp.task("webpack-watch-hassio", () => {
// we are not calling done, so this command will run forever // This command will run forever because we don't close compiler
webpack( webpack(
createHassioConfig({ createHassioConfig({
isProdBuild: false, isProdBuild: false,
latestBuild: true, latestBuild: true,
}) })
).watch({}, handler()); ).watch({}, doneHandler());
}); });
gulp.task( gulp.task("webpack-prod-hassio", () =>
"webpack-prod-hassio", prodBuild(
() => bothBuilds(createHassioConfig, {
new Promise((resolve) => isProdBuild: true,
webpack( })
bothBuilds(createHassioConfig, { )
isProdBuild: true,
}),
handler(resolve)
)
)
); );
gulp.task("webpack-dev-server-gallery", () => { gulp.task("webpack-dev-server-gallery", () => {
@@ -167,17 +157,11 @@ gulp.task("webpack-dev-server-gallery", () => {
}); });
}); });
gulp.task( gulp.task("webpack-prod-gallery", () =>
"webpack-prod-gallery", prodBuild(
() => createGalleryConfig({
new Promise((resolve) => isProdBuild: true,
webpack( latestBuild: true,
createGalleryConfig({ })
isProdBuild: true, )
latestBuild: true,
}),
handler(resolve)
)
)
); );

View File

@@ -1,5 +1,3 @@
const path = require("path");
module.exports = function (userOptions = {}) { module.exports = function (userOptions = {}) {
// Files need to be absolute paths. // Files need to be absolute paths.
// This only works if the file has no exports // This only works if the file has no exports

View File

@@ -3,7 +3,7 @@ const path = require("path");
const commonjs = require("@rollup/plugin-commonjs"); const commonjs = require("@rollup/plugin-commonjs");
const resolve = require("@rollup/plugin-node-resolve"); const resolve = require("@rollup/plugin-node-resolve");
const json = require("@rollup/plugin-json"); const json = require("@rollup/plugin-json");
const babel = require("rollup-plugin-babel"); const babel = require("@rollup/plugin-babel").babel;
const replace = require("@rollup/plugin-replace"); const replace = require("@rollup/plugin-replace");
const visualizer = require("rollup-plugin-visualizer"); const visualizer = require("rollup-plugin-visualizer");
const { string } = require("rollup-plugin-string"); const { string } = require("rollup-plugin-string");
@@ -31,6 +31,7 @@ const createRollupConfig = ({
isStatsBuild, isStatsBuild,
publicPath, publicPath,
dontHash, dontHash,
isWDS,
}) => { }) => {
return { return {
/** /**
@@ -61,6 +62,7 @@ const createRollupConfig = ({
...bundle.babelOptions({ latestBuild }), ...bundle.babelOptions({ latestBuild }),
extensions, extensions,
exclude: bundle.babelExclude(), exclude: bundle.babelExclude(),
babelHelpers: isWDS ? "inline" : "bundled",
}), }),
string({ string({
// Import certain extensions as strings // Import certain extensions as strings
@@ -69,19 +71,21 @@ const createRollupConfig = ({
replace( replace(
bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) bundle.definedVars({ isProdBuild, latestBuild, defineOverlay })
), ),
manifest({ !isWDS &&
publicPath, manifest({
}), publicPath,
worker(), }),
dontHashPlugin({ dontHash }), !isWDS && worker(),
isProdBuild && terser(bundle.terserOptions(latestBuild)), !isWDS && dontHashPlugin({ dontHash }),
isStatsBuild && !isWDS && isProdBuild && terser(bundle.terserOptions(latestBuild)),
!isWDS &&
isStatsBuild &&
visualizer({ visualizer({
// https://github.com/btd/rollup-plugin-visualizer#options // https://github.com/btd/rollup-plugin-visualizer#options
open: true, open: true,
sourcemap: true, sourcemap: true,
}), }),
], ].filter(Boolean),
}, },
/** /**
* @type { import("rollup").OutputOptions } * @type { import("rollup").OutputOptions }
@@ -108,12 +112,13 @@ const createRollupConfig = ({
}; };
}; };
const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => { const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild, isWDS }) => {
return createRollupConfig( return createRollupConfig(
bundle.config.app({ bundle.config.app({
isProdBuild, isProdBuild,
latestBuild, latestBuild,
isStatsBuild, isStatsBuild,
isWDS,
}) })
); );
}; };

View File

@@ -3,22 +3,10 @@ import { Lovelace } from "../../../src/panels/lovelace/types";
import { DemoConfig } from "./types"; import { DemoConfig } from "./types";
export const demoConfigs: Array<() => Promise<DemoConfig>> = [ export const demoConfigs: Array<() => Promise<DemoConfig>> = [
() => () => import("./arsaboo").then((mod) => mod.demoArsaboo),
import(/* webpackChunkName: "arsaboo" */ "./arsaboo").then( () => import("./teachingbirds").then((mod) => mod.demoTeachingbirds),
(mod) => mod.demoArsaboo () => import("./kernehed").then((mod) => mod.demoKernehed),
), () => import("./jimpower").then((mod) => mod.demoJimpower),
() =>
import(/* webpackChunkName: "teachingbirds" */ "./teachingbirds").then(
(mod) => mod.demoTeachingbirds
),
() =>
import(/* webpackChunkName: "kernehed" */ "./kernehed").then(
(mod) => mod.demoKernehed
),
() =>
import(/* webpackChunkName: "jimpower" */ "./jimpower").then(
(mod) => mod.demoJimpower
),
]; ];
// eslint-disable-next-line import/no-mutable-exports // eslint-disable-next-line import/no-mutable-exports

View File

@@ -9,5 +9,5 @@ export interface DemoConfig {
authorUrl: string; authorUrl: string;
lovelace: (localize: LocalizeFunc) => LovelaceConfig; lovelace: (localize: LocalizeFunc) => LovelaceConfig;
entities: (localize: LocalizeFunc) => Entity[]; entities: (localize: LocalizeFunc) => Entity[];
theme: () => { [key: string]: string } | null; theme: () => Record<string, string> | null;
} }

View File

@@ -7,7 +7,5 @@ import "./ha-demo";
/* polyfill for paper-dropdown */ /* polyfill for paper-dropdown */
setTimeout(() => { setTimeout(() => {
import( import("web-animations-js/web-animations-next-lite.min");
/* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min"
);
}, 1000); }, 1000);

View File

@@ -21,15 +21,16 @@ class DemoCard extends PolymerElement {
} }
pre { pre {
width: 400px; width: 400px;
margin: 16px; margin: 0 16px;
overflow: auto; overflow: auto;
color: var(--primary-text-color);
} }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {
.root { .root {
flex-direction: column; flex-direction: column;
} }
pre { pre {
margin-left: 0; margin: 16px 0;
} }
} }
</style> </style>

View File

@@ -26,8 +26,9 @@ class DemoMoreInfo extends PolymerElement {
pre { pre {
width: 400px; width: 400px;
margin: 16px; margin: 0 16px;
overflow: auto; overflow: auto;
color: var(--primary-text-color);
} }
@media only screen and (max-width: 800px) { @media only screen and (max-width: 800px) {
@@ -35,7 +36,7 @@ class DemoMoreInfo extends PolymerElement {
flex-direction: column; flex-direction: column;
} }
pre { pre {
margin-left: 0; margin: 16px 0;
} }
} }
</style> </style>

View File

@@ -7,8 +7,8 @@ export const createMediaPlayerEntities = () => [
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead", media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set + Browse Media // Select Source + Stop + Clear + Play + Shuffle Set
supported_features: 195135, supported_features: 64063,
entity_picture: "/images/album_cover_2.jpg", entity_picture: "/images/album_cover_2.jpg",
media_duration: 300, media_duration: 300,
media_position: 50, media_position: 50,
@@ -24,8 +24,8 @@ export const createMediaPlayerEntities = () => [
media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)",
media_artist: "Technohead", media_artist: "Technohead",
// Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media +
// Select Source + Stop + Clear + Play + Shuffle Set // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media
supported_features: 64063, supported_features: 195135,
entity_picture: "/images/album_cover.jpg", entity_picture: "/images/album_cover.jpg",
media_duration: 300, media_duration: 300,
media_position: 0, media_position: 0,

View File

@@ -73,13 +73,7 @@ const CONFIGS = [
class DemoAlarmPanelEntity extends PolymerElement { class DemoAlarmPanelEntity extends PolymerElement {
static get template() { static get template() {
return html` return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
} }
static get properties() { static get properties() {
@@ -88,7 +82,6 @@ class DemoAlarmPanelEntity extends PolymerElement {
type: Object, type: Object,
value: CONFIGS, value: CONFIGS,
}, },
hass: Object,
}; };
} }

View File

@@ -55,13 +55,7 @@ const CONFIGS = [
class DemoConditional extends PolymerElement { class DemoConditional extends PolymerElement {
static get template() { static get template() {
return html` return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
} }
static get properties() { static get properties() {
@@ -70,7 +64,6 @@ class DemoConditional extends PolymerElement {
type: Object, type: Object,
value: CONFIGS, value: CONFIGS,
}, },
hass: Object,
}; };
} }

View File

@@ -20,10 +20,10 @@ const CONFIGS = [
`, `,
}, },
{ {
heading: "With Name", heading: "With Name (defined in card)",
config: ` config: `
- type: button - type: button
name: Bedroom name: Custom Name
entity: light.bed_light entity: light.bed_light
`, `,
}, },
@@ -32,7 +32,7 @@ const CONFIGS = [
config: ` config: `
- type: button - type: button
entity: light.bed_light entity: light.bed_light
icon: mdi:hotel icon: mdi:tools
`, `,
}, },
{ {
@@ -71,13 +71,7 @@ const CONFIGS = [
class DemoButtonEntity extends PolymerElement { class DemoButtonEntity extends PolymerElement {
static get template() { static get template() {
return html` return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
} }
static get properties() { static get properties() {
@@ -86,7 +80,6 @@ class DemoButtonEntity extends PolymerElement {
type: Object, type: Object,
value: CONFIGS, value: CONFIGS,
}, },
hass: Object,
}; };
} }

View File

@@ -163,13 +163,7 @@ const CONFIGS = [
class DemoMap extends PolymerElement { class DemoMap extends PolymerElement {
static get template() { static get template() {
return html` return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
} }
static get properties() { static get properties() {
@@ -178,7 +172,6 @@ class DemoMap extends PolymerElement {
type: Object, type: Object,
value: CONFIGS, value: CONFIGS,
}, },
hass: Object,
}; };
} }

View File

@@ -146,17 +146,21 @@ const CONFIGS = [
entity: media_player.receiver_off entity: media_player.receiver_off
`, `,
}, },
{
heading: "Grid Full Size",
config: `
- type: grid
columns: 1
cards:
- type: media-control
entity: media_player.music_paused
`,
},
]; ];
class DemoHuiMediControlCard extends PolymerElement { class DemoHuiMediControlCard extends PolymerElement {
static get template() { static get template() {
return html` return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
} }
static get properties() { static get properties() {
@@ -165,7 +169,6 @@ class DemoHuiMediControlCard extends PolymerElement {
type: Object, type: Object,
value: CONFIGS, value: CONFIGS,
}, },
hass: Object,
}; };
} }

View File

@@ -57,13 +57,7 @@ const CONFIGS = [
class DemoHuiMediaPlayerRows extends PolymerElement { class DemoHuiMediaPlayerRows extends PolymerElement {
static get template() { static get template() {
return html` return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `;
<demo-cards
id="demos"
hass="[[hass]]"
configs="[[_configs]]"
></demo-cards>
`;
} }
static get properties() { static get properties() {
@@ -72,7 +66,6 @@ class DemoHuiMediaPlayerRows extends PolymerElement {
type: Object, type: Object,
value: CONFIGS, value: CONFIGS,
}, },
hass: Object,
}; };
} }

View File

@@ -20,48 +20,47 @@ class HaGallery extends PolymerElement {
static get template() { static get template() {
return html` return html`
<style include="iron-positioning ha-style"> <style include="iron-positioning ha-style">
:host { :host {
-ms-user-select: initial; -ms-user-select: initial;
-webkit-user-select: initial; -webkit-user-select: initial;
-moz-user-select: initial; -moz-user-select: initial;
} }
app-header-layout { app-header-layout {
min-height: 100vh; min-height: 100vh;
} }
ha-icon-button.invisible { ha-icon-button.invisible {
visibility: hidden; visibility: hidden;
} }
.pickers { .pickers {
display: flex; display: flex;
flex-wrap: wrap; flex-wrap: wrap;
justify-content: center; justify-content: center;
align-items: start; align-items: start;
} }
.pickers ha-card { .pickers ha-card {
width: 400px; width: 400px;
display: block; display: block;
margin: 16px 8px; margin: 16px 8px;
} }
.pickers ha-card:last-child { .pickers ha-card:last-child {
margin-bottom: 16px; margin-bottom: 16px;
} }
.intro { .intro {
margin: -1em 0; margin: -1em 0;
} }
p a { p a {
color: var(--primary-color); color: var(--primary-color);
} }
a {
color: var(--primary-text-color);
text-decoration: none;
}
a {
color: var(--primary-text-color);
text-decoration: none;
}
</style> </style>
<app-header-layout> <app-header-layout>
@@ -70,32 +69,42 @@ class HaGallery extends PolymerElement {
<ha-icon-button <ha-icon-button
icon="hass:arrow-left" icon="hass:arrow-left"
on-click="_backTapped" on-click="_backTapped"
class$='[[_computeHeaderButtonClass(_demo)]]' class$="[[_computeHeaderButtonClass(_demo)]]"
></ha-icon-button> ></ha-icon-button>
<div main-title>[[_withDefault(_demo, "Home Assistant Gallery")]]</div> <div main-title>
[[_withDefault(_demo, "Home Assistant Gallery")]]
</div>
</app-toolbar> </app-toolbar>
</app-header> </app-header>
<div class='content'> <div class="content">
<div id='demo'></div> <div id="demo"></div>
<template is='dom-if' if='[[!_demo]]'> <template is="dom-if" if="[[!_demo]]">
<div class='pickers'> <div class="pickers">
<ha-card header="Lovelace card demos"> <ha-card header="Lovelace Card Demos">
<div class='card-content intro'> <div class="card-content intro">
<p> <p>
Lovelace has many different cards. Each card allows the user to tell a different story about what is going on in their house. These cards are very customizable, as no household is the same. Lovelace has many different cards. Each card allows the user
to tell a different story about what is going on in their
house. These cards are very customizable, as no household is
the same.
</p> </p>
<p> <p>
This gallery helps our developers and designers to see all the different states that each card can be in. This gallery helps our developers and designers to see all
the different states that each card can be in.
</p> </p>
<p> <p>
Check <a href='https://www.home-assistant.io/lovelace'>the official website</a> for instructions on how to get started with Lovelace.</a>. Check
<a href="https://www.home-assistant.io/lovelace"
>the official website</a
>
for instructions on how to get started with Lovelace.
</p> </p>
</div> </div>
<template is='dom-repeat' items='[[_lovelaceDemos]]'> <template is="dom-repeat" items="[[_lovelaceDemos]]">
<a href='#[[item]]'> <a href="#[[item]]">
<paper-item> <paper-item>
<paper-item-body>{{ item }}</paper-item-body> <paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon> <ha-icon icon="hass:chevron-right"></ha-icon>
@@ -104,14 +113,14 @@ class HaGallery extends PolymerElement {
</template> </template>
</ha-card> </ha-card>
<ha-card header="More Info demos"> <ha-card header="More Info Demos">
<div class='card-content intro'> <div class="card-content intro">
<p> <p>
More info screens show up when an entity is clicked. More info screens show up when an entity is clicked.
</p> </p>
</div> </div>
<template is='dom-repeat' items='[[_moreInfoDemos]]'> <template is="dom-repeat" items="[[_moreInfoDemos]]">
<a href='#[[item]]'> <a href="#[[item]]">
<paper-item> <paper-item>
<paper-item-body>{{ item }}</paper-item-body> <paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon> <ha-icon icon="hass:chevron-right"></ha-icon>
@@ -120,14 +129,14 @@ class HaGallery extends PolymerElement {
</template> </template>
</ha-card> </ha-card>
<ha-card header="Util demos"> <ha-card header="Util Demos">
<div class='card-content intro'> <div class="card-content intro">
<p> <p>
Test pages for our utility functions. Test pages for our utility functions.
</p> </p>
</div> </div>
<template is='dom-repeat' items='[[_utilDemos]]'> <template is="dom-repeat" items="[[_utilDemos]]">
<a href='#[[item]]'> <a href="#[[item]]">
<paper-item> <paper-item>
<paper-item-body>{{ item }}</paper-item-body> <paper-item-body>{{ item }}</paper-item-body>
<ha-icon icon="hass:chevron-right"></ha-icon> <ha-icon icon="hass:chevron-right"></ha-icon>
@@ -139,7 +148,10 @@ class HaGallery extends PolymerElement {
</template> </template>
</div> </div>
</app-header-layout> </app-header-layout>
<notification-manager hass=[[_fakeHass]] id='notifications'></notification-manager> <notification-manager
hass="[[_fakeHass]]"
id="notifications"
></notification-manager>
`; `;
} }

View File

@@ -27,6 +27,8 @@ declare global {
} }
} }
const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB
@customElement("hassio-upload-snapshot") @customElement("hassio-upload-snapshot")
export class HassioUploadSnapshot extends LitElement { export class HassioUploadSnapshot extends LitElement {
public hass!: HomeAssistant; public hass!: HomeAssistant;
@@ -51,6 +53,20 @@ export class HassioUploadSnapshot extends LitElement {
private async _uploadFile(ev) { private async _uploadFile(ev) {
const file = ev.detail.files[0]; const file = ev.detail.files[0];
if (file.size > MAX_FILE_SIZE) {
showAlertDialog(this, {
title: "Snapshot file is too big",
text: html`The maximum allowed filesize is 1GB.<br />
<a
href="https://www.home-assistant.io/hassio/haos_common_tasks/#restoring-a-snapshot-on-a-new-install"
target="_blank"
>Have a look here on how to restore it.</a
>`,
confirmText: "ok",
});
return;
}
if (!["application/x-tar"].includes(file.type)) { if (!["application/x-tar"].includes(file.type)) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Unsupported file format", title: "Unsupported file format",

View File

@@ -12,7 +12,7 @@ import { atLeastVersion } from "../../../src/common/config/version";
import { navigate } from "../../../src/common/navigate"; import { navigate } from "../../../src/common/navigate";
import { compare } from "../../../src/common/string/compare"; import { compare } from "../../../src/common/string/compare";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
import { HassioAddonInfo } from "../../../src/data/hassio/addon"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import "../components/hassio-card-content"; import "../components/hassio-card-content";
@@ -22,14 +22,14 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioAddons extends LitElement { class HassioAddons extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public addons?: HassioAddonInfo[]; @property({ attribute: false }) public supervisor!: Supervisor;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="content"> <div class="content">
<h1>Add-ons</h1> <h1>Add-ons</h1>
<div class="card-group"> <div class="card-group">
${!this.addons?.length ${!this.supervisor.supervisor.addons?.length
? html` ? html`
<ha-card> <ha-card>
<div class="card-content"> <div class="card-content">
@@ -41,7 +41,7 @@ class HassioAddons extends LitElement {
</div> </div>
</ha-card> </ha-card>
` `
: this.addons : this.supervisor.supervisor.addons
.sort((a, b) => compare(a.name, b.name)) .sort((a, b) => compare(a.name, b.name))
.map( .map(
(addon) => html` (addon) => html`

View File

@@ -7,11 +7,7 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { HassioHassOSInfo } from "../../../src/data/hassio/host"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
@@ -23,16 +19,12 @@ import "./hassio-update";
class HassioDashboard extends LitElement { class HassioDashboard extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
@@ -47,13 +39,11 @@ class HassioDashboard extends LitElement {
<div class="content"> <div class="content">
<hassio-update <hassio-update
.hass=${this.hass} .hass=${this.hass}
.hassInfo=${this.hassInfo} .supervisor=${this.supervisor}
.supervisorInfo=${this.supervisorInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-update> ></hassio-update>
<hassio-addons <hassio-addons
.hass=${this.hass} .hass=${this.hass}
.addons=${this.supervisorInfo.addons} .supervisor=${this.supervisor}
></hassio-addons> ></hassio-addons>
</div> </div>
</hass-tabs-subpage> </hass-tabs-subpage>

View File

@@ -23,6 +23,7 @@ import {
HassioHomeAssistantInfo, HassioHomeAssistantInfo,
HassioSupervisorInfo, HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -35,31 +36,20 @@ import { hassioStyle } from "../resources/hassio-style";
export class HassioUpdate extends LitElement { export class HassioUpdate extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo; @property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo; private _pendingUpdates = memoizeOne((supervisor: Supervisor): number => {
return Object.keys(supervisor).filter(
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo; (value) => supervisor[value].update_available
).length;
private _pendingUpdates = memoizeOne( });
(
core?: HassioHomeAssistantInfo,
supervisor?: HassioSupervisorInfo,
os?: HassioHassOSInfo
): number => {
return [core, supervisor, os].filter(
(value) => !!value && value?.update_available
).length;
}
);
protected render(): TemplateResult { protected render(): TemplateResult {
const updatesAvailable = this._pendingUpdates( if (!this.supervisor) {
this.hassInfo, return html``;
this.supervisorInfo, }
this.hassOsInfo
);
const updatesAvailable = this._pendingUpdates(this.supervisor);
if (!updatesAvailable) { if (!updatesAvailable) {
return html``; return html``;
} }
@@ -74,26 +64,24 @@ export class HassioUpdate extends LitElement {
<div class="card-group"> <div class="card-group">
${this._renderUpdateCard( ${this._renderUpdateCard(
"Home Assistant Core", "Home Assistant Core",
this.hassInfo!, this.supervisor.core,
"hassio/homeassistant/update", "hassio/homeassistant/update",
`https://${ `https://${
this.hassInfo?.version_latest.includes("b") ? "rc" : "www" this.supervisor.core.version_latest.includes("b") ? "rc" : "www"
}.home-assistant.io/latest-release-notes/` }.home-assistant.io/latest-release-notes/`
)} )}
${this._renderUpdateCard( ${this._renderUpdateCard(
"Supervisor", "Supervisor",
this.supervisorInfo!, this.supervisor.supervisor,
"hassio/supervisor/update", "hassio/supervisor/update",
`https://github.com//home-assistant/hassio/releases/tag/${ `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}`
this.supervisorInfo!.version_latest
}`
)} )}
${this.hassOsInfo ${this.supervisor.host.features.includes("hassos")
? this._renderUpdateCard( ? this._renderUpdateCard(
"Operating System", "Operating System",
this.hassOsInfo, this.supervisor.os,
"hassio/os/update", "hassio/os/update",
`https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}` `https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}`
) )
: ""} : ""}
</div> </div>

View File

@@ -11,10 +11,7 @@ export const showHassioMarkdownDialog = (
): void => { ): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-markdown", dialogTag: "dialog-hassio-markdown",
dialogImport: () => dialogImport: () => import("./dialog-hassio-markdown"),
import(
/* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown"
),
dialogParams, dialogParams,
}); });
}; };

View File

@@ -1,5 +1,7 @@
import "@material/mwc-button/mwc-button"; import "@material/mwc-button/mwc-button";
import "@material/mwc-icon-button"; import "@material/mwc-icon-button";
import "@material/mwc-list/mwc-list";
import "@material/mwc-list/mwc-list-item";
import "@material/mwc-tab"; import "@material/mwc-tab";
import "@material/mwc-tab-bar"; import "@material/mwc-tab-bar";
import { mdiClose } from "@mdi/js"; import { mdiClose } from "@mdi/js";
@@ -16,18 +18,22 @@ import {
} from "lit-element"; } from "lit-element";
import { cache } from "lit-html/directives/cache"; import { cache } from "lit-html/directives/cache";
import { fireEvent } from "../../../../src/common/dom/fire_event"; import { fireEvent } from "../../../../src/common/dom/fire_event";
import "../../../../src/components/ha-chips";
import "../../../../src/components/ha-circular-progress"; import "../../../../src/components/ha-circular-progress";
import "../../../../src/components/ha-dialog"; import "../../../../src/components/ha-dialog";
import "../../../../src/components/ha-expansion-panel";
import "../../../../src/components/ha-formfield"; import "../../../../src/components/ha-formfield";
import "../../../../src/components/ha-header-bar"; import "../../../../src/components/ha-header-bar";
import "../../../../src/components/ha-radio"; import "../../../../src/components/ha-radio";
import type { HaRadio } from "../../../../src/components/ha-radio";
import "../../../../src/components/ha-related-items"; import "../../../../src/components/ha-related-items";
import "../../../../src/components/ha-svg-icon"; import "../../../../src/components/ha-svg-icon";
import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
import { import {
AccessPoints,
accesspointScan,
NetworkInterface, NetworkInterface,
updateNetworkInterface, updateNetworkInterface,
WifiConfiguration,
} from "../../../../src/data/hassio/network"; } from "../../../../src/data/hassio/network";
import { import {
showAlertDialog, showAlertDialog,
@@ -38,54 +44,51 @@ import { haStyleDialog } from "../../../../src/resources/styles";
import type { HomeAssistant } from "../../../../src/types"; import type { HomeAssistant } from "../../../../src/types";
import { HassioNetworkDialogParams } from "./show-dialog-network"; import { HassioNetworkDialogParams } from "./show-dialog-network";
const IP_VERSIONS = ["ipv4", "ipv6"];
@customElement("dialog-hassio-network") @customElement("dialog-hassio-network")
export class DialogHassioNetwork extends LitElement export class DialogHassioNetwork extends LitElement
implements HassDialog<HassioNetworkDialogParams> { implements HassDialog<HassioNetworkDialogParams> {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@internalProperty() private _prosessing = false; @internalProperty() private _accessPoints?: AccessPoints;
@internalProperty() private _params?: HassioNetworkDialogParams;
@internalProperty() private _network!: {
interface: string;
data: NetworkInterface;
}[];
@internalProperty() private _curTabIndex = 0; @internalProperty() private _curTabIndex = 0;
@internalProperty() private _device?: {
interface: string;
data: NetworkInterface;
};
@internalProperty() private _dirty = false; @internalProperty() private _dirty = false;
@internalProperty() private _interface?: NetworkInterface;
@internalProperty() private _interfaces!: NetworkInterface[];
@internalProperty() private _params?: HassioNetworkDialogParams;
@internalProperty() private _processing = false;
@internalProperty() private _scanning = false;
@internalProperty() private _wifiConfiguration?: WifiConfiguration;
public async showDialog(params: HassioNetworkDialogParams): Promise<void> { public async showDialog(params: HassioNetworkDialogParams): Promise<void> {
this._params = params; this._params = params;
this._dirty = false; this._dirty = false;
this._curTabIndex = 0; this._curTabIndex = 0;
this._network = Object.keys(params.network?.interfaces) this._interfaces = params.network.interfaces.sort((a, b) => {
.map((device) => ({ return a.primary > b.primary ? -1 : 1;
interface: device, });
data: params.network.interfaces[device], this._interface = { ...this._interfaces[this._curTabIndex] };
}))
.sort((a, b) => {
return a.data.primary > b.data.primary ? -1 : 1;
});
this._device = this._network[this._curTabIndex];
this._device.data.nameservers = String(this._device.data.nameservers);
await this.updateComplete; await this.updateComplete;
} }
public closeDialog(): void { public closeDialog(): void {
this._params = undefined; this._params = undefined;
this._prosessing = false; this._processing = false;
fireEvent(this, "dialog-closed", { dialog: this.localName }); fireEvent(this, "dialog-closed", { dialog: this.localName });
} }
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._params || !this._network) { if (!this._params || !this._interface) {
return html``; return html``;
} }
@@ -107,11 +110,11 @@ export class DialogHassioNetwork extends LitElement
<ha-svg-icon .path=${mdiClose}></ha-svg-icon> <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>
</ha-header-bar> </ha-header-bar>
${this._network.length > 1 ${this._interfaces.length > 1
? html` <mwc-tab-bar ? html` <mwc-tab-bar
.activeIndex=${this._curTabIndex} .activeIndex=${this._curTabIndex}
@MDCTabBar:activated=${this._handleTabActivated} @MDCTabBar:activated=${this._handleTabActivated}
>${this._network.map( >${this._interfaces.map(
(device) => (device) =>
html`<mwc-tab html`<mwc-tab
.id=${device.interface} .id=${device.interface}
@@ -129,81 +132,302 @@ export class DialogHassioNetwork extends LitElement
private _renderTab() { private _renderTab() {
return html` <div class="form container"> return html` <div class="form container">
<ha-formfield label="DHCP"> ${IP_VERSIONS.map((version) =>
<ha-radio this._interface![version] ? this._renderIPConfiguration(version) : ""
@change=${this._handleRadioValueChanged} )}
value="dhcp" ${this._interface?.type === "wireless"
name="method" ? html`
?checked=${this._device!.data.method === "dhcp"} <ha-expansion-panel header="Wi-Fi" outlined>
> ${this._interface?.wifi?.ssid
</ha-radio> ? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>`
</ha-formfield> : ""}
<ha-formfield label="Static"> <mwc-button
<ha-radio class="scan"
@change=${this._handleRadioValueChanged} @click=${this._scanForAP}
value="static" .disabled=${this._scanning}
name="method" >
?checked=${this._device!.data.method === "static"} ${this._scanning
> ? html`<ha-circular-progress active size="small">
</ha-radio> </ha-circular-progress>`
</ha-formfield> : "Scan for accesspoints"}
${this._device!.data.method !== "dhcp" </mwc-button>
? html` <paper-input ${this._accessPoints &&
class="flex-auto" this._accessPoints.accesspoints &&
id="ip_address" this._accessPoints.accesspoints.length !== 0
label="IP address/Netmask" ? html`
.value="${this._device!.data.ip_address}" <mwc-list>
@value-changed=${this._handleInputValueChanged} ${this._accessPoints.accesspoints
></paper-input> .filter((ap) => ap.ssid)
<paper-input .map(
class="flex-auto" (ap) =>
id="gateway" html`
label="Gateway address" <mwc-list-item
.value="${this._device!.data.gateway}" twoline
@value-changed=${this._handleInputValueChanged} @click=${this._selectAP}
></paper-input> .activated=${ap.ssid ===
<paper-input this._wifiConfiguration?.ssid}
class="flex-auto" .ap=${ap}
id="nameservers" >
label="DNS servers" <span>${ap.ssid}</span>
.value="${this._device!.data.nameservers as string}" <span slot="secondary">
@value-changed=${this._handleInputValueChanged} ${ap.mac} - Strength: ${ap.signal}
></paper-input> </span>
NB!: If you are changing IP or gateway addresses, you might lose </mwc-list-item>
the connection.` `
)}
</mwc-list>
`
: ""}
${this._wifiConfiguration
? html`
<div class="radio-row">
<ha-formfield label="open">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="open"
name="auth"
.checked=${this._wifiConfiguration.auth ===
undefined ||
this._wifiConfiguration.auth === "open"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wep">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wep"
name="auth"
.checked=${this._wifiConfiguration.auth === "wep"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="wpa-psk">
<ha-radio
@change=${this._handleRadioValueChangedAp}
.ap=${this._wifiConfiguration}
value="wpa-psk"
name="auth"
.checked=${this._wifiConfiguration.auth ===
"wpa-psk"}
>
</ha-radio>
</ha-formfield>
</div>
${this._wifiConfiguration.auth === "wpa-psk" ||
this._wifiConfiguration.auth === "wep"
? html`
<paper-input
class="flex-auto"
type="password"
id="psk"
label="Password"
version="wifi"
@value-changed=${this
._handleInputValueChangedWifi}
>
</paper-input>
`
: ""}
`
: ""}
</ha-expansion-panel>
`
: ""}
${this._dirty
? html`<div class="warning">
If you are changing the Wi-Fi, IP or gateway addresses, you might
lose the connection!
</div>`
: ""} : ""}
</div> </div>
<div class="buttons"> <div class="buttons">
<mwc-button label="close" @click=${this.closeDialog}> </mwc-button> <mwc-button label="close" @click=${this.closeDialog}> </mwc-button>
<mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}> <mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}>
${this._prosessing ${this._processing
? html`<ha-circular-progress active></ha-circular-progress>` ? html`<ha-circular-progress active size="small">
: "Update"} </ha-circular-progress>`
: "Save"}
</mwc-button> </mwc-button>
</div>`; </div>`;
} }
private async _updateNetwork() { private _selectAP(event) {
this._prosessing = true; this._wifiConfiguration = event.currentTarget.ap;
let options: Partial<NetworkInterface> = { this._dirty = true;
method: this._device!.data.method, }
};
if (options.method !== "dhcp") { private async _scanForAP() {
options = { if (!this._interface) {
...options, return;
address: this._device!.data.ip_address,
gateway: this._device!.data.gateway,
dns: String(this._device!.data.nameservers).split(","),
};
} }
this._scanning = true;
try { try {
await updateNetworkInterface(this.hass, this._device!.interface, options); this._accessPoints = await accesspointScan(
this.hass,
this._interface.interface
);
} catch (err) {
showAlertDialog(this, {
title: "Failed to scan for accesspoints",
text: extractApiErrorMessage(err),
});
} finally {
this._scanning = false;
}
}
private _renderIPConfiguration(version: string) {
return html`
<ha-expansion-panel
.header=${`IPv${version.charAt(version.length - 1)}`}
outlined
>
<div class="radio-row">
<ha-formfield label="DHCP">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="auto"
name="${version}method"
.checked=${this._interface![version]?.method === "auto"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Static">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="static"
name="${version}method"
.checked=${this._interface![version]?.method === "static"}
>
</ha-radio>
</ha-formfield>
<ha-formfield label="Disabled" class="warning">
<ha-radio
@change=${this._handleRadioValueChanged}
.version=${version}
value="disabled"
name="${version}method"
.checked=${this._interface![version]?.method === "disabled"}
>
</ha-radio>
</ha-formfield>
</div>
${this._interface![version].method === "static"
? html`
<paper-input
class="flex-auto"
id="address"
label="IP address/Netmask"
.version=${version}
.value=${this._toString(this._interface![version].address)}
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
<paper-input
class="flex-auto"
id="gateway"
label="Gateway address"
.version=${version}
.value=${this._interface![version].gateway}
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
<paper-input
class="flex-auto"
id="nameservers"
label="DNS servers"
.version=${version}
.value=${this._toString(this._interface![version].nameservers)}
@value-changed=${this._handleInputValueChanged}
>
</paper-input>
`
: ""}
</ha-expansion-panel>
`;
}
_toArray(data: string | string[]): string[] {
if (Array.isArray(data)) {
if (data && typeof data[0] === "string") {
data = data[0];
}
}
if (!data) {
return [];
}
if (typeof data === "string") {
return data.replace(/ /g, "").split(",");
}
return data;
}
_toString(data: string | string[]): string {
if (!data) {
return "";
}
if (Array.isArray(data)) {
return data.join(", ");
}
return data;
}
private async _updateNetwork() {
this._processing = true;
let interfaceOptions: Partial<NetworkInterface> = {};
IP_VERSIONS.forEach((version) => {
interfaceOptions[version] = {
method: this._interface![version]?.method || "auto",
};
if (this._interface![version]?.method === "static") {
interfaceOptions[version] = {
...interfaceOptions[version],
address: this._toArray(this._interface![version]?.address),
gateway: this._interface![version]?.gateway,
nameservers: this._toArray(this._interface![version]?.nameservers),
};
}
});
if (this._wifiConfiguration) {
interfaceOptions = {
...interfaceOptions,
wifi: {
ssid: this._wifiConfiguration.ssid,
mode: this._wifiConfiguration.mode,
auth: this._wifiConfiguration.auth || "open",
},
};
if (interfaceOptions.wifi!.auth !== "open") {
interfaceOptions.wifi = {
...interfaceOptions.wifi,
psk: this._wifiConfiguration.psk,
};
}
}
interfaceOptions.enabled =
this._wifiConfiguration !== undefined ||
interfaceOptions.ipv4?.method !== "disabled" ||
interfaceOptions.ipv6?.method !== "disabled";
try {
await updateNetworkInterface(
this.hass,
this._interface!.interface,
interfaceOptions
);
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to change network settings", title: "Failed to change network settings",
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
this._prosessing = false; this._processing = false;
return; return;
} }
this._params?.loadData(); this._params?.loadData();
@@ -219,40 +443,73 @@ export class DialogHassioNetwork extends LitElement
dismissText: "no", dismissText: "no",
}); });
if (!confirm) { if (!confirm) {
this.requestUpdate("_device"); this.requestUpdate("_interface");
return; return;
} }
} }
this._curTabIndex = ev.detail.index; this._curTabIndex = ev.detail.index;
this._device = this._network[ev.detail.index]; this._interface = { ...this._interfaces[ev.detail.index] };
this._device.data.nameservers = String(this._device.data.nameservers);
} }
private _handleRadioValueChanged(ev: CustomEvent): void { private _handleRadioValueChanged(ev: CustomEvent): void {
const value = (ev.target as HaRadio).value as "dhcp" | "static"; const value = (ev.target as any).value as "disabled" | "auto" | "static";
const version = (ev.target as any).version as "ipv4" | "ipv6";
if (!value || !this._device || this._device!.data.method === value) { if (
!value ||
!this._interface ||
this._interface[version]!.method === value
) {
return; return;
} }
this._dirty = true; this._dirty = true;
this._device!.data.method = value; this._interface[version]!.method = value;
this.requestUpdate("_device"); this.requestUpdate("_interface");
}
private _handleRadioValueChangedAp(ev: CustomEvent): void {
const value = ((ev.target as any).value as string) as
| "open"
| "wep"
| "wpa-psk";
this._wifiConfiguration!.auth = value;
this._dirty = true;
this.requestUpdate("_wifiConfiguration");
} }
private _handleInputValueChanged(ev: CustomEvent): void { private _handleInputValueChanged(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement) const value: string | null | undefined = (ev.target as PaperInputElement)
.value; .value;
const version = (ev.target as any).version as "ipv4" | "ipv6";
const id = (ev.target as PaperInputElement).id; const id = (ev.target as PaperInputElement).id;
if (!value || !this._device || this._device.data[id] === value) { if (
!value ||
!this._interface ||
this._toString(this._interface[version]![id]) === this._toString(value)
) {
return; return;
} }
this._dirty = true; this._dirty = true;
this._interface[version]![id] = value;
}
this._device.data[id] = value; private _handleInputValueChangedWifi(ev: CustomEvent): void {
const value: string | null | undefined = (ev.target as PaperInputElement)
.value;
const id = (ev.target as PaperInputElement).id;
if (
!value ||
!this._wifiConfiguration ||
this._wifiConfiguration![id] === value
) {
return;
}
this._dirty = true;
this._wifiConfiguration![id] = value;
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {
@@ -299,12 +556,16 @@ export class DialogHassioNetwork extends LitElement
--mdc-theme-primary: var(--error-color); --mdc-theme-primary: var(--error-color);
} }
mwc-button.scan {
margin-left: 8px;
}
:host([rtl]) app-toolbar { :host([rtl]) app-toolbar {
direction: rtl; direction: rtl;
text-align: right; text-align: right;
} }
.container { .container {
padding: 20px 24px; padding: 0 8px 4px;
} }
.form { .form {
margin-bottom: 53px; margin-bottom: 53px;
@@ -322,6 +583,24 @@ export class DialogHassioNetwork extends LitElement
padding-bottom: max(env(safe-area-inset-bottom), 8px); padding-bottom: max(env(safe-area-inset-bottom), 8px);
background-color: var(--mdc-theme-surface, #fff); background-color: var(--mdc-theme-surface, #fff);
} }
.warning {
color: var(--error-color);
--primary-color: var(--error-color);
}
div.warning {
margin: 12px 4px -12px;
}
ha-expansion-panel {
--expansion-panel-summary-padding: 0 16px;
margin: 4px 0;
}
paper-input {
padding: 0 14px;
}
mwc-list-item {
--mdc-list-side-padding: 10px;
}
`, `,
]; ];
} }

View File

@@ -13,10 +13,7 @@ export const showNetworkDialog = (
): void => { ): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-network", dialogTag: "dialog-hassio-network",
dialogImport: () => dialogImport: () => import("./dialog-hassio-network"),
import(
/* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network"
),
dialogParams, dialogParams,
}); });
}; };

View File

@@ -4,10 +4,7 @@ import "./dialog-hassio-registries";
export const showRegistriesDialog = (element: HTMLElement): void => { export const showRegistriesDialog = (element: HTMLElement): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-registries", dialogTag: "dialog-hassio-registries",
dialogImport: () => dialogImport: () => import("./dialog-hassio-registries"),
import(
/* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries"
),
dialogParams: {}, dialogParams: {},
}); });
}; };

View File

@@ -13,10 +13,7 @@ export const showRepositoriesDialog = (
): void => { ): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-repositories", dialogTag: "dialog-hassio-repositories",
dialogImport: () => dialogImport: () => import("./dialog-hassio-repositories"),
import(
/* webpackChunkName: "dialog-hassio-repositories" */ "./dialog-hassio-repositories"
),
dialogParams, dialogParams,
}); });
}; };

View File

@@ -109,7 +109,7 @@ class HassioSnapshotDialog extends LitElement {
return html``; return html``;
} }
return html` return html`
<ha-dialog open stacked @closing=${this._closeDialog} .heading=${true}> <ha-dialog open @closing=${this._closeDialog} .heading=${true}>
<div slot="heading"> <div slot="heading">
<ha-header-bar> <ha-header-bar>
<span slot="title"> <span slot="title">
@@ -191,47 +191,37 @@ class HassioSnapshotDialog extends LitElement {
: ""} : ""}
${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""} ${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""}
<div>Actions:</div> <div class="button-row" slot="primaryAction">
${!this._onboarding <mwc-button @click=${this._partialRestoreClicked}>
? html`<mwc-button <ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
@click=${this._downloadClicked} Restore Selected
slot="primaryAction" </mwc-button>
> ${!this._onboarding
<ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon> ? html`
Download Snapshot <mwc-button @click=${this._deleteClicked}>
</mwc-button>` <ha-svg-icon .path=${mdiDelete} class="icon warning">
: ""} </ha-svg-icon>
<span class="warning">Delete Snapshot</span>
<mwc-button </mwc-button>
@click=${this._partialRestoreClicked} `
slot="secondaryAction" : ""}
> </div>
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon> <div class="button-row" slot="secondaryAction">
Restore Selected ${this._snapshot.type === "full"
</mwc-button> ? html`
${this._snapshot.type === "full" <mwc-button @click=${this._fullRestoreClicked}>
? html` <ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon>
<mwc-button Restore Everything
@click=${this._fullRestoreClicked} </mwc-button>
slot="secondaryAction" `
> : ""}
<ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon> ${!this._onboarding
Wipe &amp; restore ? html`<mwc-button @click=${this._downloadClicked}>
</mwc-button> <ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon>
` Download Snapshot
: ""} </mwc-button>`
${!this._onboarding : ""}
? html`<mwc-button </div>
@click=${this._deleteClicked}
slot="secondaryAction"
>
<ha-svg-icon
.path=${mdiDelete}
class="icon warning"
></ha-svg-icon>
<span class="warning">Delete Snapshot</span>
</mwc-button>`
: ""}
</ha-dialog> </ha-dialog>
`; `;
} }
@@ -245,6 +235,14 @@ class HassioSnapshotDialog extends LitElement {
display: block; display: block;
margin: 4px; margin: 4px;
} }
mwc-button ha-svg-icon {
margin-right: 4px;
}
.button-row {
display: grid;
gap: 8px;
margin-right: 8px;
}
.details { .details {
color: var(--secondary-text-color); color: var(--secondary-text-color);
} }
@@ -252,10 +250,6 @@ class HassioSnapshotDialog extends LitElement {
.error { .error {
color: var(--error-color); color: var(--error-color);
} }
.buttons {
display: flex;
flex-direction: column;
}
.buttons li { .buttons li {
list-style-type: none; list-style-type: none;
} }

View File

@@ -12,10 +12,7 @@ export const showHassioSnapshotDialog = (
): void => { ): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-snapshot", dialogTag: "dialog-hassio-snapshot",
dialogImport: () => dialogImport: () => import("./dialog-hassio-snapshot"),
import(
/* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot"
),
dialogParams, dialogParams,
}); });
}; };

View File

@@ -13,10 +13,7 @@ export const showSnapshotUploadDialog = (
): void => { ): void => {
fireEvent(element, "show-dialog", { fireEvent(element, "show-dialog", {
dialogTag: "dialog-hassio-snapshot-upload", dialogTag: "dialog-hassio-snapshot-upload",
dialogImport: () => dialogImport: () => import("./dialog-hassio-snapshot-upload"),
import(
/* webpackChunkName: "dialog-hassio-snapshot-upload" */ "./dialog-hassio-snapshot-upload"
),
dialogParams, dialogParams,
}); });
}; };

View File

@@ -1,29 +1,22 @@
import { import { html, PropertyValues, customElement, property } from "lit-element";
html,
PropertyValues,
customElement,
LitElement,
property,
} from "lit-element";
import "./hassio-router"; import "./hassio-router";
import { urlSyncMixin } from "../../src/state/url-sync-mixin";
import { ProvideHassLitMixin } from "../../src/mixins/provide-hass-lit-mixin";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element"; import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element";
import { fireEvent } from "../../src/common/dom/fire_event"; import { fireEvent } from "../../src/common/dom/fire_event";
import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; import { makeDialogManager } from "../../src/dialogs/make-dialog-manager";
import { atLeastVersion } from "../../src/common/config/version"; import { atLeastVersion } from "../../src/common/config/version";
import { SupervisorBaseElement } from "./supervisor-base-element";
@customElement("hassio-main") @customElement("hassio-main")
export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) { export class HassioMain extends SupervisorBaseElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo; @property({ attribute: false }) public panel!: HassioPanelInfo;
@property() public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property() public route?: Route; @property({ attribute: false }) public route?: Route;
protected firstUpdated(changedProps: PropertyValues) { protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps); super.firstUpdated(changedProps);
@@ -77,9 +70,13 @@ export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) {
} }
protected render() { protected render() {
if (!this.supervisor || !this.hass) {
return html``;
}
return html` return html`
<hassio-router <hassio-router
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route} .route=${this.route}
.panel=${this.panel} .panel=${this.panel}
.narrow=${this.narrow} .narrow=${this.narrow}

View File

@@ -1,10 +1,5 @@
import { customElement, property } from "lit-element"; import { customElement, property } from "lit-element";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host"; import { Supervisor } from "../../src/data/supervisor/supervisor";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import { import {
HassRouterPage, HassRouterPage,
RouterOptions, RouterOptions,
@@ -21,20 +16,12 @@ import "./system/hassio-system";
class HassioPanelRouter extends HassRouterPage { class HassioPanelRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo?: HassioHostInfo;
@property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
routes: { routes: {
dashboard: { dashboard: {
@@ -54,13 +41,9 @@ class HassioPanelRouter extends HassRouterPage {
protected updatePageEl(el) { protected updatePageEl(el) {
el.hass = this.hass; el.hass = this.hass;
el.supervisor = this.supervisor;
el.route = this.route; el.route = this.route;
el.narrow = this.narrow; el.narrow = this.narrow;
el.supervisorInfo = this.supervisorInfo;
el.hassioInfo = this.hassioInfo;
el.hostInfo = this.hostInfo;
el.hassInfo = this.hassInfo;
el.hassOsInfo = this.hassOsInfo;
} }
} }

View File

@@ -1,18 +1,13 @@
import { import {
css,
CSSResult,
customElement, customElement,
html, html,
LitElement, LitElement,
property, property,
TemplateResult, TemplateResult,
css,
CSSResult,
} from "lit-element"; } from "lit-element";
import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host"; import { Supervisor } from "../../src/data/supervisor/supervisor";
import {
HassioHomeAssistantInfo,
HassioSupervisorInfo,
HassioInfo,
} from "../../src/data/hassio/supervisor";
import { HomeAssistant, Route } from "../../src/types"; import { HomeAssistant, Route } from "../../src/types";
import "./hassio-panel-router"; import "./hassio-panel-router";
@@ -20,34 +15,19 @@ import "./hassio-panel-router";
class HassioPanel extends LitElement { class HassioPanel extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.supervisorInfo) {
return html``;
}
return html` return html`
<hassio-panel-router <hassio-panel-router
.route=${this.route}
.hass=${this.hass} .hass=${this.hass}
.supervisor=${this.supervisor}
.route=${this.route}
.narrow=${this.narrow} .narrow=${this.narrow}
.supervisorInfo=${this.supervisorInfo}
.hassioInfo=${this.hassioInfo}
.hostInfo=${this.hostInfo}
.hassInfo=${this.hassInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-panel-router> ></hassio-panel-router>
`; `;
} }

View File

@@ -1,24 +1,6 @@
import { import { customElement, property } from "lit-element";
customElement, import { HassioPanelInfo } from "../../src/data/hassio/supervisor";
property, import { Supervisor } from "../../src/data/supervisor/supervisor";
internalProperty,
PropertyValues,
} from "lit-element";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo,
} from "../../src/data/hassio/host";
import {
fetchHassioHomeAssistantInfo,
fetchHassioSupervisorInfo,
fetchHassioInfo,
HassioHomeAssistantInfo,
HassioInfo,
HassioPanelInfo,
HassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { import {
HassRouterPage, HassRouterPage,
RouterOptions, RouterOptions,
@@ -32,9 +14,11 @@ import "./hassio-panel";
class HassioRouter extends HassRouterPage { class HassioRouter extends HassRouterPage {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public panel!: HassioPanelInfo; @property({ attribute: false }) public supervisor!: Supervisor;
@property() public narrow!: boolean; @property({ attribute: false }) public panel!: HassioPanelInfo;
@property({ type: Boolean }) public narrow!: boolean;
protected routerOptions: RouterOptions = { protected routerOptions: RouterOptions = {
// Hass.io has a page with tabs, so we route all non-matching routes to it. // Hass.io has a page with tabs, so we route all non-matching routes to it.
@@ -51,47 +35,22 @@ class HassioRouter extends HassRouterPage {
system: "dashboard", system: "dashboard",
addon: { addon: {
tag: "hassio-addon-dashboard", tag: "hassio-addon-dashboard",
load: () => load: () => import("./addon-view/hassio-addon-dashboard"),
import(
/* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard"
),
}, },
ingress: { ingress: {
tag: "hassio-ingress-view", tag: "hassio-ingress-view",
load: () => load: () => import("./ingress-view/hassio-ingress-view"),
import(
/* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view"
),
}, },
}, },
}; };
@internalProperty() private _supervisorInfo?: HassioSupervisorInfo;
@internalProperty() private _hostInfo?: HassioHostInfo;
@internalProperty() private _hassioInfo?: HassioInfo;
@internalProperty() private _hassOsInfo?: HassioHassOSInfo;
@internalProperty() private _hassInfo?: HassioHomeAssistantInfo;
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev));
}
protected updatePageEl(el) { protected updatePageEl(el) {
// the tabs page does its own routing so needs full route. // the tabs page does its own routing so needs full route.
const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail; const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail;
el.hass = this.hass; el.hass = this.hass;
el.supervisor = this.supervisor;
el.narrow = this.narrow; el.narrow = this.narrow;
el.supervisorInfo = this._supervisorInfo;
el.hassioInfo = this._hassioInfo;
el.hostInfo = this._hostInfo;
el.hassInfo = this._hassInfo;
el.hassOsInfo = this._hassOsInfo;
el.route = route; el.route = route;
if (el.localName === "hassio-ingress-view") { if (el.localName === "hassio-ingress-view") {
@@ -102,45 +61,12 @@ class HassioRouter extends HassRouterPage {
private async _fetchData() { private async _fetchData() {
if (this.panel.config && this.panel.config.ingress) { if (this.panel.config && this.panel.config.ingress) {
this._redirectIngress(this.panel.config.ingress); this._redirectIngress(this.panel.config.ingress);
return;
}
const [supervisorInfo, hostInfo, hassInfo, hassioInfo] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
]);
this._supervisorInfo = supervisorInfo;
this._hassioInfo = hassioInfo;
this._hostInfo = hostInfo;
this._hassInfo = hassInfo;
if (this._hostInfo.features && this._hostInfo.features.includes("hassos")) {
this._hassOsInfo = await fetchHassioHassOsInfo(this.hass);
} }
} }
private _redirectIngress(addonSlug: string) { private _redirectIngress(addonSlug: string) {
this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` }; this.route = { prefix: "/hassio", path: `/ingress/${addonSlug}` };
} }
private _apiCalled(ev) {
if (!ev.detail.success) {
return;
}
let tries = 1;
const tryUpdate = () => {
this._fetchData().catch(() => {
tries += 1;
setTimeout(tryUpdate, Math.min(tries, 5) * 1000);
});
};
tryUpdate();
}
} }
declare global { declare global {

View File

@@ -26,7 +26,6 @@ import {
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { atLeastVersion } from "../../../src/common/config/version"; 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/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -41,7 +40,7 @@ import {
HassioSnapshot, HassioSnapshot,
reloadHassioSnapshots, reloadHassioSnapshots,
} from "../../../src/data/hassio/snapshot"; } from "../../../src/data/hassio/snapshot";
import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { PolymerChangedEvent } from "../../../src/polymer-types"; import { PolymerChangedEvent } from "../../../src/polymer-types";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
@@ -67,7 +66,7 @@ class HassioSnapshots extends LitElement {
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo; @property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _snapshotName = ""; @internalProperty() private _snapshotName = "";
@@ -266,7 +265,7 @@ class HassioSnapshots extends LitElement {
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {
if (changedProps.has("supervisorInfo")) { if (changedProps.has("supervisorInfo")) {
this._addonList = this.supervisorInfo.addons this._addonList = this.supervisor.supervisor.addons
.map((addon) => ({ .map((addon) => ({
slug: addon.slug, slug: addon.slug,
name: addon.name, name: addon.name,
@@ -372,7 +371,6 @@ class HassioSnapshots extends LitElement {
await createHassioPartialSnapshot(this.hass, data); await createHassioPartialSnapshot(this.hass, data);
} }
this._updateSnapshots(); this._updateSnapshots();
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) { } catch (err) {
this._error = extractApiErrorMessage(err); this._error = extractApiErrorMessage(err);
} }

View File

@@ -0,0 +1,69 @@
import { LitElement, property, PropertyValues } from "lit-element";
import {
fetchHassioHassOsInfo,
fetchHassioHostInfo,
} from "../../src/data/hassio/host";
import { fetchNetworkInfo } from "../../src/data/hassio/network";
import { fetchHassioResolution } from "../../src/data/hassio/resolution";
import {
fetchHassioHomeAssistantInfo,
fetchHassioInfo,
fetchHassioSupervisorInfo,
} from "../../src/data/hassio/supervisor";
import { Supervisor } 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>;
}
}
export class SupervisorBaseElement extends urlSyncMixin(
ProvideHassLitMixin(LitElement)
) {
@property({ attribute: false }) public supervisor?: Supervisor;
protected _updateSupervisor(obj: Partial<Supervisor>): void {
this.supervisor = { ...this.supervisor!, ...obj };
}
protected firstUpdated(changedProps: PropertyValues): void {
super.firstUpdated(changedProps);
this._initSupervisor();
this.addEventListener("supervisor-update", (ev) =>
this._updateSupervisor(ev.detail)
);
}
private async _initSupervisor(): Promise<void> {
const [
supervisor,
host,
core,
info,
os,
network,
resolution,
] = await Promise.all([
fetchHassioSupervisorInfo(this.hass),
fetchHassioHostInfo(this.hass),
fetchHassioHomeAssistantInfo(this.hass),
fetchHassioInfo(this.hass),
fetchHassioHassOsInfo(this.hass),
fetchNetworkInfo(this.hass),
fetchHassioResolution(this.hass),
]);
this.supervisor = {
supervisor,
host,
core,
info,
os,
network,
resolution,
};
}
}

View File

@@ -8,12 +8,12 @@ import {
CSSResult, CSSResult,
customElement, customElement,
html, html,
internalProperty,
LitElement, LitElement,
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
import { fireEvent } from "../../../src/common/dom/fire_event";
import "../../../src/components/buttons/ha-progress-button"; import "../../../src/components/buttons/ha-progress-button";
import "../../../src/components/ha-button-menu"; import "../../../src/components/ha-button-menu";
import "../../../src/components/ha-card"; import "../../../src/components/ha-card";
@@ -27,8 +27,6 @@ import {
changeHostOptions, changeHostOptions,
configSyncOS, configSyncOS,
fetchHassioHostInfo, fetchHassioHostInfo,
HassioHassOSInfo,
HassioHostInfo as HassioHostInfoType,
rebootHost, rebootHost,
shutdownHost, shutdownHost,
updateOS, updateOS,
@@ -37,7 +35,7 @@ import {
fetchNetworkInfo, fetchNetworkInfo,
NetworkInfo, NetworkInfo,
} from "../../../src/data/hassio/network"; } from "../../../src/data/hassio/network";
import { HassioInfo } from "../../../src/data/hassio/supervisor"; import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -53,28 +51,22 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioHostInfo extends LitElement { class HassioHostInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public hostInfo!: HassioHostInfoType; @property({ attribute: false }) public supervisor!: Supervisor;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
@internalProperty() public _networkInfo?: NetworkInfo;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
const primaryIpAddress = this.hostInfo.features.includes("network") const primaryIpAddress = this.supervisor.host.features.includes("network")
? this._primaryIpAddress(this._networkInfo!) ? this._primaryIpAddress(this.supervisor.network!)
: ""; : "";
return html` return html`
<ha-card header="Host System"> <ha-card header="Host System">
<div class="card-content"> <div class="card-content">
${this.hostInfo.features.includes("hostname") ${this.supervisor.host.features.includes("hostname")
? html`<ha-settings-row> ? html`<ha-settings-row>
<span slot="heading"> <span slot="heading">
Hostname Hostname
</span> </span>
<span slot="description"> <span slot="description">
${this.hostInfo.hostname} ${this.supervisor.host.hostname}
</span> </span>
<mwc-button <mwc-button
title="Change the hostname" title="Change the hostname"
@@ -84,7 +76,7 @@ class HassioHostInfo extends LitElement {
</mwc-button> </mwc-button>
</ha-settings-row>` </ha-settings-row>`
: ""} : ""}
${this.hostInfo.features.includes("network") ${this.supervisor.host.features.includes("network")
? html` <ha-settings-row> ? html` <ha-settings-row>
<span slot="heading"> <span slot="heading">
IP Address IP Address
@@ -106,10 +98,9 @@ class HassioHostInfo extends LitElement {
Operating System Operating System
</span> </span>
<span slot="description"> <span slot="description">
${this.hostInfo.operating_system} ${this.supervisor.host.operating_system}
</span> </span>
${this.hostInfo.features.includes("hassos") && ${this.supervisor.os.update_available
this.hassOsInfo.update_available
? html` ? html`
<ha-progress-button <ha-progress-button
title="Update the host OS" title="Update the host OS"
@@ -120,29 +111,29 @@ class HassioHostInfo extends LitElement {
` `
: ""} : ""}
</ha-settings-row> </ha-settings-row>
${!this.hostInfo.features.includes("hassos") ${!this.supervisor.host.features.includes("hassos")
? html`<ha-settings-row> ? html`<ha-settings-row>
<span slot="heading"> <span slot="heading">
Docker version Docker version
</span> </span>
<span slot="description"> <span slot="description">
${this.hassioInfo.docker} ${this.supervisor.info.docker}
</span> </span>
</ha-settings-row>` </ha-settings-row>`
: ""} : ""}
${this.hostInfo.deployment ${this.supervisor.host.deployment
? html`<ha-settings-row> ? html`<ha-settings-row>
<span slot="heading"> <span slot="heading">
Deployment Deployment
</span> </span>
<span slot="description"> <span slot="description">
${this.hostInfo.deployment} ${this.supervisor.host.deployment}
</span> </span>
</ha-settings-row>` </ha-settings-row>`
: ""} : ""}
</div> </div>
<div class="card-actions"> <div class="card-actions">
${this.hostInfo.features.includes("reboot") ${this.supervisor.host.features.includes("reboot")
? html` ? html`
<ha-progress-button <ha-progress-button
title="Reboot the host OS" title="Reboot the host OS"
@@ -153,7 +144,7 @@ class HassioHostInfo extends LitElement {
</ha-progress-button> </ha-progress-button>
` `
: ""} : ""}
${this.hostInfo.features.includes("shutdown") ${this.supervisor.host.features.includes("shutdown")
? html` ? html`
<ha-progress-button <ha-progress-button
title="Shutdown the host OS" title="Shutdown the host OS"
@@ -175,7 +166,7 @@ class HassioHostInfo extends LitElement {
<mwc-list-item title="Show a list of hardware"> <mwc-list-item title="Show a list of hardware">
Hardware Hardware
</mwc-list-item> </mwc-list-item>
${this.hostInfo.features.includes("hassos") ${this.supervisor.host.features.includes("hassos")
? html`<mwc-list-item ? html`<mwc-list-item
title="Load HassOS configs or updates from USB" title="Load HassOS configs or updates from USB"
> >
@@ -193,12 +184,10 @@ class HassioHostInfo extends LitElement {
} }
private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => {
if (!network_info) { if (!network_info || !network_info.interfaces) {
return ""; return "";
} }
return Object.keys(network_info?.interfaces) return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0];
.map((device) => network_info.interfaces[device])
.find((device) => device.primary)?.ip_address;
}); });
private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { private async _handleMenuAction(ev: CustomEvent<ActionDetail>) {
@@ -316,13 +305,13 @@ class HassioHostInfo extends LitElement {
private async _changeNetworkClicked(): Promise<void> { private async _changeNetworkClicked(): Promise<void> {
showNetworkDialog(this, { showNetworkDialog(this, {
network: this._networkInfo!, network: this.supervisor.network!,
loadData: () => this._loadData(), loadData: () => this._loadData(),
}); });
} }
private async _changeHostnameClicked(): Promise<void> { private async _changeHostnameClicked(): Promise<void> {
const curHostname: string = this.hostInfo.hostname; const curHostname: string = this.supervisor.host.hostname;
const hostname = await showPromptDialog(this, { const hostname = await showPromptDialog(this, {
title: "Change Hostname", title: "Change Hostname",
inputLabel: "Please enter a new hostname:", inputLabel: "Please enter a new hostname:",
@@ -333,7 +322,8 @@ class HassioHostInfo extends LitElement {
if (hostname && hostname !== curHostname) { if (hostname && hostname !== curHostname) {
try { try {
await changeHostOptions(this.hass, { hostname }); await changeHostOptions(this.hass, { hostname });
this.hostInfo = await fetchHassioHostInfo(this.hass); const host = await fetchHassioHostInfo(this.hass);
fireEvent(this, "supervisor-update", { host });
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Setting hostname failed", title: "Setting hostname failed",
@@ -346,7 +336,8 @@ class HassioHostInfo extends LitElement {
private async _importFromUSB(): Promise<void> { private async _importFromUSB(): Promise<void> {
try { try {
await configSyncOS(this.hass); await configSyncOS(this.hass);
this.hostInfo = await fetchHassioHostInfo(this.hass); const host = await fetchHassioHostInfo(this.hass);
fireEvent(this, "supervisor-update", { host });
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to import from USB", title: "Failed to import from USB",
@@ -356,7 +347,8 @@ class HassioHostInfo extends LitElement {
} }
private async _loadData(): Promise<void> { private async _loadData(): Promise<void> {
this._networkInfo = await fetchNetworkInfo(this.hass); const network = await fetchNetworkInfo(this.hass);
fireEvent(this, "supervisor-update", { network });
} }
static get styles(): CSSResult[] { static get styles(): CSSResult[] {

View File

@@ -13,16 +13,15 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import "../../../src/components/ha-switch"; import "../../../src/components/ha-switch";
import { extractApiErrorMessage } from "../../../src/data/hassio/common"; import { extractApiErrorMessage } from "../../../src/data/hassio/common";
import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host";
import { fetchHassioResolution } from "../../../src/data/hassio/resolution";
import { import {
fetchHassioSupervisorInfo, fetchHassioSupervisorInfo,
HassioSupervisorInfo as HassioSupervisorInfoType,
reloadSupervisor, reloadSupervisor,
restartSupervisor,
setSupervisorOption, setSupervisorOption,
SupervisorOptions, SupervisorOptions,
updateSupervisor, updateSupervisor,
} from "../../../src/data/hassio/supervisor"; } from "../../../src/data/hassio/supervisor";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { import {
showAlertDialog, showAlertDialog,
showConfirmationDialog, showConfirmationDialog,
@@ -32,7 +31,7 @@ import { HomeAssistant } from "../../../src/types";
import { documentationUrl } from "../../../src/util/documentation-url"; import { documentationUrl } from "../../../src/util/documentation-url";
import { hassioStyle } from "../resources/hassio-style"; import { hassioStyle } from "../resources/hassio-style";
const ISSUES = { const UNSUPPORTED_REASON = {
container: { container: {
title: "Containers known to cause issues", title: "Containers known to cause issues",
url: "/more-info/unsupported/container", url: "/more-info/unsupported/container",
@@ -46,6 +45,10 @@ const ISSUES = {
title: "Docker Version", title: "Docker Version",
url: "/more-info/unsupported/docker_version", url: "/more-info/unsupported/docker_version",
}, },
job_conditions: {
title: "Ignored job conditions",
url: "/more-info/unsupported/job_conditions",
},
lxc: { title: "LXC", url: "/more-info/unsupported/lxc" }, lxc: { title: "LXC", url: "/more-info/unsupported/lxc" },
network_manager: { network_manager: {
title: "Network Manager", title: "Network Manager",
@@ -59,14 +62,30 @@ const ISSUES = {
systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" }, systemd: { title: "Systemd", url: "/more-info/unsupported/systemd" },
}; };
const UNHEALTHY_REASON = {
privileged: {
title: "Supervisor is not privileged",
url: "/more-info/unsupported/privileged",
},
supervisor: {
title: "Supervisor was not able to update",
url: "/more-info/unhealthy/supervisor",
},
setup: {
title: "Setup of the Supervisor failed",
url: "/more-info/unhealthy/setup",
},
docker: {
title: "The Docker environment is not working properly",
url: "/more-info/unhealthy/docker",
},
};
@customElement("hassio-supervisor-info") @customElement("hassio-supervisor-info")
class HassioSupervisorInfo extends LitElement { class HassioSupervisorInfo extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) @property({ attribute: false }) public supervisor!: Supervisor;
public supervisorInfo!: HassioSupervisorInfoType;
@property() public hostInfo!: HassioHostInfoType;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
@@ -77,7 +96,7 @@ class HassioSupervisorInfo extends LitElement {
Version Version
</span> </span>
<span slot="description"> <span slot="description">
${this.supervisorInfo.version} ${this.supervisor.supervisor.version}
</span> </span>
</ha-settings-row> </ha-settings-row>
<ha-settings-row> <ha-settings-row>
@@ -85,9 +104,9 @@ class HassioSupervisorInfo extends LitElement {
Newest Version Newest Version
</span> </span>
<span slot="description"> <span slot="description">
${this.supervisorInfo.version_latest} ${this.supervisor.supervisor.version_latest}
</span> </span>
${this.supervisorInfo.update_available ${this.supervisor.supervisor.update_available
? html` ? html`
<ha-progress-button <ha-progress-button
title="Update the supervisor" title="Update the supervisor"
@@ -103,9 +122,9 @@ class HassioSupervisorInfo extends LitElement {
Channel Channel
</span> </span>
<span slot="description"> <span slot="description">
${this.supervisorInfo.channel} ${this.supervisor.supervisor.channel}
</span> </span>
${this.supervisorInfo.channel === "beta" ${this.supervisor.supervisor.channel === "beta"
? html` ? html`
<ha-progress-button <ha-progress-button
@click=${this._toggleBeta} @click=${this._toggleBeta}
@@ -114,7 +133,7 @@ class HassioSupervisorInfo extends LitElement {
Leave beta channel Leave beta channel
</ha-progress-button> </ha-progress-button>
` `
: this.supervisorInfo.channel === "stable" : this.supervisor.supervisor.channel === "stable"
? html` ? html`
<ha-progress-button <ha-progress-button
@click=${this._toggleBeta} @click=${this._toggleBeta}
@@ -126,7 +145,7 @@ class HassioSupervisorInfo extends LitElement {
: ""} : ""}
</ha-settings-row> </ha-settings-row>
${this.supervisorInfo?.supported ${this.supervisor.supervisor.supported
? html` <ha-settings-row three-line> ? html` <ha-settings-row three-line>
<span slot="heading"> <span slot="heading">
Share Diagnostics Share Diagnostics
@@ -143,7 +162,7 @@ class HassioSupervisorInfo extends LitElement {
</div> </div>
<ha-switch <ha-switch
haptic haptic
.checked=${this.supervisorInfo.diagnostics} .checked=${this.supervisor.supervisor.diagnostics}
@change=${this._toggleDiagnostics} @change=${this._toggleDiagnostics}
></ha-switch> ></ha-switch>
</ha-settings-row>` </ha-settings-row>`
@@ -157,14 +176,33 @@ class HassioSupervisorInfo extends LitElement {
Learn more Learn more
</button> </button>
</div>`} </div>`}
${!this.supervisor.supervisor.healthy
? html`<div class="error">
Your installtion is running in an unhealthy state.
<button
class="link"
title="Learn more about why your system is marked as unhealthy"
@click=${this._unhealthyDialog}
>
Learn more
</button>
</div>`
: ""}
</div> </div>
<div class="card-actions"> <div class="card-actions">
<ha-progress-button <ha-progress-button
@click=${this._supervisorReload} @click=${this._supervisorReload}
title="Reload parts of the supervisor" title="Reload parts of the Supervisor"
> >
Reload Reload
</ha-progress-button> </ha-progress-button>
<ha-progress-button
class="warning"
@click=${this._supervisorRestart}
title="Restart the Supervisor"
>
Restart
</ha-progress-button>
</div> </div>
</ha-card> </ha-card>
`; `;
@@ -174,7 +212,7 @@ class HassioSupervisorInfo extends LitElement {
const button = ev.currentTarget as any; const button = ev.currentTarget as any;
button.progress = true; button.progress = true;
if (this.supervisorInfo.channel === "stable") { if (this.supervisor.supervisor.channel === "stable") {
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: "WARNING", title: "WARNING",
text: html` Beta releases are for testers and early adopters and can text: html` Beta releases are for testers and early adopters and can
@@ -203,18 +241,19 @@ class HassioSupervisorInfo extends LitElement {
try { try {
const data: Partial<SupervisorOptions> = { const data: Partial<SupervisorOptions> = {
channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable", channel:
this.supervisor.supervisor.channel === "stable" ? "beta" : "stable",
}; };
await setSupervisorOption(this.hass, data); await setSupervisorOption(this.hass, data);
await reloadSupervisor(this.hass); await this._reloadSupervisor();
fireEvent(this, "hass-api-called", { success: true, response: null });
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to set supervisor option", title: "Failed to set supervisor option",
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
} finally {
button.progress = false;
} }
button.progress = false;
} }
private async _supervisorReload(ev: CustomEvent): Promise<void> { private async _supervisorReload(ev: CustomEvent): Promise<void> {
@@ -222,15 +261,37 @@ class HassioSupervisorInfo extends LitElement {
button.progress = true; button.progress = true;
try { try {
await reloadSupervisor(this.hass); await this._reloadSupervisor();
this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass);
} catch (err) { } catch (err) {
showAlertDialog(this, { showAlertDialog(this, {
title: "Failed to reload the supervisor", title: "Failed to reload the supervisor",
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
} finally {
button.progress = false;
}
}
private async _reloadSupervisor(): Promise<void> {
await reloadSupervisor(this.hass);
const supervisor = await fetchHassioSupervisorInfo(this.hass);
fireEvent(this, "supervisor-update", { supervisor });
}
private async _supervisorRestart(ev: CustomEvent): Promise<void> {
const button = ev.currentTarget as any;
button.progress = true;
try {
await restartSupervisor(this.hass);
} catch (err) {
showAlertDialog(this, {
title: "Failed to restart the supervisor",
text: extractApiErrorMessage(err),
});
} finally {
button.progress = false;
} }
button.progress = false;
} }
private async _supervisorUpdate(ev: CustomEvent): Promise<void> { private async _supervisorUpdate(ev: CustomEvent): Promise<void> {
@@ -239,7 +300,7 @@ class HassioSupervisorInfo extends LitElement {
const confirmed = await showConfirmationDialog(this, { const confirmed = await showConfirmationDialog(this, {
title: "Update Supervisor", title: "Update Supervisor",
text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`, text: `Are you sure you want to update supervisor to version ${this.supervisor.supervisor.version_latest}?`,
confirmText: "update", confirmText: "update",
dismissText: "cancel", dismissText: "cancel",
}); });
@@ -256,8 +317,9 @@ class HassioSupervisorInfo extends LitElement {
title: "Failed to update the supervisor", title: "Failed to update the supervisor",
text: extractApiErrorMessage(err), text: extractApiErrorMessage(err),
}); });
} finally {
button.progress = false;
} }
button.progress = false;
} }
private async _diagnosticsInformationDialog(): Promise<void> { private async _diagnosticsInformationDialog(): Promise<void> {
@@ -276,22 +338,53 @@ class HassioSupervisorInfo extends LitElement {
} }
private async _unsupportedDialog(): Promise<void> { private async _unsupportedDialog(): Promise<void> {
const resolution = await fetchHassioResolution(this.hass);
await showAlertDialog(this, { await showAlertDialog(this, {
title: "You are running an unsupported installation", title: "You are running an unsupported installation",
text: html`Below is a list of issues found with your installation, click text: html`Below is a list of issues found with your installation, click
on the links to learn how you can resolve the issues. <br /><br /> on the links to learn how you can resolve the issues. <br /><br />
<ul> <ul>
${resolution.unsupported.map( ${this.supervisor.resolution.unsupported.map(
(issue) => html` (issue) => html`
<li> <li>
${ISSUES[issue] ${UNSUPPORTED_REASON[issue]
? html`<a ? html`<a
href="${documentationUrl(this.hass, ISSUES[issue].url)}" href="${documentationUrl(
this.hass,
UNSUPPORTED_REASON[issue].url
)}"
target="_blank" target="_blank"
rel="noreferrer" rel="noreferrer"
> >
${ISSUES[issue].title} ${UNSUPPORTED_REASON[issue].title}
</a>`
: issue}
</li>
`
)}
</ul>`,
});
}
private async _unhealthyDialog(): Promise<void> {
await showAlertDialog(this, {
title: "Your installation is unhealthy",
text: html`Running an unhealthy installation will cause issues. Below is a
list of issues found with your installation, click on the links to learn
how you can resolve the issues. <br /><br />
<ul>
${this.supervisor.resolution.unhealthy.map(
(issue) => html`
<li>
${UNHEALTHY_REASON[issue]
? html`<a
href="${documentationUrl(
this.hass,
UNHEALTHY_REASON[issue].url
)}"
target="_blank"
rel="noreferrer"
>
${UNHEALTHY_REASON[issue].title}
</a>` </a>`
: issue} : issue}
</li> </li>
@@ -304,7 +397,7 @@ class HassioSupervisorInfo extends LitElement {
private async _toggleDiagnostics(): Promise<void> { private async _toggleDiagnostics(): Promise<void> {
try { try {
const data: SupervisorOptions = { const data: SupervisorOptions = {
diagnostics: !this.supervisorInfo?.diagnostics, diagnostics: !this.supervisor.supervisor?.diagnostics,
}; };
await setSupervisorOption(this.hass, data); await setSupervisorOption(this.hass, data);
} catch (err) { } catch (err) {

View File

@@ -19,6 +19,7 @@ import "../../../src/components/ha-card";
import "../../../src/components/ha-settings-row"; import "../../../src/components/ha-settings-row";
import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common"; import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common";
import { HassioHostInfo } from "../../../src/data/hassio/host"; import { HassioHostInfo } from "../../../src/data/hassio/host";
import { Supervisor } from "../../../src/data/supervisor/supervisor";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant } from "../../../src/types"; import { HomeAssistant } from "../../../src/types";
import { bytesToString } from "../../../src/util/bytes-to-string"; import { bytesToString } from "../../../src/util/bytes-to-string";
@@ -32,7 +33,7 @@ import { hassioStyle } from "../resources/hassio-style";
class HassioSystemMetrics extends LitElement { class HassioSystemMetrics extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property() public hostInfo!: HassioHostInfo; @property({ attribute: false }) public supervisor!: Supervisor;
@internalProperty() private _supervisorMetrics?: HassioStats; @internalProperty() private _supervisorMetrics?: HassioStats;
@@ -64,8 +65,8 @@ class HassioSystemMetrics extends LitElement {
}, },
{ {
description: "Used Space", description: "Used Space",
value: this._getUsedSpace(this.hostInfo), value: this._getUsedSpace(this.supervisor.host),
tooltip: `${this.hostInfo.disk_used} GB/${this.hostInfo.disk_total} GB`, tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`,
}, },
]; ];

View File

@@ -7,14 +7,7 @@ import {
property, property,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import { import { Supervisor } from "../../../src/data/supervisor/supervisor";
HassioHassOSInfo,
HassioHostInfo,
} from "../../../src/data/hassio/host";
import {
HassioInfo,
HassioSupervisorInfo,
} from "../../../src/data/hassio/supervisor";
import "../../../src/layouts/hass-tabs-subpage"; import "../../../src/layouts/hass-tabs-subpage";
import { haStyle } from "../../../src/resources/styles"; import { haStyle } from "../../../src/resources/styles";
import { HomeAssistant, Route } from "../../../src/types"; import { HomeAssistant, Route } from "../../../src/types";
@@ -29,18 +22,12 @@ import "./hassio-system-metrics";
class HassioSystem extends LitElement { class HassioSystem extends LitElement {
@property({ attribute: false }) public hass!: HomeAssistant; @property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public supervisor!: Supervisor;
@property({ type: Boolean }) public narrow!: boolean; @property({ type: Boolean }) public narrow!: boolean;
@property({ attribute: false }) public route!: Route; @property({ attribute: false }) public route!: Route;
@property() public supervisorInfo!: HassioSupervisorInfo;
@property({ attribute: false }) public hassioInfo!: HassioInfo;
@property() public hostInfo!: HassioHostInfo;
@property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo;
protected render(): TemplateResult | void { protected render(): TemplateResult | void {
return html` return html`
<hass-tabs-subpage <hass-tabs-subpage
@@ -56,18 +43,15 @@ class HassioSystem extends LitElement {
<div class="card-group"> <div class="card-group">
<hassio-supervisor-info <hassio-supervisor-info
.hass=${this.hass} .hass=${this.hass}
.hostInfo=${this.hostInfo} .supervisor=${this.supervisor}
.supervisorInfo=${this.supervisorInfo}
></hassio-supervisor-info> ></hassio-supervisor-info>
<hassio-host-info <hassio-host-info
.hass=${this.hass} .hass=${this.hass}
.hassioInfo=${this.hassioInfo} .supervisor=${this.supervisor}
.hostInfo=${this.hostInfo}
.hassOsInfo=${this.hassOsInfo}
></hassio-host-info> ></hassio-host-info>
<hassio-system-metrics <hassio-system-metrics
.hass=${this.hass} .hass=${this.hass}
.hostInfo=${this.hostInfo} .supervisor=${this.supervisor}
></hassio-system-metrics> ></hassio-system-metrics>
</div> </div>
<hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log> <hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log>

View File

@@ -83,6 +83,9 @@
"@types/sortablejs": "^1.10.6", "@types/sortablejs": "^1.10.6",
"@vaadin/vaadin-combo-box": "^5.0.10", "@vaadin/vaadin-combo-box": "^5.0.10",
"@vaadin/vaadin-date-picker": "^4.0.7", "@vaadin/vaadin-date-picker": "^4.0.7",
"@vibrant/color": "^3.2.1-alpha.1",
"@vibrant/core": "^3.2.1-alpha.1",
"@vibrant/quantizer-mmcq": "^3.2.1-alpha.1",
"@vue/web-component-wrapper": "^1.2.0", "@vue/web-component-wrapper": "^1.2.0",
"@webcomponents/webcomponentsjs": "^2.2.7", "@webcomponents/webcomponentsjs": "^2.2.7",
"chart.js": "~2.8.0", "chart.js": "~2.8.0",
@@ -109,7 +112,7 @@
"marked": "^1.1.1", "marked": "^1.1.1",
"mdn-polyfills": "^5.16.0", "mdn-polyfills": "^5.16.0",
"memoize-one": "^5.0.2", "memoize-one": "^5.0.2",
"node-vibrant": "^3.1.6", "node-vibrant": "3.2.1-alpha.1",
"proxy-polyfill": "^0.3.1", "proxy-polyfill": "^0.3.1",
"punycode": "^2.1.1", "punycode": "^2.1.1",
"qrcode": "^1.4.4", "qrcode": "^1.4.4",
@@ -120,6 +123,8 @@
"superstruct": "^0.10.12", "superstruct": "^0.10.12",
"tinykeys": "^1.1.1", "tinykeys": "^1.1.1",
"unfetch": "^4.1.0", "unfetch": "^4.1.0",
"vis-data": "^7.1.1",
"vis-network": "^8.5.4",
"vue": "^2.6.11", "vue": "^2.6.11",
"vue2-daterange-picker": "^0.5.1", "vue2-daterange-picker": "^0.5.1",
"web-animations-js": "^2.3.2", "web-animations-js": "^2.3.2",
@@ -141,6 +146,9 @@
"@babel/plugin-syntax-import-meta": "^7.10.4", "@babel/plugin-syntax-import-meta": "^7.10.4",
"@babel/preset-env": "^7.11.5", "@babel/preset-env": "^7.11.5",
"@babel/preset-typescript": "^7.10.4", "@babel/preset-typescript": "^7.10.4",
"@koa/cors": "^3.1.0",
"@open-wc/dev-server-hmr": "^0.0.2",
"@rollup/plugin-babel": "^5.2.1",
"@rollup/plugin-commonjs": "^11.1.0", "@rollup/plugin-commonjs": "^11.1.0",
"@rollup/plugin-json": "^4.0.3", "@rollup/plugin-json": "^4.0.3",
"@rollup/plugin-node-resolve": "^7.1.3", "@rollup/plugin-node-resolve": "^7.1.3",
@@ -159,6 +167,8 @@
"@types/webspeechapi": "^0.0.29", "@types/webspeechapi": "^0.0.29",
"@typescript-eslint/eslint-plugin": "^4.4.0", "@typescript-eslint/eslint-plugin": "^4.4.0",
"@typescript-eslint/parser": "^4.4.0", "@typescript-eslint/parser": "^4.4.0",
"@web/dev-server": "^0.0.24",
"@web/dev-server-rollup": "^0.2.11",
"babel-loader": "^8.1.0", "babel-loader": "^8.1.0",
"chai": "^4.2.0", "chai": "^4.2.0",
"cpx": "^1.5.0", "cpx": "^1.5.0",
@@ -195,7 +205,6 @@
"raw-loader": "^2.0.0", "raw-loader": "^2.0.0",
"require-dir": "^1.2.0", "require-dir": "^1.2.0",
"rollup": "^2.8.2", "rollup": "^2.8.2",
"rollup-plugin-babel": "^4.4.0",
"rollup-plugin-string": "^3.0.0", "rollup-plugin-string": "^3.0.0",
"rollup-plugin-terser": "^5.3.0", "rollup-plugin-terser": "^5.3.0",
"rollup-plugin-visualizer": "^4.0.4", "rollup-plugin-visualizer": "^4.0.4",

View File

@@ -1,40 +0,0 @@
{
"entrypoint": "index.html",
"shell": "src/entrypoints/app.js",
"fragments": [
"src/panels/config/ha-panel-config.js",
"src/panels/dev-event/ha-panel-dev-event.js",
"src/panels/dev-info/ha-panel-dev-info.js",
"src/panels/dev-mqtt/ha-panel-dev-mqtt.js",
"src/panels/dev-service/ha-panel-dev-service.js",
"src/panels/dev-state/ha-panel-dev-state.js",
"src/panels/dev-template/ha-panel-dev-template.js",
"src/panels/history/ha-panel-history.js",
"src/panels/iframe/ha-panel-iframe.js",
"src/panels/logbook/ha-panel-logbook.js",
"src/panels/map/ha-panel-map.js",
"src/panels/mailbox/ha-panel-mailbox.js",
"hassio/src/entrypoint.js"
],
"sources": ["src/**/*", "!src/translations/*"],
"lint": {
"rules": ["polymer-3"],
"ignoreWarnings": ["could-not-resolve-reference", "could-not-load"],
"filesToIgnore": [
"**/*.html",
"**/src/panels/config/js/**/*.js",
"**/ha-iconset-svg.js",
"**/ha-script-editor.js",
"**/ha-automation-editor.js",
"**/ha-big-calendar.js"
]
},
"builds": [
{
"preset": "es5-bundled"
},
{
"preset": "es6-bundled"
}
]
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 30 KiB

View File

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

View File

@@ -18,7 +18,7 @@ import "./ha-auth-flow";
import { extractSearchParamsObject } from "../common/url/search-params"; import { extractSearchParamsObject } from "../common/url/search-params";
import punycode from "punycode"; import punycode from "punycode";
import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider"); import("./ha-pick-auth-provider");
class HaAuthorize extends litLocalizeLiteMixin(LitElement) { class HaAuthorize extends litLocalizeLiteMixin(LitElement) {
@property() public clientId?: string; @property() public clientId?: string;

View File

@@ -22,3 +22,8 @@ export const rgbContrast = (
return (lum2 + 0.05) / (lum1 + 0.05); return (lum2 + 0.05) / (lum1 + 0.05);
}; };
export const getRGBContrastRatio = (
rgb1: [number, number, number],
rgb2: [number, number, number]
) => Math.round((rgbContrast(rgb1, rgb2) + Number.EPSILON) * 100) / 100;

View File

@@ -1,5 +1,6 @@
import { Theme } from "../../data/ws-themes";
import { darkStyles, derivedStyles } from "../../resources/styles"; import { darkStyles, derivedStyles } from "../../resources/styles";
import { HomeAssistant, Theme } from "../../types"; import type { HomeAssistant } from "../../types";
import { import {
hex2rgb, hex2rgb,
lab2hex, lab2hex,
@@ -13,10 +14,10 @@ import { rgbContrast } from "../color/rgb";
interface ProcessedTheme { interface ProcessedTheme {
keys: { [key: string]: "" }; keys: { [key: string]: "" };
styles: { [key: string]: string }; styles: Record<string, string>;
} }
let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {}; let PROCESSED_THEMES: Record<string, ProcessedTheme> = {};
/** /**
* Apply a theme to an element by setting the CSS variables on it. * Apply a theme to an element by setting the CSS variables on it.

View File

@@ -1,7 +1,7 @@
import { directive, NodePart, Part } from "lit-html"; import { directive, NodePart, Part } from "lit-html";
export const dynamicElement = directive( export const dynamicElement = directive(
(tag: string, properties?: { [key: string]: any }) => (part: Part): void => { (tag: string, properties?: Record<string, any>) => (part: Part): void => {
if (!(part instanceof NodePart)) { if (!(part instanceof NodePart)) {
throw new Error( throw new Error(
"dynamicElementDirective can only be used in content bindings" "dynamicElementDirective can only be used in content bindings"

View File

@@ -13,13 +13,12 @@ export const setupLeafletMap = async (
throw new Error("Cannot setup Leaflet map on disconnected element"); throw new Error("Cannot setup Leaflet map on disconnected element");
} }
// eslint-disable-next-line // eslint-disable-next-line
const Leaflet = ((await import( const Leaflet = ((await import("leaflet")) as any)
/* webpackChunkName: "leaflet" */ "leaflet" .default as LeafletModuleType;
)) as any).default as LeafletModuleType;
Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/";
if (draw) { if (draw) {
await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw"); await import("leaflet-draw");
} }
const map = Leaflet.map(mapElement); const map = Leaflet.map(mapElement);

View File

@@ -0,0 +1,6 @@
export const ensureArray = (value?: any) => {
if (!value || Array.isArray(value)) {
return value;
}
return [value];
};

View File

@@ -5,7 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time";
import { formatTime } from "../datetime/format_time"; import { formatTime } from "../datetime/format_time";
import { LocalizeFunc } from "../translations/localize"; import { LocalizeFunc } from "../translations/localize";
import { computeStateDomain } from "./compute_state_domain"; import { computeStateDomain } from "./compute_state_domain";
import { numberFormat } from "../string/number-format"; import { formatNumber } from "../string/format_number";
export const computeStateDisplay = ( export const computeStateDisplay = (
localize: LocalizeFunc, localize: LocalizeFunc,
@@ -20,7 +20,7 @@ export const computeStateDisplay = (
} }
if (stateObj.attributes.unit_of_measurement) { if (stateObj.attributes.unit_of_measurement) {
return `${numberFormat(compareState, language)} ${ return `${formatNumber(compareState, language)} ${
stateObj.attributes.unit_of_measurement stateObj.attributes.unit_of_measurement
}`; }`;
} }
@@ -67,6 +67,10 @@ export const computeStateDisplay = (
} }
} }
if (domain === "counter") {
return formatNumber(compareState, language);
}
return ( return (
// Return device class translation // Return device class translation
(stateObj.attributes.device_class && (stateObj.attributes.device_class &&

View File

@@ -1,5 +1,5 @@
import { HassEntities } from "home-assistant-js-websocket"; import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../types"; import type { GroupEntity } from "../../data/group";
import { DEFAULT_VIEW_ENTITY_ID } from "../const"; import { DEFAULT_VIEW_ENTITY_ID } from "../const";
// Return an ordered array of available views // Return an ordered array of available views

View File

@@ -1,5 +1,5 @@
import { HassEntities } from "home-assistant-js-websocket"; import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../types"; import { GroupEntity } from "../../data/group";
export const getGroupEntities = ( export const getGroupEntities = (
entities: HassEntities, entities: HassEntities,

View File

@@ -1,5 +1,5 @@
import { HassEntities } from "home-assistant-js-websocket"; import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../types"; import { GroupEntity } from "../../data/group";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
import { getGroupEntities } from "./get_group_entities"; import { getGroupEntities } from "./get_group_entities";

View File

@@ -1,5 +1,5 @@
import { HassEntities } from "home-assistant-js-websocket"; import { HassEntities } from "home-assistant-js-websocket";
import { GroupEntity } from "../../types"; import { GroupEntity } from "../../data/group";
import { computeDomain } from "./compute_domain"; import { computeDomain } from "./compute_domain";
// Split a collection into a list of groups and a 'rest' list of ungrouped // Split a collection into a list of groups and a 'rest' list of ungrouped

View File

@@ -0,0 +1,132 @@
import Vibrant from "node-vibrant/lib/browser";
import MMCQ from "@vibrant/quantizer-mmcq";
import { BasicPipeline } from "@vibrant/core/lib/pipeline";
import { Swatch, Vec3 } from "@vibrant/color";
import { getRGBContrastRatio } from "../color/rgb";
const CONTRAST_RATIO = 4.5;
// How much the total diff between 2 RGB colors can be
// to be considered similar.
const COLOR_SIMILARITY_THRESHOLD = 150;
// For debug purposes, is being tree shaken.
const DEBUG_COLOR = __DEV__ && false;
const logColor = (
color: Swatch,
label = `${color.hex} - ${color.population}`
) =>
// eslint-disable-next-line no-console
console.log(
`%c${label}`,
`color: ${color.bodyTextColor}; background-color: ${color.hex}`
);
const customGenerator = (colors: Swatch[]) => {
colors.sort((colorA, colorB) => colorB.population - colorA.population);
const backgroundColor = colors[0];
let foregroundColor: Vec3 | undefined;
const contrastRatios = new Map<string, number>();
const approvedContrastRatio = (hex: string, rgb: Swatch["rgb"]) => {
if (!contrastRatios.has(hex)) {
contrastRatios.set(hex, getRGBContrastRatio(backgroundColor.rgb, rgb));
}
return contrastRatios.get(hex)! > CONTRAST_RATIO;
};
// We take each next color and find one that has better contrast.
for (let i = 1; i < colors.length && foregroundColor === undefined; i++) {
// If this color matches, score, take it.
if (approvedContrastRatio(colors[i].hex, colors[i].rgb)) {
if (DEBUG_COLOR) {
logColor(colors[i], "PICKED");
}
foregroundColor = colors[i].rgb;
break;
}
// This color has the wrong contrast ratio, but it is the right color.
// Let's find similar colors that might have the right contrast ratio
const currentColor = colors[i];
if (DEBUG_COLOR) {
logColor(colors[i], "Finding similar color with better contrast");
}
for (let j = i + 1; j < colors.length; j++) {
const compareColor = colors[j];
// difference. 0 is same, 765 max difference
const diffScore =
Math.abs(currentColor.rgb[0] - compareColor.rgb[0]) +
Math.abs(currentColor.rgb[1] - compareColor.rgb[1]) +
Math.abs(currentColor.rgb[2] - compareColor.rgb[2]);
if (DEBUG_COLOR) {
logColor(colors[j], `${colors[j].hex} - ${diffScore}`);
}
if (diffScore > COLOR_SIMILARITY_THRESHOLD) {
continue;
}
if (approvedContrastRatio(compareColor.hex, compareColor.rgb)) {
if (DEBUG_COLOR) {
logColor(compareColor, "PICKED");
}
foregroundColor = compareColor.rgb;
break;
}
}
}
if (foregroundColor === undefined) {
foregroundColor =
// @ts-expect-error
backgroundColor.getYiq() < 200 ? [255, 255, 255] : [0, 0, 0];
}
if (DEBUG_COLOR) {
// eslint-disable-next-line no-console
console.log();
// eslint-disable-next-line no-console
console.log(
"%cPicked colors",
`color: ${foregroundColor}; background-color: ${backgroundColor.hex}; font-weight: bold; padding: 16px;`
);
colors.forEach((color) => logColor(color));
// eslint-disable-next-line no-console
console.log();
}
return {
foreground: new Swatch(foregroundColor, 0),
background: backgroundColor,
};
};
Vibrant.use(
new BasicPipeline().filter
.register(
"default",
(r: number, g: number, b: number, a: number) =>
a >= 125 && !(r > 250 && g > 250 && b > 250)
)
.quantizer.register("mmcq", MMCQ)
// Our generator has different output
// @ts-expect-error
.generator.register("default", customGenerator)
);
export const extractColors = (url: string, downsampleColors = 16) =>
new Vibrant(url, {
colorCount: downsampleColors,
})
.getPalette()
.then(({ foreground, background }) => {
return { background: background!, foreground: foreground! };
});

View File

@@ -0,0 +1,54 @@
/**
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
*
* @param num The number to format
* @param language The language to use when formatting the number
*/
export const formatNumber = (
num: string | number,
language: string,
options?: Intl.NumberFormatOptions
): string => {
// Polyfill for Number.isNaN, which is more reliable than the global isNaN()
Number.isNaN =
Number.isNaN ||
function isNaN(input) {
return typeof input === "number" && isNaN(input);
};
if (!Number.isNaN(Number(num)) && Intl) {
return new Intl.NumberFormat(
language,
getDefaultFormatOptions(num, options)
).format(Number(num));
}
return num.toString();
};
/**
* Generates default options for Intl.NumberFormat
* @param num The number to be formatted
* @param options The Intl.NumberFormatOptions that should be included in the returned options
*/
const getDefaultFormatOptions = (
num: string | number,
options?: Intl.NumberFormatOptions
): Intl.NumberFormatOptions => {
const defaultOptions: Intl.NumberFormatOptions = options || {};
if (typeof num !== "string") {
return defaultOptions;
}
// Keep decimal trailing zeros if they are present in a string numeric value
if (
!options ||
(!options.minimumFractionDigits && !options.maximumFractionDigits)
) {
const digits = num.indexOf(".") > -1 ? num.split(".")[1].length : 0;
defaultOptions.minimumFractionDigits = digits;
defaultOptions.maximumFractionDigits = digits;
}
return defaultOptions;
};

View File

@@ -1,22 +0,0 @@
/**
* Formats a number based on the specified language with thousands separator(s) and decimal character for better legibility.
*
* @param num The number to format
* @param language The language to use when formatting the number
*/
export const numberFormat = (
num: string | number,
language: string
): string => {
// Polyfill for Number.isNaN, which is more reliable that the global isNaN()
Number.isNaN =
Number.isNaN ||
function isNaN(input) {
return typeof input === "number" && isNaN(input);
};
if (!Number.isNaN(Number(num)) && Intl) {
return new Intl.NumberFormat(language).format(Number(num));
}
return num.toString();
};

View File

@@ -102,7 +102,7 @@ export const computeLocalize = async (
export const localizeKey = ( export const localizeKey = (
localize: LocalizeFunc, localize: LocalizeFunc,
key: string, key: string,
placeholders?: { [key: string]: string } placeholders?: Record<string, string>
) => { ) => {
const args: [string, ...string[]] = [key]; const args: [string, ...string[]] = [key];
if (placeholders) { if (placeholders) {

View File

@@ -1,4 +1,4 @@
export const extractSearchParamsObject = (): { [key: string]: string } => { export const extractSearchParamsObject = (): Record<string, string> => {
const query = {}; const query = {};
const searchParams = new URLSearchParams(location.search); const searchParams = new URLSearchParams(location.search);
for (const [key, value] of searchParams.entries()) { for (const [key, value] of searchParams.entries()) {

View File

@@ -1,8 +1,12 @@
export const copyToClipboard = (str) => { export const copyToClipboard = (str) => {
const el = document.createElement("textarea"); if (navigator.clipboard) {
el.value = str; navigator.clipboard.writeText(str);
document.body.appendChild(el); } else {
el.select(); const el = document.createElement("textarea");
document.execCommand("copy"); el.value = str;
document.body.removeChild(el); document.body.appendChild(el);
el.select();
document.execCommand("copy");
document.body.removeChild(el);
}
}; };

View File

@@ -98,6 +98,12 @@ export class HaDataTable extends LitElement {
@property({ type: Boolean }) public hasFab = false; @property({ type: Boolean }) public hasFab = false;
/**
* Add an extra rows at the bottom of the datatabel
* @type {TemplateResult}
*/
@property({ attribute: false }) public appendRow?;
@property({ type: Boolean, attribute: "auto-height" }) @property({ type: Boolean, attribute: "auto-height" })
public autoHeight = false; public autoHeight = false;
@@ -126,6 +132,8 @@ export class HaDataTable extends LitElement {
@query("slot[name='header']") private _header!: HTMLSlotElement; @query("slot[name='header']") private _header!: HTMLSlotElement;
private _items: DataTableRowData[] = [];
private _checkableRowsCount?: number; private _checkableRowsCount?: number;
private _checkedRows: string[] = []; private _checkedRows: string[] = [];
@@ -318,10 +326,13 @@ export class HaDataTable extends LitElement {
@scroll=${this._saveScrollPos} @scroll=${this._saveScrollPos}
> >
${scroll({ ${scroll({
items: !this.hasFab items: this._items,
? this._filteredData
: [...this._filteredData, ...[{ empty: true }]],
renderItem: (row: DataTableRowData, index) => { renderItem: (row: DataTableRowData, index) => {
if (row.append) {
return html`
<div class="mdc-data-table__row">${row.content}</div>
`;
}
if (row.empty) { if (row.empty) {
return html` <div class="mdc-data-table__row"></div> `; return html` <div class="mdc-data-table__row"></div> `;
} }
@@ -447,6 +458,20 @@ export class HaDataTable extends LitElement {
if (this.curRequest !== curRequest) { if (this.curRequest !== curRequest) {
return; return;
} }
if (this.appendRow || this.hasFab) {
this._items = [...data];
if (this.appendRow) {
this._items.push({ append: true, content: this.appendRow });
}
if (this.hasFab) {
this._items.push({ empty: true });
}
} else {
this._items = data;
}
this._filteredData = data; this._filteredData = data;
} }

View File

@@ -139,7 +139,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
private _filteredDevices: DeviceRegistryEntry[] = []; private _filteredDevices: DeviceRegistryEntry[] = [];
private _getDevices = memoizeOne( private _getAreasWithDevices = memoizeOne(
( (
devices: DeviceRegistryEntry[], devices: DeviceRegistryEntry[],
areas: AreaRegistryEntry[], areas: AreaRegistryEntry[],
@@ -277,7 +277,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) {
if (!this._devices || !this._areas || !this._entities) { if (!this._devices || !this._areas || !this._entities) {
return html``; return html``;
} }
const areas = this._getDevices( const areas = this._getAreasWithDevices(
this._devices, this._devices,
this._areas, this._areas,
this._entities, this._entities,

View File

@@ -1,4 +1,5 @@
import "../ha-icon-button"; import "../ha-svg-icon";
import "@material/mwc-icon-button/mwc-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@@ -12,6 +13,8 @@ import {
html, html,
LitElement, LitElement,
property, property,
PropertyValues,
query,
TemplateResult, TemplateResult,
} from "lit-element"; } from "lit-element";
import memoizeOne from "memoize-one"; import memoizeOne from "memoize-one";
@@ -35,6 +38,7 @@ import {
import { SubscribeMixin } from "../../mixins/subscribe-mixin"; import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../../polymer-types"; import { PolymerChangedEvent } from "../../polymer-types";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import { mdiClose, mdiMenuUp, mdiMenuDown } from "@mdi/js";
interface Device { interface Device {
name: string; name: string;
@@ -42,6 +46,10 @@ interface Device {
id: string; id: string;
} }
export type HaDevicePickerDeviceFilterFunc = (
device: DeviceRegistryEntry
) => boolean;
const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => { const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => {
if (!root.firstElementChild) { if (!root.firstElementChild) {
root.innerHTML = ` root.innerHTML = `
@@ -102,9 +110,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
@property({ type: Array, attribute: "include-device-classes" }) @property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[]; public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property({ type: Boolean }) @property({ type: Boolean })
private _opened?: boolean; private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
private _init = false;
private _getDevices = memoizeOne( private _getDevices = memoizeOne(
( (
devices: DeviceRegistryEntry[], devices: DeviceRegistryEntry[],
@@ -112,21 +126,31 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
entities: EntityRegistryEntry[], entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"], includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"], excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"] includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"]
): Device[] => { ): Device[] => {
if (!devices.length) { if (!devices.length) {
return []; return [
{
id: "",
area: "",
name: this.hass.localize("ui.components.device-picker.no_devices"),
},
];
} }
const deviceEntityLookup: DeviceEntityLookup = {}; const deviceEntityLookup: DeviceEntityLookup = {};
for (const entity of entities) {
if (!entity.device_id) { if (includeDomains || excludeDomains || includeDeviceClasses) {
continue; for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
} }
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
deviceEntityLookup[entity.device_id].push(entity);
} }
const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; const areaLookup: { [areaId: string]: AreaRegistryEntry } = {};
@@ -134,7 +158,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
areaLookup[area.area_id] = area; areaLookup[area.area_id] = area;
} }
let inputDevices = [...devices]; let inputDevices = devices.filter(
(device) => device.id === this.value || !device.disabled_by
);
if (includeDomains) { if (includeDomains) {
inputDevices = inputDevices.filter((device) => { inputDevices = inputDevices.filter((device) => {
@@ -180,6 +206,14 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
}); });
} }
if (deviceFilter) {
inputDevices = inputDevices.filter(
(device) =>
// We always want to include the device of the current value
device.id === this.value || deviceFilter!(device)
);
}
const outputDevices = inputDevices.map((device) => { const outputDevices = inputDevices.map((device) => {
return { return {
id: device.id, id: device.id,
@@ -193,6 +227,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
: this.hass.localize("ui.components.device-picker.no_area"), : this.hass.localize("ui.components.device-picker.no_area"),
}; };
}); });
if (!outputDevices.length) {
return [
{
id: "",
area: "",
name: this.hass.localize("ui.components.device-picker.no_match"),
},
];
}
if (outputDevices.length === 1) { if (outputDevices.length === 1) {
return outputDevices; return outputDevices;
} }
@@ -200,6 +243,18 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
} }
); );
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
}
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeDeviceRegistry(this.hass.connection!, (devices) => { subscribeDeviceRegistry(this.hass.connection!, (devices) => {
@@ -214,24 +269,33 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
]; ];
} }
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this.devices && this.areas && this.entities) ||
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this._comboBox as any).items = this._getDevices(
this.devices!,
this.areas!,
this.entities!,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter
);
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this.devices || !this.areas || !this.entities) { if (!this.devices || !this.areas || !this.entities) {
return html``; return html``;
} }
const devices = this._getDevices(
this.devices,
this.areas,
this.entities,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses
);
return html` return html`
<vaadin-combo-box-light <vaadin-combo-box-light
item-value-path="id" item-value-path="id"
item-id-path="id" item-id-path="id"
item-label-path="name" item-label-path="name"
.items=${devices}
.value=${this._value} .value=${this._value}
.renderer=${rowRenderer} .renderer=${rowRenderer}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@@ -249,34 +313,30 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
> >
${this.value ${this.value
? html` ? html`
<ha-icon-button <mwc-icon-button
aria-label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.device-picker.clear" "ui.components.device-picker.clear"
)} )}
slot="suffix" slot="suffix"
class="clear-button" class="clear-button"
icon="hass:close"
@click=${this._clearValue} @click=${this._clearValue}
no-ripple
> >
Clear <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</ha-icon-button> </mwc-icon-button>
`
: ""}
${devices.length > 0
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.device-picker.show_devices"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
Toggle
</ha-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> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
`; `;
@@ -313,7 +373,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
paper-input > ha-icon-button { paper-input > mwc-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 2px; padding: 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@@ -230,9 +230,7 @@ class HaChartBase extends mixinBehaviors(
} }
if (scriptsLoaded === null) { if (scriptsLoaded === null) {
scriptsLoaded = import( scriptsLoaded = import("../../resources/ha-chart-scripts.js");
/* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js"
);
} }
scriptsLoaded.then((ChartModule) => { scriptsLoaded.then((ChartModule) => {
this.ChartClass = ChartModule.default; this.ChartClass = ChartModule.default;

View File

@@ -101,6 +101,18 @@ export class HaEntityPicker extends LitElement {
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
}
private _initedStates = false; private _initedStates = false;
private _states: HassEntity[] = []; private _states: HassEntity[] = [];
@@ -153,6 +165,24 @@ export class HaEntityPicker extends LitElement {
); );
} }
if (!states.length) {
return [
{
entity_id: "",
state: "",
last_changed: "",
last_updated: "",
context: { id: "", user_id: null },
attributes: {
friendly_name: this.hass!.localize(
"ui.components.entity.entity-picker.no_match"
),
icon: "mdi:magnify",
},
},
];
}
return states; return states;
} }
); );
@@ -203,7 +233,6 @@ export class HaEntityPicker extends LitElement {
.label=${this.label === undefined .label=${this.label === undefined
? this.hass.localize("ui.components.entity.entity-picker.entity") ? this.hass.localize("ui.components.entity.entity-picker.entity")
: this.label} : this.label}
.value=${this._value}
.disabled=${this.disabled} .disabled=${this.disabled}
class="input" class="input"
autocapitalize="none" autocapitalize="none"

View File

@@ -21,6 +21,7 @@ import { timerTimeRemaining } from "../../common/entity/timer_time_remaining";
import { HomeAssistant } from "../../types"; import { HomeAssistant } from "../../types";
import "../ha-label-badge"; import "../ha-label-badge";
import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; import { UNAVAILABLE, UNKNOWN } from "../../data/entity";
import { formatNumber } from "../../common/string/format_number";
@customElement("ha-state-label-badge") @customElement("ha-state-label-badge")
export class HaStateLabelBadge extends LitElement { export class HaStateLabelBadge extends LitElement {
@@ -115,7 +116,7 @@ export class HaStateLabelBadge extends LitElement {
: state.state === UNKNOWN : state.state === UNKNOWN
? "-" ? "-"
: state.attributes.unit_of_measurement : state.attributes.unit_of_measurement
? state.state ? formatNumber(state.state, this.hass!.language)
: computeStateDisplay( : computeStateDisplay(
this.hass!.localize, this.hass!.localize,
state, state,

View File

@@ -94,7 +94,6 @@ class StateInfo extends LitElement {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
:host { :host {
@apply --paper-font-body1;
min-width: 120px; min-width: 120px;
white-space: nowrap; white-space: nowrap;
} }
@@ -118,9 +117,10 @@ class StateInfo extends LitElement {
} }
.name { .name {
@apply --paper-font-common-nowrap;
color: var(--primary-text-color); color: var(--primary-text-color);
line-height: 40px; white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.name[in-dialog], .name[in-dialog],
@@ -131,8 +131,10 @@ class StateInfo extends LitElement {
.time-ago, .time-ago,
.extra-info, .extra-info,
.extra-info > * { .extra-info > * {
@apply --paper-font-common-nowrap;
color: var(--secondary-text-color); color: var(--secondary-text-color);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
} }
.row { .row {

View File

@@ -1,4 +1,5 @@
import "./ha-icon-button"; import "./ha-svg-icon";
import "@material/mwc-icon-button/mwc-icon-button";
import "@polymer/paper-input/paper-input"; import "@polymer/paper-input/paper-input";
import "@polymer/paper-item/paper-item"; import "@polymer/paper-item/paper-item";
import "@polymer/paper-item/paper-item-body"; import "@polymer/paper-item/paper-item-body";
@@ -14,6 +15,8 @@ import {
property, property,
internalProperty, internalProperty,
TemplateResult, TemplateResult,
PropertyValues,
query,
} from "lit-element"; } from "lit-element";
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import { import {
@@ -28,6 +31,19 @@ import {
import { SubscribeMixin } from "../mixins/subscribe-mixin"; import { SubscribeMixin } from "../mixins/subscribe-mixin";
import { PolymerChangedEvent } from "../polymer-types"; import { PolymerChangedEvent } from "../polymer-types";
import { HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import memoizeOne from "memoize-one";
import {
DeviceEntityLookup,
DeviceRegistryEntry,
subscribeDeviceRegistry,
} from "../data/device_registry";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../data/entity_registry";
import { computeDomain } from "../common/entity/compute_domain";
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js";
const rowRenderer = ( const rowRenderer = (
root: HTMLElement, root: HTMLElement,
@@ -68,31 +84,252 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
@property() public value?: string; @property() public value?: string;
@property() public _areas?: AreaRegistryEntry[]; @property() public placeholder?: string;
@property({ type: Boolean, attribute: "no-add" }) @property({ type: Boolean, attribute: "no-add" })
public noAdd?: boolean; public noAdd?: boolean;
/**
* Show only areas with entities from specific domains.
* @type {Array}
* @attr include-domains
*/
@property({ type: Array, attribute: "include-domains" })
public includeDomains?: string[];
/**
* Show no areas with entities of these domains.
* @type {Array}
* @attr exclude-domains
*/
@property({ type: Array, attribute: "exclude-domains" })
public excludeDomains?: string[];
/**
* Show only areas with entities of these device classes.
* @type {Array}
* @attr include-device-classes
*/
@property({ type: Array, attribute: "include-device-classes" })
public includeDeviceClasses?: string[];
@property() public deviceFilter?: HaDevicePickerDeviceFilterFunc;
@property() public entityFilter?: (entity: EntityRegistryEntry) => boolean;
@internalProperty() private _areas?: AreaRegistryEntry[];
@internalProperty() private _devices?: DeviceRegistryEntry[];
@internalProperty() private _entities?: EntityRegistryEntry[];
@internalProperty() private _opened?: boolean; @internalProperty() private _opened?: boolean;
@query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement;
private _init = false;
public hassSubscribe(): UnsubscribeFunc[] { public hassSubscribe(): UnsubscribeFunc[] {
return [ return [
subscribeAreaRegistry(this.hass.connection!, (areas) => { subscribeAreaRegistry(this.hass.connection!, (areas) => {
this._areas = this.noAdd this._areas = areas;
? areas }),
: [ subscribeDeviceRegistry(this.hass.connection!, (devices) => {
...areas, this._devices = devices;
{ }),
area_id: "add_new", subscribeEntityRegistry(this.hass.connection!, (entities) => {
name: this.hass.localize("ui.components.area-picker.add_new"), this._entities = entities;
},
];
}), }),
]; ];
} }
public open() {
this.updateComplete.then(() => {
(this.shadowRoot?.querySelector("vaadin-combo-box-light") as any)?.open();
});
}
public focus() {
this.updateComplete.then(() => {
this.shadowRoot?.querySelector("paper-input")?.focus();
});
}
private _getAreas = memoizeOne(
(
areas: AreaRegistryEntry[],
devices: DeviceRegistryEntry[],
entities: EntityRegistryEntry[],
includeDomains: this["includeDomains"],
excludeDomains: this["excludeDomains"],
includeDeviceClasses: this["includeDeviceClasses"],
deviceFilter: this["deviceFilter"],
entityFilter: this["entityFilter"],
noAdd: this["noAdd"]
): AreaRegistryEntry[] => {
if (!areas.length) {
return [
{
area_id: "",
name: this.hass.localize("ui.components.area-picker.no_areas"),
},
];
}
const deviceEntityLookup: DeviceEntityLookup = {};
let inputDevices: DeviceRegistryEntry[] | undefined;
let inputEntities: EntityRegistryEntry[] | undefined;
if (includeDomains || excludeDomains || includeDeviceClasses) {
for (const entity of entities) {
if (!entity.device_id) {
continue;
}
if (!(entity.device_id in deviceEntityLookup)) {
deviceEntityLookup[entity.device_id] = [];
}
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);
}
if (includeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter((entity) =>
includeDomains.includes(computeDomain(entity.entity_id))
);
}
if (excludeDomains) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return true;
}
return entities.every(
(entity) =>
!excludeDomains.includes(computeDomain(entity.entity_id))
);
});
inputEntities = inputEntities!.filter(
(entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
);
}
if (includeDeviceClasses) {
inputDevices = inputDevices!.filter((device) => {
const devEntities = deviceEntityLookup[device.id];
if (!devEntities || !devEntities.length) {
return false;
}
return deviceEntityLookup[device.id].some((entity) => {
const stateObj = this.hass.states[entity.entity_id];
if (!stateObj) {
return false;
}
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
});
inputEntities = inputEntities!.filter((entity) => {
const stateObj = this.hass.states[entity.entity_id];
return (
stateObj.attributes.device_class &&
includeDeviceClasses.includes(stateObj.attributes.device_class)
);
});
}
if (deviceFilter) {
inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
}
if (entityFilter) {
inputEntities = inputEntities!.filter((entity) =>
entityFilter!(entity)
);
}
let outputAreas = areas;
let areaIds: string[] | undefined;
if (inputDevices) {
areaIds = inputDevices
.filter((device) => device.area_id)
.map((device) => device.area_id!);
}
if (inputEntities) {
areaIds = (areaIds ?? []).concat(
inputEntities
.filter((entity) => entity.area_id)
.map((entity) => entity.area_id!)
);
}
if (areaIds) {
outputAreas = areas.filter((area) => areaIds!.includes(area.area_id));
}
if (!outputAreas.length) {
outputAreas = [
{
area_id: "",
name: this.hass.localize("ui.components.area-picker.no_match"),
},
];
}
return noAdd
? outputAreas
: [
...outputAreas,
{
area_id: "add_new",
name: this.hass.localize("ui.components.area-picker.add_new"),
},
];
}
);
protected updated(changedProps: PropertyValues) {
if (
(!this._init && this._devices && this._areas && this._entities) ||
(changedProps.has("_opened") && this._opened)
) {
this._init = true;
(this._comboBox as any).items = this._getAreas(
this._areas!,
this._devices!,
this._entities!,
this.includeDomains,
this.excludeDomains,
this.includeDeviceClasses,
this.deviceFilter,
this.entityFilter,
this.noAdd
);
}
}
protected render(): TemplateResult { protected render(): TemplateResult {
if (!this._areas) { if (!this._devices || !this._areas || !this._entities) {
return html``; return html``;
} }
return html` return html`
@@ -100,7 +337,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
item-value-path="area_id" item-value-path="area_id"
item-id-path="area_id" item-id-path="area_id"
item-label-path="name" item-label-path="name"
.items=${this._areas}
.value=${this._value} .value=${this._value}
.renderer=${rowRenderer} .renderer=${rowRenderer}
@opened-changed=${this._openedChanged} @opened-changed=${this._openedChanged}
@@ -110,6 +346,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
.label=${this.label === undefined && this.hass .label=${this.label === undefined && this.hass
? this.hass.localize("ui.components.area-picker.area") ? this.hass.localize("ui.components.area-picker.area")
: this.label} : this.label}
.placeholder=${this.placeholder
? this._area(this.placeholder)?.name
: undefined}
class="input" class="input"
autocapitalize="none" autocapitalize="none"
autocomplete="off" autocomplete="off"
@@ -118,39 +357,39 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
> >
${this.value ${this.value
? html` ? html`
<ha-icon-button <mwc-icon-button
aria-label=${this.hass.localize( .label=${this.hass.localize(
"ui.components.area-picker.clear" "ui.components.area-picker.clear"
)} )}
slot="suffix" slot="suffix"
class="clear-button" class="clear-button"
icon="hass:close"
@click=${this._clearValue} @click=${this._clearValue}
no-ripple
> >
${this.hass.localize("ui.components.area-picker.clear")} <ha-svg-icon .path=${mdiClose}></ha-svg-icon>
</ha-icon-button> </mwc-icon-button>
`
: ""}
${this._areas.length > 0
? html`
<ha-icon-button
aria-label=${this.hass.localize(
"ui.components.area-picker.show_areas"
)}
slot="suffix"
class="toggle-button"
.icon=${this._opened ? "hass:menu-up" : "hass:menu-down"}
>
${this.hass.localize("ui.components.area-picker.toggle")}
</ha-icon-button>
` `
: ""} : ""}
<mwc-icon-button
.label=${this.hass.localize("ui.components.area-picker.toggle")}
slot="suffix"
class="toggle-button"
>
<ha-svg-icon
.path=${this._opened ? mdiMenuUp : mdiMenuDown}
></ha-svg-icon>
</mwc-icon-button>
</paper-input> </paper-input>
</vaadin-combo-box-light> </vaadin-combo-box-light>
`; `;
} }
private _area = memoizeOne((areaId: string):
| AreaRegistryEntry
| undefined => {
return this._areas?.find((area) => area.area_id === areaId);
});
private _clearValue(ev: Event) { private _clearValue(ev: Event) {
ev.stopPropagation(); ev.stopPropagation();
this._setValue(""); this._setValue("");
@@ -215,7 +454,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) {
static get styles(): CSSResult { static get styles(): CSSResult {
return css` return css`
paper-input > ha-icon-button { paper-input > mwc-icon-button {
--mdc-icon-button-size: 24px; --mdc-icon-button-size: 24px;
padding: 2px; padding: 2px;
color: var(--secondary-text-color); color: var(--secondary-text-color);

View File

@@ -107,7 +107,7 @@ class HaAttributes extends LitElement {
(!Array.isArray(value) && value instanceof Object) (!Array.isArray(value) && value instanceof Object)
) { ) {
if (!jsYamlPromise) { if (!jsYamlPromise) {
jsYamlPromise = import(/* webpackChunkName: "js-yaml" */ "js-yaml"); jsYamlPromise = import("js-yaml");
} }
const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value)); const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value));
return html` <pre>${until(yaml, "")}</pre> `; return html` <pre>${until(yaml, "")}</pre> `;

View File

@@ -0,0 +1,125 @@
import "@polymer/paper-dropdown-menu/paper-dropdown-menu-light";
import "@polymer/paper-item/paper-item";
import "@polymer/paper-listbox/paper-listbox";
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
TemplateResult,
} from "lit-element";
import memoizeOne from "memoize-one";
import { fireEvent } from "../common/dom/fire_event";
import { compare } from "../common/string/compare";
import { Blueprint, Blueprints, fetchBlueprints } from "../data/blueprint";
import { HomeAssistant } from "../types";
@customElement("ha-blueprint-picker")
class HaBluePrintPicker extends LitElement {
public hass?: HomeAssistant;
@property() public label?: string;
@property() public value = "";
@property() public domain = "automation";
@property() public blueprints?: Blueprints;
@property({ type: Boolean }) public disabled = false;
private _processedBlueprints = memoizeOne((blueprints?: Blueprints) => {
if (!blueprints) {
return [];
}
const result = Object.entries(blueprints)
.filter(([_path, blueprint]) => !("error" in blueprint))
.map(([path, blueprint]) => ({
...(blueprint as Blueprint).metadata,
path,
}));
return result.sort((a, b) => compare(a.name, b.name));
});
protected render(): TemplateResult {
if (!this.hass) {
return html``;
}
return html`
<paper-dropdown-menu-light
.label=${this.label ||
this.hass.localize("ui.components.blueprint-picker.label")}
.disabled=${this.disabled}
horizontal-align="left"
>
<paper-listbox
slot="dropdown-content"
.selected=${this.value}
attr-for-selected="data-blueprint-path"
@iron-select=${this._blueprintChanged}
>
<paper-item data-blueprint-path="">
${this.hass.localize(
"ui.components.blueprint-picker.select_blueprint"
)}
</paper-item>
${this._processedBlueprints(this.blueprints).map(
(blueprint) => html`
<paper-item data-blueprint-path=${blueprint.path}>
${blueprint.name}
</paper-item>
`
)}
</paper-listbox>
</paper-dropdown-menu-light>
`;
}
protected firstUpdated(changedProps) {
super.firstUpdated(changedProps);
if (this.blueprints === undefined) {
fetchBlueprints(this.hass!, this.domain).then((blueprints) => {
this.blueprints = blueprints;
});
}
}
private _blueprintChanged(ev) {
const newValue = ev.detail.item.dataset.blueprintPath;
if (newValue !== this.value) {
this.value = ev.detail.value;
setTimeout(() => {
fireEvent(this, "value-changed", { value: newValue });
fireEvent(this, "change");
}, 0);
}
}
static get styles(): CSSResult {
return css`
:host {
display: inline-block;
}
paper-dropdown-menu-light {
width: 100%;
min-width: 200px;
display: block;
}
paper-listbox {
min-width: 200px;
}
paper-item {
cursor: pointer;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-blueprint-picker": HaBluePrintPicker;
}
}

View File

@@ -11,6 +11,7 @@ import {
import { fireEvent } from "../common/dom/fire_event"; import { fireEvent } from "../common/dom/fire_event";
import type { ToggleButton } from "../types"; import type { ToggleButton } from "../types";
import "./ha-svg-icon"; import "./ha-svg-icon";
import "@material/mwc-button/mwc-button";
@customElement("ha-button-toggle-group") @customElement("ha-button-toggle-group")
export class HaButtonToggleGroup extends LitElement { export class HaButtonToggleGroup extends LitElement {
@@ -21,17 +22,22 @@ export class HaButtonToggleGroup extends LitElement {
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div> <div>
${this.buttons.map( ${this.buttons.map((button) =>
(button) => html` button.iconPath
<mwc-icon-button ? html`<mwc-icon-button
.label=${button.label} .label=${button.label}
.value=${button.value} .value=${button.value}
?active=${this.active === button.value} ?active=${this.active === button.value}
@click=${this._handleClick} @click=${this._handleClick}
> >
<ha-svg-icon .path=${button.iconPath}></ha-svg-icon> <ha-svg-icon .path=${button.iconPath}></ha-svg-icon>
</mwc-icon-button> </mwc-icon-button>`
` : html`<mwc-button
.value=${button.value}
?active=${this.active === button.value}
@click=${this._handleClick}
>${button.label}</mwc-button
>`
)} )}
</div> </div>
`; `;
@@ -49,13 +55,15 @@ export class HaButtonToggleGroup extends LitElement {
--mdc-icon-button-size: var(--button-toggle-size, 36px); --mdc-icon-button-size: var(--button-toggle-size, 36px);
--mdc-icon-size: var(--button-toggle-icon-size, 20px); --mdc-icon-size: var(--button-toggle-icon-size, 20px);
} }
mwc-icon-button { mwc-icon-button,
mwc-button {
border: 1px solid var(--primary-color); border: 1px solid var(--primary-color);
border-right-width: 0px; border-right-width: 0px;
position: relative; position: relative;
cursor: pointer; cursor: pointer;
} }
mwc-icon-button::before { mwc-icon-button::before,
mwc-button::before {
top: 0; top: 0;
left: 0; left: 0;
width: 100%; width: 100%;
@@ -67,17 +75,21 @@ export class HaButtonToggleGroup extends LitElement {
content: ""; content: "";
transition: opacity 15ms linear, background-color 15ms linear; transition: opacity 15ms linear, background-color 15ms linear;
} }
mwc-icon-button[active]::before { mwc-icon-button[active]::before,
mwc-button[active]::before {
opacity: var(--mdc-icon-button-ripple-opacity, 0.12); opacity: var(--mdc-icon-button-ripple-opacity, 0.12);
} }
mwc-icon-button:first-child { mwc-icon-button:first-child,
mwc-button:first-child {
border-radius: 4px 0 0 4px; border-radius: 4px 0 0 4px;
} }
mwc-icon-button:last-child { mwc-icon-button:last-child,
mwc-button:last-child {
border-radius: 0 4px 4px 0; border-radius: 0 4px 4px 0;
border-right-width: 1px; border-right-width: 1px;
} }
mwc-icon-button:only-child { mwc-icon-button:only-child,
mwc-button:only-child {
border-radius: 4px; border-radius: 4px;
border-right-width: 1px; border-right-width: 1px;
} }

View File

@@ -13,11 +13,12 @@ import { fireEvent } from "../common/dom/fire_event";
import { computeStateName } from "../common/entity/compute_state_name"; import { computeStateName } from "../common/entity/compute_state_name";
import { supportsFeature } from "../common/entity/supports-feature"; import { supportsFeature } from "../common/entity/supports-feature";
import { import {
CameraEntity,
CAMERA_SUPPORT_STREAM, CAMERA_SUPPORT_STREAM,
computeMJPEGStreamUrl, computeMJPEGStreamUrl,
fetchStreamUrl, fetchStreamUrl,
} from "../data/camera"; } from "../data/camera";
import { CameraEntity, HomeAssistant } from "../types"; import { HomeAssistant } from "../types";
import "./ha-hls-player"; import "./ha-hls-player";
@customElement("ha-camera-stream") @customElement("ha-camera-stream")

View File

@@ -11,6 +11,7 @@ import { HassEntity } from "home-assistant-js-websocket";
import { CLIMATE_PRESET_NONE } from "../data/climate"; import { CLIMATE_PRESET_NONE } from "../data/climate";
import type { HomeAssistant } from "../types"; import type { HomeAssistant } from "../types";
import { formatNumber } from "../common/string/format_number";
@customElement("ha-climate-state") @customElement("ha-climate-state")
class HaClimateState extends LitElement { class HaClimateState extends LitElement {
@@ -51,11 +52,17 @@ class HaClimateState extends LitElement {
} }
if (this.stateObj.attributes.current_temperature != null) { if (this.stateObj.attributes.current_temperature != null) {
return `${this.stateObj.attributes.current_temperature} ${this.hass.config.unit_system.temperature}`; return `${formatNumber(
this.stateObj.attributes.current_temperature,
this.hass!.language
)} ${this.hass.config.unit_system.temperature}`;
} }
if (this.stateObj.attributes.current_humidity != null) { if (this.stateObj.attributes.current_humidity != null) {
return `${this.stateObj.attributes.current_humidity} %`; return `${formatNumber(
this.stateObj.attributes.current_humidity,
this.hass!.language
)} %`;
} }
return undefined; return undefined;
@@ -70,21 +77,39 @@ class HaClimateState extends LitElement {
this.stateObj.attributes.target_temp_low != null && this.stateObj.attributes.target_temp_low != null &&
this.stateObj.attributes.target_temp_high != null this.stateObj.attributes.target_temp_high != null
) { ) {
return `${this.stateObj.attributes.target_temp_low}-${this.stateObj.attributes.target_temp_high} ${this.hass.config.unit_system.temperature}`; return `${formatNumber(
this.stateObj.attributes.target_temp_low,
this.hass!.language
)}-${formatNumber(
this.stateObj.attributes.target_temp_high,
this.hass!.language
)} ${this.hass.config.unit_system.temperature}`;
} }
if (this.stateObj.attributes.temperature != null) { if (this.stateObj.attributes.temperature != null) {
return `${this.stateObj.attributes.temperature} ${this.hass.config.unit_system.temperature}`; return `${formatNumber(
this.stateObj.attributes.temperature,
this.hass!.language
)} ${this.hass.config.unit_system.temperature}`;
} }
if ( if (
this.stateObj.attributes.target_humidity_low != null && this.stateObj.attributes.target_humidity_low != null &&
this.stateObj.attributes.target_humidity_high != null this.stateObj.attributes.target_humidity_high != null
) { ) {
return `${this.stateObj.attributes.target_humidity_low}-${this.stateObj.attributes.target_humidity_high}%`; return `${formatNumber(
this.stateObj.attributes.target_humidity_low,
this.hass!.language
)}-${formatNumber(
this.stateObj.attributes.target_humidity_high,
this.hass!.language
)}%`;
} }
if (this.stateObj.attributes.humidity != null) { if (this.stateObj.attributes.humidity != null) {
return `${this.stateObj.attributes.humidity} %`; return `${formatNumber(
this.stateObj.attributes.humidity,
this.hass!.language
)} %`;
} }
return ""; return "";

View File

@@ -41,7 +41,7 @@ class HaCoverTiltControls extends LitElement {
return html` <ha-icon-button return html` <ha-icon-button
class=${classMap({ class=${classMap({
invisible: !this._entityObj.supportsStop, invisible: !this._entityObj.supportsOpenTilt,
})} })}
label=${this.hass.localize( label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover" "ui.dialogs.more_info_control.open_tilt_cover"
@@ -61,10 +61,10 @@ class HaCoverTiltControls extends LitElement {
></ha-icon-button> ></ha-icon-button>
<ha-icon-button <ha-icon-button
class=${classMap({ class=${classMap({
invisible: !this._entityObj.supportsStop, invisible: !this._entityObj.supportsCloseTilt,
})} })}
label=${this.hass.localize( label=${this.hass.localize(
"ui.dialogs.more_info_control.open_tilt_cover" "ui.dialogs.more_info_control.close_tilt_cover"
)} )}
icon="hass:arrow-bottom-left" icon="hass:arrow-bottom-left"
@click=${this._onCloseTiltTap} @click=${this._onCloseTiltTap}

View File

@@ -19,12 +19,14 @@ class HaExpansionPanel extends LitElement {
@property({ type: Boolean, reflect: true }) outlined = false; @property({ type: Boolean, reflect: true }) outlined = false;
@property() header?: string;
@query(".container") private _container!: HTMLDivElement; @query(".container") private _container!: HTMLDivElement;
protected render(): TemplateResult { protected render(): TemplateResult {
return html` return html`
<div class="summary" @click=${this._toggleContainer}> <div class="summary" @click=${this._toggleContainer}>
<slot name="title"></slot> <slot name="header">${this.header}</slot>
<ha-svg-icon <ha-svg-icon
.path=${mdiChevronDown} .path=${mdiChevronDown}
class="summary-icon ${classMap({ expanded: this.expanded })}" class="summary-icon ${classMap({ expanded: this.expanded })}"
@@ -76,7 +78,7 @@ class HaExpansionPanel extends LitElement {
.summary { .summary {
display: flex; display: flex;
padding: 0px 16px; padding: var(--expansion-panel-summary-padding, 0);
min-height: 48px; min-height: 48px;
align-items: center; align-items: center;
cursor: pointer; cursor: pointer;

20
src/components/ha-fab.ts Normal file
View File

@@ -0,0 +1,20 @@
import type { Fab } from "@material/mwc-fab";
import "@material/mwc-fab";
import { customElement } from "lit-element";
import { Constructor } from "../types";
const MwcFab = customElements.get("mwc-fab") as Constructor<Fab>;
@customElement("ha-fab")
export class HaFab extends MwcFab {
protected firstUpdated(changedProperties) {
super.firstUpdated(changedProperties);
this.style.setProperty("--mdc-theme-secondary", "var(--primary-color)");
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-fab": HaFab;
}
}

View File

@@ -54,7 +54,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema {
export interface HaFormMultiSelectSchema extends HaFormBaseSchema { export interface HaFormMultiSelectSchema extends HaFormBaseSchema {
type: "multi_select"; type: "multi_select";
options?: { [key: string]: string } | string[] | Array<[string, string]>; options?: Record<string, string> | string[] | Array<[string, string]>;
} }
export interface HaFormFloatSchema extends HaFormBaseSchema { export interface HaFormFloatSchema extends HaFormBaseSchema {

View File

@@ -12,6 +12,7 @@ import { afterNextRender } from "../common/util/render-status";
import { ifDefined } from "lit-html/directives/if-defined"; import { ifDefined } from "lit-html/directives/if-defined";
import { getValueInPercentage, normalize } from "../util/calculate"; import { getValueInPercentage, normalize } from "../util/calculate";
import { formatNumber } from "../common/string/format_number";
const getAngle = (value: number, min: number, max: number) => { const getAngle = (value: number, min: number, max: number) => {
const percentage = getValueInPercentage(normalize(value, min, max), min, max); const percentage = getValueInPercentage(normalize(value, min, max), min, max);
@@ -29,6 +30,8 @@ export class Gauge extends LitElement {
@property({ type: Number }) public value = 0; @property({ type: Number }) public value = 0;
@property({ type: String }) public language = "";
@property() public label = ""; @property() public label = "";
@internalProperty() private _angle = 0; @internalProperty() private _angle = 0;
@@ -88,7 +91,7 @@ export class Gauge extends LitElement {
</svg> </svg>
<svg class="text"> <svg class="text">
<text class="value-text"> <text class="value-text">
${this.value} ${this.label} ${formatNumber(this.value, this.language)} ${this.label}
</text> </text>
</svg>`; </svg>`;
} }

View File

@@ -107,9 +107,7 @@ class HaHLSPlayer extends LitElement {
const useExoPlayerPromise = this._getUseExoPlayer(); const useExoPlayerPromise = this._getUseExoPlayer();
const masterPlaylistPromise = fetch(this.url); const masterPlaylistPromise = fetch(this.url);
const hls = ((await import( const hls = ((await import("hls.js")) as any).default as HLSModule;
/* webpackChunkName: "hls.js" */ "hls.js"
)) as any).default as HLSModule;
let hlsSupported = hls.isSupported(); let hlsSupported = hls.isSupported();
if (!hlsSupported) { if (!hlsSupported) {
@@ -129,7 +127,7 @@ class HaHLSPlayer extends LitElement {
// Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url // Parse playlist assuming it is a master playlist. Match group 1 is whether hevc, match group 2 is regular playlist url
// See https://tools.ietf.org/html/rfc8216 for HLS spec details // See https://tools.ietf.org/html/rfc8216 for HLS spec details
const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(?<isHevc>hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(?<streamUrl>.+)/g; const playlistRegexp = /#EXT-X-STREAM-INF:.*?(?:CODECS=".*?(hev1|hvc1)?\..*?".*?)?(?:\n|\r\n)(.+)/g;
const match = playlistRegexp.exec(masterPlaylist); const match = playlistRegexp.exec(masterPlaylist);
const matchTwice = playlistRegexp.exec(masterPlaylist); const matchTwice = playlistRegexp.exec(masterPlaylist);
@@ -138,17 +136,13 @@ class HaHLSPlayer extends LitElement {
let playlist_url: string; let playlist_url: string;
if (match !== null && matchTwice === null) { if (match !== null && matchTwice === null) {
// Only send the regular playlist url if we match exactly once // Only send the regular playlist url if we match exactly once
playlist_url = new URL(match.groups!.streamUrl, this.url).href; playlist_url = new URL(match[2], this.url).href;
} else { } else {
playlist_url = this.url; playlist_url = this.url;
} }
// If codec is HEVC and ExoPlayer is supported, use ExoPlayer. // If codec is HEVC and ExoPlayer is supported, use ExoPlayer.
if ( if (this._useExoPlayer && match !== null && match[1] !== undefined) {
this._useExoPlayer &&
match !== null &&
match.groups!.isHevc !== undefined
) {
this._renderHLSExoPlayer(playlist_url); this._renderHLSExoPlayer(playlist_url);
} else if (hls.isSupported()) { } else if (hls.isSupported()) {
this._renderHLSPolyfill(videoEl, hls, playlist_url); this._renderHLSPolyfill(videoEl, hls, playlist_url);

View File

@@ -60,8 +60,9 @@ export class HaIconInput extends LitElement {
static get styles() { static get styles() {
return css` return css`
ha-icon { ha-icon {
position: relative; position: absolute;
bottom: 4px; bottom: 2px;
right: 0;
} }
`; `;
} }

View File

@@ -39,7 +39,7 @@ checkCacheVersion();
const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); const debouncedWriteCache = debounce(() => writeCache(chunks), 2000);
const cachedIcons: { [key: string]: string } = {}; const cachedIcons: Record<string, string> = {};
@customElement("ha-icon") @customElement("ha-icon")
export class HaIcon extends LitElement { export class HaIcon extends LitElement {

View File

@@ -58,6 +58,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) {
getConfigEntries(this.hass).then((configEntries) => { getConfigEntries(this.hass).then((configEntries) => {
this._entries = configEntries; this._entries = configEntries;
}); });
this.hass.loadBackendTranslation("title");
} }
protected updated(changedProps: PropertyValues) { protected updated(changedProps: PropertyValues) {

View File

@@ -1,66 +0,0 @@
import { dom } from "@polymer/polymer/lib/legacy/polymer.dom";
/* eslint-plugin-disable lit */
import { PolymerElement } from "@polymer/polymer/polymer-element";
import relativeTime from "../common/datetime/relative_time";
import LocalizeMixin from "../mixins/localize-mixin";
/*
* @appliesMixin LocalizeMixin
*/
class HaRelativeTime extends LocalizeMixin(PolymerElement) {
static get properties() {
return {
hass: Object,
datetime: {
type: String,
observer: "datetimeChanged",
},
datetimeObj: {
type: Object,
observer: "datetimeObjChanged",
},
parsedDateTime: Object,
};
}
constructor() {
super();
this.updateRelative = this.updateRelative.bind(this);
}
connectedCallback() {
super.connectedCallback();
// update every 60 seconds
this.updateInterval = setInterval(this.updateRelative, 60000);
}
disconnectedCallback() {
super.disconnectedCallback();
clearInterval(this.updateInterval);
}
datetimeChanged(newVal) {
this.parsedDateTime = newVal ? new Date(newVal) : null;
this.updateRelative();
}
datetimeObjChanged(newVal) {
this.parsedDateTime = newVal;
this.updateRelative();
}
updateRelative() {
const root = dom(this);
if (!this.parsedDateTime) {
root.innerHTML = this.localize("ui.components.relative_time.never");
} else {
root.innerHTML = relativeTime(this.parsedDateTime, this.localize);
}
}
}
customElements.define("ha-relative-time", HaRelativeTime);

View File

@@ -0,0 +1,72 @@
import {
customElement,
UpdatingElement,
property,
PropertyValues,
} from "lit-element";
import relativeTime from "../common/datetime/relative_time";
import type { HomeAssistant } from "../types";
@customElement("ha-relative-time")
class HaRelativeTime extends UpdatingElement {
@property({ attribute: false }) public hass!: HomeAssistant;
@property({ attribute: false }) public datetime?: string | Date;
private _interval?: number;
public disconnectedCallback(): void {
super.disconnectedCallback();
this._clearInterval();
}
public connectedCallback(): void {
super.connectedCallback();
if (this.datetime) {
this._startInterval();
}
}
protected firstUpdated(changedProps: PropertyValues) {
super.firstUpdated(changedProps);
this._updateRelative();
}
protected update(changedProps: PropertyValues) {
super.update(changedProps);
this._updateRelative();
}
private _clearInterval(): void {
if (this._interval) {
window.clearInterval(this._interval);
this._interval = undefined;
}
}
private _startInterval(): void {
this._clearInterval();
// update every 60 seconds
this._interval = window.setInterval(() => this._updateRelative(), 60000);
}
private _updateRelative(): void {
if (!this.datetime) {
this.innerHTML = this.hass.localize("ui.components.relative_time.never");
} else {
this.innerHTML = relativeTime(
new Date(this.datetime),
this.hass.localize
);
}
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-relative-time": HaRelativeTime;
}
}

View File

@@ -0,0 +1,45 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { ActionSelector } from "../../data/selector";
import { Action } from "../../data/script";
import "../../panels/config/automation/action/ha-automation-action";
@customElement("ha-selector-action")
export class HaActionSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: ActionSelector;
@property() public value?: Action;
@property() public label?: string;
protected render() {
return html`<ha-automation-action
.actions=${this.value || []}
.hass=${this.hass}
></ha-automation-action>`;
}
static get styles(): CSSResult {
return css`
ha-automation-action {
display: block;
margin-bottom: 16px;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-action": HaActionSelector;
}
}

View File

@@ -0,0 +1,102 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { AreaSelector } from "../../data/selector";
import "../ha-area-picker";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry";
import { EntityRegistryEntry } from "../../data/entity_registry";
@customElement("ha-selector-area")
export class HaAreaSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: AreaSelector;
@property() public value?: any;
@property() public label?: string;
@internalProperty() public _configEntries?: ConfigEntry[];
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (
oldSelector !== this.selector &&
this.selector.area.device?.integration
) {
this._loadConfigEntries();
}
}
}
protected render() {
return html`<ha-area-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
no-add
.deviceFilter=${(device) => this._filterDevices(device)}
.entityFilter=${(entity) => this._filterEntities(entity)}
.includeDeviceClasses=${this.selector.area.entity?.device_class
? [this.selector.area.entity.device_class]
: undefined}
.includeDomains=${this.selector.area.entity?.domain
? [this.selector.area.entity.domain]
: undefined}
></ha-area-picker>`;
}
private _filterEntities(entity: EntityRegistryEntry): boolean {
if (this.selector.area.entity?.integration) {
if (entity.platform !== this.selector.area.entity.integration) {
return false;
}
}
return true;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.area.device?.manufacturer &&
device.manufacturer !== this.selector.area.device.manufacturer
) {
return false;
}
if (
this.selector.area.device?.model &&
device.model !== this.selector.area.device.model
) {
return false;
}
if (this.selector.area.device?.integration) {
if (
this._configEntries &&
!this._configEntries.some((entry) =>
device.config_entries.includes(entry.entry_id)
)
) {
return false;
}
}
return true;
}
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.area.device?.integration
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-area": HaAreaSelector;
}
}

View File

@@ -0,0 +1,54 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { fireEvent } from "../../common/dom/fire_event";
import { HomeAssistant } from "../../types";
import "../ha-formfield";
import "../ha-switch";
@customElement("ha-selector-boolean")
export class HaBooleanSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public value?: number;
@property() public label?: string;
protected render() {
return html` <ha-formfield alignEnd spaceBetween .label=${this.label}>
<ha-switch
.checked=${this.value}
@change=${this._handleChange}
></ha-switch>
</ha-formfield>`;
}
private _handleChange(ev) {
const value = ev.target.checked;
if (this.value === value) {
return;
}
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResult {
return css`
ha-formfield {
width: 100%;
margin: 16px 0;
--mdc-typography-body2-font-size: 1em;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-boolean": HaBooleanSelector;
}
}

View File

@@ -0,0 +1,88 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import "../device/ha-device-picker";
import { DeviceRegistryEntry } from "../../data/device_registry";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceSelector } from "../../data/selector";
@customElement("ha-selector-device")
export class HaDeviceSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: DeviceSelector;
@property() public value?: any;
@property() public label?: string;
@internalProperty() public _configEntries?: ConfigEntry[];
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (oldSelector !== this.selector && this.selector.device.integration) {
this._loadConfigEntries();
}
}
}
protected render() {
return html`<ha-device-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.deviceFilter=${(device) => this._filterDevices(device)}
.includeDeviceClasses=${this.selector.device.entity?.device_class
? [this.selector.device.entity.device_class]
: undefined}
.includeDomains=${this.selector.device.entity?.domain
? [this.selector.device.entity.domain]
: undefined}
allow-custom-entity
></ha-device-picker>`;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.device.manufacturer &&
device.manufacturer !== this.selector.device.manufacturer
) {
return false;
}
if (
this.selector.device.model &&
device.model !== this.selector.device.model
) {
return false;
}
if (this.selector.device.integration) {
if (
this._configEntries &&
!this._configEntries.some((entry) =>
device.config_entries.includes(entry.entry_id)
)
) {
return false;
}
}
return true;
}
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.device.integration
);
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-device": HaDeviceSelector;
}
}

View File

@@ -0,0 +1,84 @@
import {
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import "../entity/ha-entity-picker";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { computeStateDomain } from "../../common/entity/compute_state_domain";
import { subscribeEntityRegistry } from "../../data/entity_registry";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
import { EntitySelector } from "../../data/selector";
@customElement("ha-selector-entity")
export class HaEntitySelector extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public selector!: EntitySelector;
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
@property() public value?: any;
@property() public label?: string;
protected render() {
return html`<ha-entity-picker
.hass=${this.hass}
.value=${this.value}
.label=${this.label}
.entityFilter=${(entity) => this._filterEntities(entity)}
allow-custom-entity
></ha-entity-picker>`;
}
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
const entityLookup = {};
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entityPlaformLookup = entityLookup;
}),
];
}
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.entity.domain) {
if (computeStateDomain(entity) !== this.selector.entity.domain) {
return false;
}
}
if (this.selector.entity.device_class) {
if (
!entity.attributes.device_class ||
entity.attributes.device_class !== this.selector.entity.device_class
) {
return false;
}
}
if (this.selector.entity.integration) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==
this.selector.entity.integration
) {
return false;
}
}
return true;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-entity": HaEntitySelector;
}
}

View File

@@ -0,0 +1,104 @@
import {
css,
CSSResult,
customElement,
html,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { NumberSelector } from "../../data/selector";
import "@polymer/paper-input/paper-input";
import "../ha-slider";
import { fireEvent } from "../../common/dom/fire_event";
import { classMap } from "lit-html/directives/class-map";
@customElement("ha-selector-number")
export class HaNumberSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: NumberSelector;
@property() public value?: number;
@property() public label?: string;
protected render() {
return html`${this.label}
${this.selector.number.mode === "slider"
? html`<ha-slider
.min=${this.selector.number.min}
.max=${this.selector.number.max}
.value=${this._value}
.step=${this.selector.number.step}
pin
ignore-bar-touch
@change=${this._handleSliderChange}
>
</ha-slider>`
: ""}
<paper-input
pattern="[0-9]+([\\.][0-9]+)?"
.label=${this.selector.number.mode === "slider"
? undefined
: this.label}
.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}
.step=${this.selector.number.step}
type="number"
auto-validate
@value-changed=${this._handleInputChange}
>
${this.selector.number.unit_of_measurement
? html`<div slot="suffix">
${this.selector.number.unit_of_measurement}
</div>`
: ""}
</paper-input>`;
}
private get _value() {
return this.value || 0;
}
private _handleInputChange(ev) {
const value = 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) {
return;
}
fireEvent(this, "value-changed", { value });
}
static get styles(): CSSResult {
return css`
:host {
display: flex;
justify-content: space-between;
align-items: center;
}
ha-slider {
flex: 1;
}
.single {
flex: 1;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-number": HaNumberSelector;
}
}

View File

@@ -0,0 +1,153 @@
import {
css,
CSSResult,
customElement,
html,
internalProperty,
LitElement,
property,
} from "lit-element";
import { HomeAssistant } from "../../types";
import { TargetSelector } from "../../data/selector";
import { ConfigEntry, getConfigEntries } from "../../data/config_entries";
import { DeviceRegistryEntry } from "../../data/device_registry";
import "../ha-target-picker";
import "@material/mwc-list/mwc-list-item";
import "@polymer/paper-input/paper-input";
import "@material/mwc-list/mwc-list";
import {
EntityRegistryEntry,
subscribeEntityRegistry,
} from "../../data/entity_registry";
import { Target } from "../../data/target";
import "@material/mwc-tab-bar/mwc-tab-bar";
import "@material/mwc-tab/mwc-tab";
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
import { SubscribeMixin } from "../../mixins/subscribe-mixin";
@customElement("ha-selector-target")
export class HaTargetSelector extends SubscribeMixin(LitElement) {
@property() public hass!: HomeAssistant;
@property() public selector!: TargetSelector;
@property() public value?: Target;
@property() public label?: string;
@internalProperty() private _entityPlaformLookup?: Record<string, string>;
@internalProperty() private _configEntries?: ConfigEntry[];
public hassSubscribe(): UnsubscribeFunc[] {
return [
subscribeEntityRegistry(this.hass.connection!, (entities) => {
const entityLookup = {};
for (const confEnt of entities) {
if (!confEnt.platform) {
continue;
}
entityLookup[confEnt.entity_id] = confEnt.platform;
}
this._entityPlaformLookup = entityLookup;
}),
];
}
protected updated(changedProperties) {
if (changedProperties.has("selector")) {
const oldSelector = changedProperties.get("selector");
if (
oldSelector !== this.selector &&
this.selector.target.device?.integration
) {
this._loadConfigEntries();
}
}
}
protected render() {
return html`<ha-target-picker
.hass=${this.hass}
.value=${this.value}
.deviceFilter=${(device) => this._filterDevices(device)}
.entityRegFilter=${(entity: EntityRegistryEntry) =>
this._filterRegEntities(entity)}
.entityFilter=${(entity: HassEntity) => this._filterEntities(entity)}
.includeDeviceClasses=${this.selector.target.entity?.device_class
? [this.selector.target.entity.device_class]
: undefined}
.includeDomains=${this.selector.target.entity?.domain
? [this.selector.target.entity.domain]
: undefined}
></ha-target-picker>`;
}
private _filterEntities(entity: HassEntity): boolean {
if (this.selector.target.entity?.integration) {
if (
!this._entityPlaformLookup ||
this._entityPlaformLookup[entity.entity_id] !==
this.selector.target.entity.integration
) {
return false;
}
}
return true;
}
private _filterRegEntities(entity: EntityRegistryEntry): boolean {
if (this.selector.target.entity?.integration) {
if (entity.platform !== this.selector.target.entity.integration) {
return false;
}
}
return true;
}
private _filterDevices(device: DeviceRegistryEntry): boolean {
if (
this.selector.target.device?.manufacturer &&
device.manufacturer !== this.selector.target.device.manufacturer
) {
return false;
}
if (
this.selector.target.device?.model &&
device.model !== this.selector.target.device.model
) {
return false;
}
if (this.selector.target.device?.integration) {
if (
!this._configEntries?.some((entry) =>
device.config_entries.includes(entry.entry_id)
)
) {
return false;
}
}
return true;
}
private async _loadConfigEntries() {
this._configEntries = (await getConfigEntries(this.hass)).filter(
(entry) => entry.domain === this.selector.target.device?.integration
);
}
static get styles(): CSSResult {
return css`
ha-target-picker {
margin: 0 -8px;
display: block;
}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-target": HaTargetSelector;
}
}

View File

@@ -0,0 +1,59 @@
import { customElement, html, LitElement, property } from "lit-element";
import { HomeAssistant } from "../../types";
import { TimeSelector } from "../../data/selector";
import { fireEvent } from "../../common/dom/fire_event";
import "../paper-time-input";
const test = new Date().toLocaleString();
const useAMPM = test.includes("AM") || test.includes("PM");
@customElement("ha-selector-time")
export class HaTimeSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: TimeSelector;
@property() public value?: string;
@property() public label?: string;
protected render() {
const parts = this.value?.split(":") || [];
const hours = useAMPM ? parts[0] ?? "12" : parts[0] ?? "0";
return html`
<paper-time-input
.label=${this.label}
.hour=${useAMPM && Number(hours) > 12 ? Number(hours) - 12 : hours}
.min=${parts[1] ?? "00"}
.sec=${parts[2] ?? "00"}
.format=${useAMPM ? 12 : 24}
.amPm=${useAMPM && (Number(hours) > 12 ? "PM" : "AM")}
@change=${this._timeChanged}
@am-pm-changed=${this._timeChanged}
hide-label
enable-second
></paper-time-input>
`;
}
private _timeChanged(ev) {
let value = ev.target.value;
if (useAMPM) {
let hours = Number(ev.target.hour);
if (ev.target.amPm === "PM") {
hours += 12;
}
value = `${hours}:${ev.target.min}:${ev.target.sec}`;
}
fireEvent(this, "value-changed", {
value,
});
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector-time": HaTimeSelector;
}
}

View File

@@ -0,0 +1,54 @@
import { customElement, html, LitElement, property } from "lit-element";
import { dynamicElement } from "../../common/dom/dynamic-element-directive";
import { HomeAssistant } from "../../types";
import "./ha-selector-entity";
import "./ha-selector-device";
import "./ha-selector-area";
import "./ha-selector-target";
import "./ha-selector-number";
import "./ha-selector-boolean";
import "./ha-selector-time";
import "./ha-selector-action";
import { Selector } from "../../data/selector";
@customElement("ha-selector")
export class HaSelector extends LitElement {
@property() public hass!: HomeAssistant;
@property() public selector!: Selector;
@property() public value?: any;
@property() public label?: string;
public focus() {
const input = this.shadowRoot!.getElementById("selector");
if (!input) {
return;
}
(input as HTMLElement).focus();
}
private get _type() {
return Object.keys(this.selector)[0];
}
protected render() {
return html`
${dynamicElement(`ha-selector-${this._type}`, {
hass: this.hass,
selector: this.selector,
value: this.value,
label: this.label,
id: "selector",
})}
`;
}
}
declare global {
interface HTMLElementTagNameMap {
"ha-selector": HaSelector;
}
}

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