mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-30 22:19:55 +00:00 
			
		
		
		
	Compare commits
	
		
			161 Commits
		
	
	
		
			attributes
			...
			sidebar-mw
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 5409752817 | ||
|   | a70e6c49a1 | ||
|   | 3d83d5f4b5 | ||
|   | f9dece0743 | ||
|   | ac0871d0e8 | ||
|   | ffc19e591d | ||
|   | c53380ca3d | ||
|   | 7c74a2026a | ||
|   | adaed438d9 | ||
|   | baf38305cb | ||
|   | 8254712521 | ||
|   | 53214781e3 | ||
|   | 88cbbbdf65 | ||
|   | c10dca9c7b | ||
|   | 7f2ebb4bde | ||
|   | f1abb60e4a | ||
|   | e014c7aff6 | ||
|   | b79c03433e | ||
|   | 34eb4d974d | ||
|   | 3264be3c5e | ||
|   | 655f4f75fb | ||
|   | 4383f31696 | ||
|   | 99eb15d15e | ||
|   | 2682c6e150 | ||
|   | 3a5d854e6d | ||
|   | 1e90c6387c | ||
|   | 2cca25f4d0 | ||
|   | 565724d201 | ||
|   | 3e4955becd | ||
|   | 7b560c727f | ||
|   | 35abd9dfdb | ||
|   | b7ccf3e0e5 | ||
|   | 0d9ab8fdd0 | ||
|   | 303f9290a8 | ||
|   | e0c4dc08a1 | ||
|   | 8c655883fe | ||
|   | ba90785115 | ||
|   | 7b392b626b | ||
|   | 8e4ceb7d48 | ||
|   | 2ab1c6e9a9 | ||
|   | dbdced0971 | ||
|   | 5e481880bd | ||
|   | faec063f34 | ||
|   | bbea38d227 | ||
|   | a0ef60de49 | ||
|   | 3313572606 | ||
|   | c4f850cb14 | ||
|   | 3bdab738c6 | ||
|   | faaef31b9f | ||
|   | ca7b8b8b4c | ||
|   | 9ca84e0694 | ||
|   | daaf2b1796 | ||
|   | 25f7cbea5a | ||
|   | c485ea9d7b | ||
|   | 295390c8e9 | ||
|   | 3ebf816ce2 | ||
|   | 0e362b851b | ||
|   | 8d7ba19a08 | ||
|   | 08f4aa9d10 | ||
|   | 98175d5c72 | ||
|   | 7d4cad90bc | ||
|   | 335354d962 | ||
|   | fe31d15d27 | ||
|   | 7ceb6eb50d | ||
|   | 4c4db46aa8 | ||
|   | b5724ed343 | ||
|   | cae94175fe | ||
|   | 0494a9d410 | ||
|   | c261b5c1ce | ||
|   | c89e17ac00 | ||
|   | 50e7410002 | ||
|   | c5b0ebf76d | ||
|   | 1d08978d6c | ||
|   | fc78b6c933 | ||
|   | 480a5718fc | ||
|   | f093bd115c | ||
|   | 8a86beff14 | ||
|   | 6020890384 | ||
|   | 124aa947e2 | ||
|   | e1add14453 | ||
|   | e3293837a8 | ||
|   | 5cb2743780 | ||
|   | 6f0c79ec25 | ||
|   | 7de7d1d926 | ||
|   | 89175f8e85 | ||
|   | fc48c59eb0 | ||
|   | 51332bc7e7 | ||
|   | 7403405d12 | ||
|   | 1d13947e71 | ||
|   | f6cb1ffe20 | ||
|   | 6d92b5651a | ||
|   | 3ea5bb2a6c | ||
|   | 1d367eca69 | ||
|   | d4bf3a2ec3 | ||
|   | 0ef8881660 | ||
|   | d7d1121f7d | ||
|   | 7f089c309f | ||
|   | 4dcc0bb66c | ||
|   | 0049be7feb | ||
|   | 39ff641be9 | ||
|   | e2fed24995 | ||
|   | c0aa353f83 | ||
|   | d8521be63d | ||
|   | 6d4569c89d | ||
|   | cd07553b59 | ||
|   | 641bfcc9f7 | ||
|   | 6c01371958 | ||
|   | 7b00260b1a | ||
|   | 875142373e | ||
|   | ba505b15ef | ||
|   | 17d227b142 | ||
|   | e7e192ffe3 | ||
|   | c53ec6e12d | ||
|   | aad6492a6a | ||
|   | fd5b125c2d | ||
|   | 5acee76c70 | ||
|   | 10916fa82a | ||
|   | f69951a523 | ||
|   | 38ba85e89d | ||
|   | 97023921b8 | ||
|   | f835810f0a | ||
|   | 46f5589530 | ||
|   | ff9840c8ef | ||
|   | 0c197558a1 | ||
|   | c409ba149d | ||
|   | 0b896ddfb1 | ||
|   | 45721eb4fe | ||
|   | 1289bd03b2 | ||
|   | c1ba8ba6b8 | ||
|   | 4973d8f629 | ||
|   | 3aff4c96c4 | ||
|   | 4005bc8985 | ||
|   | 62e9792c39 | ||
|   | 33183cc595 | ||
|   | 394d552856 | ||
|   | aa4f0929e0 | ||
|   | f99b9215e3 | ||
|   | c51d621fee | ||
|   | 7499892bc2 | ||
|   | cbddebeaa8 | ||
|   | bbe4c95109 | ||
|   | 4c6f9f0dd8 | ||
|   | 90f7dba793 | ||
|   | 7c492338a2 | ||
|   | 530f494df8 | ||
|   | 8fd1f35c59 | ||
|   | af1518e924 | ||
|   | 473e381d75 | ||
|   | 7d3acc747d | ||
|   | bf7424a67c | ||
|   | 3fb35871c7 | ||
|   | d6d20cd704 | ||
|   | 9cc6a6b885 | ||
|   | ee0be7b6d0 | ||
|   | a856337eae | ||
|   | 6cf47ba4eb | ||
|   | 3b7a189708 | ||
|   | 79c542b76a | ||
|   | e37b7bd73f | ||
|   | d6f3c34b33 | ||
|   | 7e6153ba7d | 
							
								
								
									
										13
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										13
									
								
								.devcontainer/Dockerfile
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,13 @@ | ||||
| # See here for image contents: https://github.com/microsoft/vscode-dev-containers/tree/v0.148.1/containers/python-3/.devcontainer/base.Dockerfile | ||||
| FROM mcr.microsoft.com/vscode/devcontainers/python:0-3.9 | ||||
|  | ||||
| ENV \ | ||||
|   DEBIAN_FRONTEND=noninteractive \ | ||||
|   DEVCONTAINER=true \ | ||||
|   PATH=$PATH:./node_modules/.bin | ||||
|  | ||||
| # Install nvm | ||||
| COPY .nvmrc /tmp/.nvmrc | ||||
| RUN \ | ||||
|   su vscode -c \ | ||||
|     "source /usr/local/share/nvm/nvm.sh && nvm install $(cat /tmp/.nvmrc) 2>&1" | ||||
							
								
								
									
										31
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								.devcontainer/devcontainer.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| { | ||||
|   "name": "Home Assistant Frontend", | ||||
|   "build": { | ||||
|     "dockerfile": "Dockerfile", | ||||
|     "context": ".." | ||||
|   }, | ||||
|   "appPort": 8123, | ||||
|   "context": "..", | ||||
|   "postCreateCommand": "script/bootstrap", | ||||
|   "extensions": [ | ||||
|     "github.vscode-pull-request-github", | ||||
|     "dbaeumer.vscode-eslint", | ||||
|     "ms-vscode.vscode-typescript-tslint-plugin", | ||||
|     "esbenp.prettier-vscode", | ||||
|     "bierner.lit-html", | ||||
|     "runem.lit-plugin", | ||||
|     "ms-python.vscode-pylance" | ||||
|   ], | ||||
|   "settings": { | ||||
|     "terminal.integrated.shell.linux": "/bin/bash", | ||||
|     "files.eol": "\n", | ||||
|     "editor.tabSize": 2, | ||||
|     "editor.formatOnPaste": false, | ||||
|     "editor.formatOnSave": true, | ||||
|     "editor.formatOnType": true, | ||||
|     "[typescript]": { | ||||
|       "editor.defaultFormatter": "esbenp.prettier-vscode" | ||||
|     }, | ||||
|     "files.trimTrailingWhitespace": true | ||||
|   } | ||||
| } | ||||
							
								
								
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										3
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -35,3 +35,6 @@ yarn-error.log | ||||
|  | ||||
| #asdf | ||||
| .tool-versions | ||||
|  | ||||
| # Home Assistant config | ||||
| /config | ||||
|   | ||||
| @@ -1,6 +0,0 @@ | ||||
| jshint: | ||||
|   enabled: false | ||||
|  | ||||
| eslint: | ||||
|   enabled: true | ||||
|   config_file: .eslintrc-hound.json | ||||
							
								
								
									
										71
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										71
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -37,6 +37,37 @@ | ||||
|         "instanceLimit": 1 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "label": "Develop Supervisor panel", | ||||
|       "type": "gulp", | ||||
|       "task": "develop-hassio", | ||||
|       "problemMatcher": { | ||||
|         "owner": "ha-build", | ||||
|         "source": "ha-build", | ||||
|         "fileLocation": "absolute", | ||||
|         "severity": "error", | ||||
|         "pattern": [ | ||||
|           { | ||||
|             "regexp": "(SyntaxError): (.+): (.+) \\((\\d+):(\\d+)\\)", | ||||
|             "severity": 1, | ||||
|             "file": 2, | ||||
|             "message": 3, | ||||
|             "line": 4, | ||||
|             "column": 5 | ||||
|           } | ||||
|         ], | ||||
|         "background": { | ||||
|           "activeOnStart": true, | ||||
|           "beginsPattern": "Changes detected. Starting compilation", | ||||
|           "endsPattern": "Build done @" | ||||
|         } | ||||
|       }, | ||||
|       "isBackground": true, | ||||
|       "group": "build", | ||||
|       "runOptions": { | ||||
|         "instanceLimit": 1 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "label": "Develop Gallery", | ||||
|       "type": "gulp", | ||||
| @@ -133,5 +164,45 @@ | ||||
|         "instanceLimit": 1 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "label": "Run HA Core in devcontainer", | ||||
|       "type": "shell", | ||||
|       "command": "script/core", | ||||
|       "isBackground": true, | ||||
|       "group": { | ||||
|         "kind": "build", | ||||
|         "isDefault": true | ||||
|       }, | ||||
|       "problemMatcher": [], | ||||
|       "runOptions": { | ||||
|         "instanceLimit": 1 | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       "label": "Run HA Core for Supervisor in devcontainer", | ||||
|       "type": "shell", | ||||
|       "command": "HASSIO=${input:supervisorHost} HASSIO_TOKEN=${input:supervisorToken} script/core", | ||||
|       "isBackground": true, | ||||
|       "group": { | ||||
|         "kind": "build", | ||||
|         "isDefault": true | ||||
|       }, | ||||
|       "problemMatcher": [], | ||||
|       "runOptions": { | ||||
|         "instanceLimit": 1 | ||||
|       } | ||||
|     } | ||||
|   ], | ||||
|   "inputs": [ | ||||
|     { | ||||
|       "id": "supervisorHost", | ||||
|       "type": "promptString", | ||||
|       "description": "The IP of the Supervisor host running the Remote API proxy add-on" | ||||
|     }, | ||||
|     { | ||||
|       "id": "supervisorToken", | ||||
|       "type": "promptString", | ||||
|       "description": "The token for the Remote API proxy add-on" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
|   | ||||
							
								
								
									
										39
									
								
								build-scripts/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								build-scripts/README.md
									
									
									
									
									
										Normal 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). | ||||
| @@ -44,7 +44,7 @@ module.exports.definedVars = ({ isProdBuild, latestBuild, defineOverlay }) => ({ | ||||
| }); | ||||
|  | ||||
| module.exports.terserOptions = (latestBuild) => ({ | ||||
|   safari10: true, | ||||
|   safari10: !latestBuild, | ||||
|   ecma: latestBuild ? undefined : 5, | ||||
|   output: { comments: false }, | ||||
| }); | ||||
| @@ -117,7 +117,7 @@ BundleConfig { | ||||
| */ | ||||
|  | ||||
| module.exports.config = { | ||||
|   app({ isProdBuild, latestBuild, isStatsBuild }) { | ||||
|   app({ isProdBuild, latestBuild, isStatsBuild, isWDS }) { | ||||
|     return { | ||||
|       entry: { | ||||
|         service_worker: "./src/entrypoints/service_worker.ts", | ||||
| @@ -132,6 +132,7 @@ module.exports.config = { | ||||
|       isProdBuild, | ||||
|       latestBuild, | ||||
|       isStatsBuild, | ||||
|       isWDS, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   | ||||
| @@ -6,6 +6,9 @@ module.exports = { | ||||
|   useRollup() { | ||||
|     return process.env.ROLLUP === "1"; | ||||
|   }, | ||||
|   useWDS() { | ||||
|     return process.env.WDS === "1"; | ||||
|   }, | ||||
|   isProdBuild() { | ||||
|     return ( | ||||
|       process.env.NODE_ENV === "production" || module.exports.isStatsBuild() | ||||
|   | ||||
| @@ -12,6 +12,7 @@ require("./webpack.js"); | ||||
| require("./service-worker.js"); | ||||
| require("./entry-html.js"); | ||||
| require("./rollup.js"); | ||||
| require("./wds.js"); | ||||
|  | ||||
| gulp.task( | ||||
|   "develop-app", | ||||
| @@ -28,7 +29,11 @@ gulp.task( | ||||
|       "build-translations" | ||||
|     ), | ||||
|     "copy-static-app", | ||||
|     env.useRollup() ? "rollup-watch-app" : "webpack-watch-app" | ||||
|     env.useWDS() | ||||
|       ? "wds-watch-app" | ||||
|       : env.useRollup() | ||||
|       ? "rollup-watch-app" | ||||
|       : "webpack-watch-app" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
|   | ||||
| @@ -19,6 +19,7 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => { | ||||
|   return compiled({ | ||||
|     ...data, | ||||
|     useRollup: env.useRollup(), | ||||
|     useWDS: env.useWDS(), | ||||
|     renderTemplate, | ||||
|   }); | ||||
| }; | ||||
| @@ -90,10 +91,23 @@ gulp.task("gen-pages-prod", (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", { | ||||
|     latestAppJS: "/frontend_latest/app.js", | ||||
|     latestCoreJS: "/frontend_latest/core.js", | ||||
|     latestCustomPanelJS: "/frontend_latest/custom-panel.js", | ||||
|     latestAppJS, | ||||
|     latestCoreJS, | ||||
|     latestCustomPanelJS, | ||||
|  | ||||
|     es5AppJS: "/frontend_es5/app.js", | ||||
|     es5CoreJS: "/frontend_es5/core.js", | ||||
|   | ||||
| @@ -33,21 +33,10 @@ String.prototype.rsplit = function (sep, maxsplit) { | ||||
|     : split; | ||||
| }; | ||||
|  | ||||
| // Panel translations which should be split from the core translations. These | ||||
| // should mirror the fragment definitions in polymer.json, so that we load | ||||
| // additional resources at equivalent points. | ||||
| const TRANSLATION_FRAGMENTS = [ | ||||
|   "config", | ||||
|   "history", | ||||
|   "logbook", | ||||
|   "mailbox", | ||||
|   "profile", | ||||
|   "shopping-list", | ||||
|   "page-authorize", | ||||
|   "page-demo", | ||||
|   "page-onboarding", | ||||
|   "developer-tools", | ||||
| ]; | ||||
| // Panel translations which should be split from the core translations. | ||||
| const TRANSLATION_FRAGMENTS = Object.keys( | ||||
|   require("../../src/translations/en.json").ui.panel | ||||
| ); | ||||
|  | ||||
| function recursiveFlatten(prefix, data) { | ||||
|   let output = {}; | ||||
|   | ||||
							
								
								
									
										11
									
								
								build-scripts/gulp/wds.js
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								build-scripts/gulp/wds.js
									
									
									
									
									
										Normal 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, | ||||
|     }, | ||||
|   }); | ||||
| }); | ||||
| @@ -47,7 +47,7 @@ const runDevServer = ({ | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
| const handler = (done) => (err, stats) => { | ||||
| const doneHandler = (done) => (err, stats) => { | ||||
|   if (err) { | ||||
|     log.error(err.stack || err); | ||||
|     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", () => { | ||||
|   // 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( | ||||
|     { ignored: /build-translations/ }, | ||||
|     handler() | ||||
|     doneHandler() | ||||
|   ); | ||||
|   gulp.watch( | ||||
|     path.join(paths.translations_src, "en.json"), | ||||
| @@ -79,15 +88,12 @@ gulp.task("webpack-watch-app", () => { | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| gulp.task( | ||||
|   "webpack-prod-app", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         bothBuilds(createAppConfig, { isProdBuild: true }), | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| gulp.task("webpack-prod-app", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createAppConfig, { | ||||
|       isProdBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task("webpack-dev-server-demo", () => { | ||||
| @@ -98,17 +104,12 @@ gulp.task("webpack-dev-server-demo", () => { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task( | ||||
|   "webpack-prod-demo", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         bothBuilds(createDemoConfig, { | ||||
|           isProdBuild: true, | ||||
|         }), | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| gulp.task("webpack-prod-demo", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createDemoConfig, { | ||||
|       isProdBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task("webpack-dev-server-cast", () => { | ||||
| @@ -121,41 +122,30 @@ gulp.task("webpack-dev-server-cast", () => { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task( | ||||
|   "webpack-prod-cast", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         bothBuilds(createCastConfig, { | ||||
|           isProdBuild: true, | ||||
|         }), | ||||
|  | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| gulp.task("webpack-prod-cast", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createCastConfig, { | ||||
|       isProdBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| 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( | ||||
|     createHassioConfig({ | ||||
|       isProdBuild: false, | ||||
|       latestBuild: true, | ||||
|     }) | ||||
|   ).watch({}, handler()); | ||||
|   ).watch({}, doneHandler()); | ||||
| }); | ||||
|  | ||||
| gulp.task( | ||||
|   "webpack-prod-hassio", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         bothBuilds(createHassioConfig, { | ||||
|           isProdBuild: true, | ||||
|         }), | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| gulp.task("webpack-prod-hassio", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createHassioConfig, { | ||||
|       isProdBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
|  | ||||
| gulp.task("webpack-dev-server-gallery", () => { | ||||
| @@ -167,17 +157,11 @@ gulp.task("webpack-dev-server-gallery", () => { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task( | ||||
|   "webpack-prod-gallery", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         createGalleryConfig({ | ||||
|           isProdBuild: true, | ||||
|           latestBuild: true, | ||||
|         }), | ||||
|  | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| gulp.task("webpack-prod-gallery", () => | ||||
|   prodBuild( | ||||
|     createGalleryConfig({ | ||||
|       isProdBuild: true, | ||||
|       latestBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| ); | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| const path = require("path"); | ||||
|  | ||||
| module.exports = function (userOptions = {}) { | ||||
|   // Files need to be absolute paths. | ||||
|   // This only works if the file has no exports | ||||
|   | ||||
| @@ -3,7 +3,7 @@ const path = require("path"); | ||||
| const commonjs = require("@rollup/plugin-commonjs"); | ||||
| const resolve = require("@rollup/plugin-node-resolve"); | ||||
| 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 visualizer = require("rollup-plugin-visualizer"); | ||||
| const { string } = require("rollup-plugin-string"); | ||||
| @@ -31,6 +31,7 @@ const createRollupConfig = ({ | ||||
|   isStatsBuild, | ||||
|   publicPath, | ||||
|   dontHash, | ||||
|   isWDS, | ||||
| }) => { | ||||
|   return { | ||||
|     /** | ||||
| @@ -61,6 +62,7 @@ const createRollupConfig = ({ | ||||
|           ...bundle.babelOptions({ latestBuild }), | ||||
|           extensions, | ||||
|           exclude: bundle.babelExclude(), | ||||
|           babelHelpers: isWDS ? "inline" : "bundled", | ||||
|         }), | ||||
|         string({ | ||||
|           // Import certain extensions as strings | ||||
| @@ -69,19 +71,21 @@ const createRollupConfig = ({ | ||||
|         replace( | ||||
|           bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) | ||||
|         ), | ||||
|         manifest({ | ||||
|           publicPath, | ||||
|         }), | ||||
|         worker(), | ||||
|         dontHashPlugin({ dontHash }), | ||||
|         isProdBuild && terser(bundle.terserOptions(latestBuild)), | ||||
|         isStatsBuild && | ||||
|         !isWDS && | ||||
|           manifest({ | ||||
|             publicPath, | ||||
|           }), | ||||
|         !isWDS && worker(), | ||||
|         !isWDS && dontHashPlugin({ dontHash }), | ||||
|         !isWDS && isProdBuild && terser(bundle.terserOptions(latestBuild)), | ||||
|         !isWDS && | ||||
|           isStatsBuild && | ||||
|           visualizer({ | ||||
|             // https://github.com/btd/rollup-plugin-visualizer#options | ||||
|             open: true, | ||||
|             sourcemap: true, | ||||
|           }), | ||||
|       ], | ||||
|       ].filter(Boolean), | ||||
|     }, | ||||
|     /** | ||||
|      * @type { import("rollup").OutputOptions } | ||||
| @@ -108,12 +112,13 @@ const createRollupConfig = ({ | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => { | ||||
| const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild, isWDS }) => { | ||||
|   return createRollupConfig( | ||||
|     bundle.config.app({ | ||||
|       isProdBuild, | ||||
|       latestBuild, | ||||
|       isStatsBuild, | ||||
|       isWDS, | ||||
|     }) | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -3,22 +3,10 @@ import { Lovelace } from "../../../src/panels/lovelace/types"; | ||||
| import { DemoConfig } from "./types"; | ||||
|  | ||||
| export const demoConfigs: Array<() => Promise<DemoConfig>> = [ | ||||
|   () => | ||||
|     import(/* webpackChunkName: "arsaboo" */ "./arsaboo").then( | ||||
|       (mod) => mod.demoArsaboo | ||||
|     ), | ||||
|   () => | ||||
|     import(/* webpackChunkName: "teachingbirds" */ "./teachingbirds").then( | ||||
|       (mod) => mod.demoTeachingbirds | ||||
|     ), | ||||
|   () => | ||||
|     import(/* webpackChunkName: "kernehed" */ "./kernehed").then( | ||||
|       (mod) => mod.demoKernehed | ||||
|     ), | ||||
|   () => | ||||
|     import(/* webpackChunkName: "jimpower" */ "./jimpower").then( | ||||
|       (mod) => mod.demoJimpower | ||||
|     ), | ||||
|   () => import("./arsaboo").then((mod) => mod.demoArsaboo), | ||||
|   () => import("./teachingbirds").then((mod) => mod.demoTeachingbirds), | ||||
|   () => import("./kernehed").then((mod) => mod.demoKernehed), | ||||
|   () => import("./jimpower").then((mod) => mod.demoJimpower), | ||||
| ]; | ||||
|  | ||||
| // eslint-disable-next-line import/no-mutable-exports | ||||
|   | ||||
| @@ -9,5 +9,5 @@ export interface DemoConfig { | ||||
|   authorUrl: string; | ||||
|   lovelace: (localize: LocalizeFunc) => LovelaceConfig; | ||||
|   entities: (localize: LocalizeFunc) => Entity[]; | ||||
|   theme: () => { [key: string]: string } | null; | ||||
|   theme: () => Record<string, string> | null; | ||||
| } | ||||
|   | ||||
| @@ -7,7 +7,5 @@ import "./ha-demo"; | ||||
|  | ||||
| /* polyfill for paper-dropdown */ | ||||
| setTimeout(() => { | ||||
|   import( | ||||
|     /* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min" | ||||
|   ); | ||||
|   import("web-animations-js/web-animations-next-lite.min"); | ||||
| }, 1000); | ||||
|   | ||||
| @@ -21,15 +21,16 @@ class DemoCard extends PolymerElement { | ||||
|         } | ||||
|         pre { | ||||
|           width: 400px; | ||||
|           margin: 16px; | ||||
|           margin: 0 16px; | ||||
|           overflow: auto; | ||||
|           color: var(--primary-text-color); | ||||
|         } | ||||
|         @media only screen and (max-width: 800px) { | ||||
|           .root { | ||||
|             flex-direction: column; | ||||
|           } | ||||
|           pre { | ||||
|             margin-left: 0; | ||||
|             margin: 16px 0; | ||||
|           } | ||||
|         } | ||||
|       </style> | ||||
|   | ||||
| @@ -26,8 +26,9 @@ class DemoMoreInfo extends PolymerElement { | ||||
|  | ||||
|         pre { | ||||
|           width: 400px; | ||||
|           margin: 16px; | ||||
|           margin: 0 16px; | ||||
|           overflow: auto; | ||||
|           color: var(--primary-text-color); | ||||
|         } | ||||
|  | ||||
|         @media only screen and (max-width: 800px) { | ||||
| @@ -35,7 +36,7 @@ class DemoMoreInfo extends PolymerElement { | ||||
|             flex-direction: column; | ||||
|           } | ||||
|           pre { | ||||
|             margin-left: 0; | ||||
|             margin: 16px 0; | ||||
|           } | ||||
|         } | ||||
|       </style> | ||||
|   | ||||
| @@ -7,8 +7,8 @@ export const createMediaPlayerEntities = () => [ | ||||
|     media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", | ||||
|     media_artist: "Technohead", | ||||
|     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + | ||||
|     // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media | ||||
|     supported_features: 195135, | ||||
|     // Select Source + Stop + Clear + Play + Shuffle Set | ||||
|     supported_features: 64063, | ||||
|     entity_picture: "/images/album_cover_2.jpg", | ||||
|     media_duration: 300, | ||||
|     media_position: 50, | ||||
| @@ -24,8 +24,8 @@ export const createMediaPlayerEntities = () => [ | ||||
|     media_title: "I Wanna Be A Hippy (Flamman & Abraxas Radio Mix)", | ||||
|     media_artist: "Technohead", | ||||
|     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + | ||||
|     // Select Source + Stop + Clear + Play + Shuffle Set | ||||
|     supported_features: 64063, | ||||
|     // Select Source + Stop + Clear + Play + Shuffle Set + Browse Media | ||||
|     supported_features: 195135, | ||||
|     entity_picture: "/images/album_cover.jpg", | ||||
|     media_duration: 300, | ||||
|     media_position: 0, | ||||
|   | ||||
| @@ -73,13 +73,7 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoAlarmPanelEntity extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -88,7 +82,6 @@ class DemoAlarmPanelEntity extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -55,13 +55,7 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoConditional extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -70,7 +64,6 @@ class DemoConditional extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -20,10 +20,10 @@ const CONFIGS = [ | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "With Name", | ||||
|     heading: "With Name (defined in card)", | ||||
|     config: ` | ||||
| - type: button | ||||
|   name: Bedroom | ||||
|   name: Custom Name | ||||
|   entity: light.bed_light | ||||
|     `, | ||||
|   }, | ||||
| @@ -32,7 +32,7 @@ const CONFIGS = [ | ||||
|     config: ` | ||||
| - type: button | ||||
|   entity: light.bed_light | ||||
|   icon: mdi:hotel | ||||
|   icon: mdi:tools | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -71,13 +71,7 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoButtonEntity extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -86,7 +80,6 @@ class DemoButtonEntity extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,8 @@ import "../components/demo-cards"; | ||||
|  | ||||
| const ENTITIES = [ | ||||
|   getEntity("sensor", "brightness", "12", {}), | ||||
|   getEntity("sensor", "brightness_medium", "53", {}), | ||||
|   getEntity("sensor", "brightness_high", "87", {}), | ||||
|   getEntity("plant", "bonsai", "ok", {}), | ||||
|   getEntity("sensor", "not_working", "unavailable", {}), | ||||
|   getEntity("sensor", "outside_humidity", "54", { | ||||
| @@ -21,16 +23,10 @@ const CONFIGS = [ | ||||
|   { | ||||
|     heading: "Basic example", | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   entity: sensor.brightness | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "With title", | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   title: Humidity | ||||
|   entity: sensor.outside_humidity | ||||
|   name: Outside Humidity | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -39,6 +35,7 @@ const CONFIGS = [ | ||||
| - type: gauge | ||||
|   entity: sensor.outside_temperature | ||||
|   unit_of_measurement: C | ||||
|   name: Outside Temperature | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -46,19 +43,45 @@ const CONFIGS = [ | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   entity: sensor.brightness | ||||
|   name: Brightness Low | ||||
|   severity: | ||||
|     red: 32 | ||||
|     red: 75 | ||||
|     green: 0 | ||||
|     yellow: 23 | ||||
|     yellow: 50 | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Setting Min and Max Values", | ||||
|     heading: "Setting Severity Levels", | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   entity: sensor.brightness_medium | ||||
|   name: Brightness Medium | ||||
|   severity: | ||||
|     red: 75 | ||||
|     green: 0 | ||||
|     yellow: 50 | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Setting Severity Levels", | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   entity: sensor.brightness_high | ||||
|   name: Brightness High | ||||
|   severity: | ||||
|     red: 75 | ||||
|     green: 0 | ||||
|     yellow: 50 | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Setting Min (0) and Max (15) Values", | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   entity: sensor.brightness | ||||
|   name: Brightness | ||||
|   min: 0 | ||||
|   max: 38 | ||||
|   max: 15 | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|   | ||||
| @@ -8,29 +8,43 @@ import "../components/demo-cards"; | ||||
| const ENTITIES = [ | ||||
|   getEntity("light", "bed_light", "on", { | ||||
|     friendly_name: "Bed Light", | ||||
|     brightness: 130, | ||||
|     brightness: 255, | ||||
|   }), | ||||
|   getEntity("light", "dim", "off", { | ||||
|   getEntity("light", "dim_on", "on", { | ||||
|     friendly_name: "Dining Room", | ||||
|     supported_features: 1, | ||||
|     brightness: 100, | ||||
|   }), | ||||
|   getEntity("light", "dim_off", "off", { | ||||
|     friendly_name: "Dining Room", | ||||
|     supported_features: 1, | ||||
|   }), | ||||
|   getEntity("light", "unavailable", "unavailable", { | ||||
|     friendly_name: "Lost Light", | ||||
|     supported_features: 1, | ||||
|   }), | ||||
| ]; | ||||
|  | ||||
| const CONFIGS = [ | ||||
|   { | ||||
|     heading: "Basic example", | ||||
|     heading: "Switchable Light", | ||||
|     config: ` | ||||
| - type: light | ||||
|   entity: light.bed_light | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Dim", | ||||
|     heading: "Dimmable Light On", | ||||
|     config: ` | ||||
| - type: light | ||||
|   entity: light.dim | ||||
|   entity: light.dim_on | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Dimmable Light Off", | ||||
|     config: ` | ||||
| - type: light | ||||
|   entity: light.dim_off | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|   | ||||
| @@ -163,13 +163,7 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoMap extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -178,7 +172,6 @@ class DemoMap extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -146,17 +146,21 @@ const CONFIGS = [ | ||||
|     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 { | ||||
|   static get template() { | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -165,7 +169,6 @@ class DemoHuiMediControlCard extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -57,13 +57,7 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoHuiMediaPlayerRows extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -72,7 +66,6 @@ class DemoHuiMediaPlayerRows extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -20,48 +20,47 @@ class HaGallery extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` | ||||
|       <style include="iron-positioning ha-style"> | ||||
|       :host { | ||||
|         -ms-user-select: initial; | ||||
|         -webkit-user-select: initial; | ||||
|         -moz-user-select: initial; | ||||
|       } | ||||
|       app-header-layout { | ||||
|         min-height: 100vh; | ||||
|       } | ||||
|       ha-icon-button.invisible { | ||||
|         visibility: hidden; | ||||
|       } | ||||
|         :host { | ||||
|           -ms-user-select: initial; | ||||
|           -webkit-user-select: initial; | ||||
|           -moz-user-select: initial; | ||||
|         } | ||||
|         app-header-layout { | ||||
|           min-height: 100vh; | ||||
|         } | ||||
|         ha-icon-button.invisible { | ||||
|           visibility: hidden; | ||||
|         } | ||||
|  | ||||
|       .pickers { | ||||
|         display: flex; | ||||
|         flex-wrap: wrap; | ||||
|         justify-content: center; | ||||
|         align-items: start; | ||||
|       } | ||||
|         .pickers { | ||||
|           display: flex; | ||||
|           flex-wrap: wrap; | ||||
|           justify-content: center; | ||||
|           align-items: start; | ||||
|         } | ||||
|  | ||||
|       .pickers ha-card { | ||||
|         width: 400px; | ||||
|         display: block; | ||||
|         margin: 16px 8px; | ||||
|       } | ||||
|         .pickers ha-card { | ||||
|           width: 400px; | ||||
|           display: block; | ||||
|           margin: 16px 8px; | ||||
|         } | ||||
|  | ||||
|       .pickers ha-card:last-child { | ||||
|         margin-bottom: 16px; | ||||
|       } | ||||
|         .pickers ha-card:last-child { | ||||
|           margin-bottom: 16px; | ||||
|         } | ||||
|  | ||||
|       .intro { | ||||
|         margin: -1em 0; | ||||
|       } | ||||
|         .intro { | ||||
|           margin: -1em 0; | ||||
|         } | ||||
|  | ||||
|       p a { | ||||
|         color: var(--primary-color); | ||||
|       } | ||||
|  | ||||
|       a { | ||||
|         color: var(--primary-text-color); | ||||
|         text-decoration: none; | ||||
|       } | ||||
|         p a { | ||||
|           color: var(--primary-color); | ||||
|         } | ||||
|  | ||||
|         a { | ||||
|           color: var(--primary-text-color); | ||||
|           text-decoration: none; | ||||
|         } | ||||
|       </style> | ||||
|  | ||||
|       <app-header-layout> | ||||
| @@ -70,32 +69,42 @@ class HaGallery extends PolymerElement { | ||||
|             <ha-icon-button | ||||
|               icon="hass:arrow-left" | ||||
|               on-click="_backTapped" | ||||
|               class$='[[_computeHeaderButtonClass(_demo)]]' | ||||
|               class$="[[_computeHeaderButtonClass(_demo)]]" | ||||
|             ></ha-icon-button> | ||||
|             <div main-title>[[_withDefault(_demo, "Home Assistant Gallery")]]</div> | ||||
|             <div main-title> | ||||
|               [[_withDefault(_demo, "Home Assistant Gallery")]] | ||||
|             </div> | ||||
|           </app-toolbar> | ||||
|         </app-header> | ||||
|  | ||||
|         <div class='content'> | ||||
|           <div id='demo'></div> | ||||
|           <template is='dom-if' if='[[!_demo]]'> | ||||
|             <div class='pickers'> | ||||
|               <ha-card header="Lovelace card demos"> | ||||
|                 <div class='card-content intro'> | ||||
|         <div class="content"> | ||||
|           <div id="demo"></div> | ||||
|           <template is="dom-if" if="[[!_demo]]"> | ||||
|             <div class="pickers"> | ||||
|               <ha-card header="Lovelace Card Demos"> | ||||
|                 <div class="card-content intro"> | ||||
|                   <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> | ||||
|                     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> | ||||
|                     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> | ||||
|                 </div> | ||||
|                 <template is='dom-repeat' items='[[_lovelaceDemos]]'> | ||||
|                   <a href='#[[item]]'> | ||||
|                 <template is="dom-repeat" items="[[_lovelaceDemos]]"> | ||||
|                   <a href="#[[item]]"> | ||||
|                     <paper-item> | ||||
|                       <paper-item-body>{{ item }}</paper-item-body> | ||||
|                       <ha-icon icon="hass:chevron-right"></ha-icon> | ||||
| @@ -104,14 +113,14 @@ class HaGallery extends PolymerElement { | ||||
|                 </template> | ||||
|               </ha-card> | ||||
|  | ||||
|               <ha-card header="More Info demos"> | ||||
|                 <div class='card-content intro'> | ||||
|               <ha-card header="More Info Demos"> | ||||
|                 <div class="card-content intro"> | ||||
|                   <p> | ||||
|                     More info screens show up when an entity is clicked. | ||||
|                   </p> | ||||
|                 </div> | ||||
|                 <template is='dom-repeat' items='[[_moreInfoDemos]]'> | ||||
|                   <a href='#[[item]]'> | ||||
|                 <template is="dom-repeat" items="[[_moreInfoDemos]]"> | ||||
|                   <a href="#[[item]]"> | ||||
|                     <paper-item> | ||||
|                       <paper-item-body>{{ item }}</paper-item-body> | ||||
|                       <ha-icon icon="hass:chevron-right"></ha-icon> | ||||
| @@ -120,14 +129,14 @@ class HaGallery extends PolymerElement { | ||||
|                 </template> | ||||
|               </ha-card> | ||||
|  | ||||
|               <ha-card header="Util demos"> | ||||
|                 <div class='card-content intro'> | ||||
|               <ha-card header="Util Demos"> | ||||
|                 <div class="card-content intro"> | ||||
|                   <p> | ||||
|                     Test pages for our utility functions. | ||||
|                   </p> | ||||
|                 </div> | ||||
|                 <template is='dom-repeat' items='[[_utilDemos]]'> | ||||
|                   <a href='#[[item]]'> | ||||
|                 <template is="dom-repeat" items="[[_utilDemos]]"> | ||||
|                   <a href="#[[item]]"> | ||||
|                     <paper-item> | ||||
|                       <paper-item-body>{{ item }}</paper-item-body> | ||||
|                       <ha-icon icon="hass:chevron-right"></ha-icon> | ||||
| @@ -139,7 +148,10 @@ class HaGallery extends PolymerElement { | ||||
|           </template> | ||||
|         </div> | ||||
|       </app-header-layout> | ||||
|       <notification-manager hass=[[_fakeHass]] id='notifications'></notification-manager> | ||||
|       <notification-manager | ||||
|         hass="[[_fakeHass]]" | ||||
|         id="notifications" | ||||
|       ></notification-manager> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -27,6 +27,8 @@ declare global { | ||||
|   } | ||||
| } | ||||
|  | ||||
| const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB | ||||
|  | ||||
| @customElement("hassio-upload-snapshot") | ||||
| export class HassioUploadSnapshot extends LitElement { | ||||
|   public hass!: HomeAssistant; | ||||
| @@ -51,6 +53,20 @@ export class HassioUploadSnapshot extends LitElement { | ||||
|   private async _uploadFile(ev) { | ||||
|     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)) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "Unsupported file format", | ||||
|   | ||||
| @@ -12,7 +12,7 @@ import { atLeastVersion } from "../../../src/common/config/version"; | ||||
| import { navigate } from "../../../src/common/navigate"; | ||||
| import { compare } from "../../../src/common/string/compare"; | ||||
| 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 { HomeAssistant } from "../../../src/types"; | ||||
| import "../components/hassio-card-content"; | ||||
| @@ -22,14 +22,14 @@ import { hassioStyle } from "../resources/hassio-style"; | ||||
| class HassioAddons extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public addons?: HassioAddonInfo[]; | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <div class="content"> | ||||
|         <h1>Add-ons</h1> | ||||
|         <div class="card-group"> | ||||
|           ${!this.addons?.length | ||||
|           ${!this.supervisor.supervisor.addons?.length | ||||
|             ? html` | ||||
|                 <ha-card> | ||||
|                   <div class="card-content"> | ||||
| @@ -41,7 +41,7 @@ class HassioAddons extends LitElement { | ||||
|                   </div> | ||||
|                 </ha-card> | ||||
|               ` | ||||
|             : this.addons | ||||
|             : this.supervisor.supervisor.addons | ||||
|                 .sort((a, b) => compare(a.name, b.name)) | ||||
|                 .map( | ||||
|                   (addon) => html` | ||||
|   | ||||
| @@ -7,11 +7,7 @@ import { | ||||
|   property, | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import { HassioHassOSInfo } from "../../../src/data/hassio/host"; | ||||
| import { | ||||
|   HassioHomeAssistantInfo, | ||||
|   HassioSupervisorInfo, | ||||
| } from "../../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| @@ -23,16 +19,12 @@ import "./hassio-update"; | ||||
| class HassioDashboard extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   @property({ type: Boolean }) public narrow!: boolean; | ||||
|  | ||||
|   @property({ attribute: false }) public route!: Route; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo; | ||||
|  | ||||
|   @property({ attribute: false }) public hassInfo!: HassioHomeAssistantInfo; | ||||
|  | ||||
|   @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <hass-tabs-subpage | ||||
| @@ -47,13 +39,11 @@ class HassioDashboard extends LitElement { | ||||
|         <div class="content"> | ||||
|           <hassio-update | ||||
|             .hass=${this.hass} | ||||
|             .hassInfo=${this.hassInfo} | ||||
|             .supervisorInfo=${this.supervisorInfo} | ||||
|             .hassOsInfo=${this.hassOsInfo} | ||||
|             .supervisor=${this.supervisor} | ||||
|           ></hassio-update> | ||||
|           <hassio-addons | ||||
|             .hass=${this.hass} | ||||
|             .addons=${this.supervisorInfo.addons} | ||||
|             .supervisor=${this.supervisor} | ||||
|           ></hassio-addons> | ||||
|         </div> | ||||
|       </hass-tabs-subpage> | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import { | ||||
|   HassioHomeAssistantInfo, | ||||
|   HassioSupervisorInfo, | ||||
| } from "../../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| @@ -35,31 +36,20 @@ import { hassioStyle } from "../resources/hassio-style"; | ||||
| export class HassioUpdate extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo; | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   @property({ attribute: false }) public hassOsInfo?: HassioHassOSInfo; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisorInfo?: HassioSupervisorInfo; | ||||
|  | ||||
|   private _pendingUpdates = memoizeOne( | ||||
|     ( | ||||
|       core?: HassioHomeAssistantInfo, | ||||
|       supervisor?: HassioSupervisorInfo, | ||||
|       os?: HassioHassOSInfo | ||||
|     ): number => { | ||||
|       return [core, supervisor, os].filter( | ||||
|         (value) => !!value && value?.update_available | ||||
|       ).length; | ||||
|     } | ||||
|   ); | ||||
|   private _pendingUpdates = memoizeOne((supervisor: Supervisor): number => { | ||||
|     return Object.keys(supervisor).filter( | ||||
|       (value) => supervisor[value].update_available | ||||
|     ).length; | ||||
|   }); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const updatesAvailable = this._pendingUpdates( | ||||
|       this.hassInfo, | ||||
|       this.supervisorInfo, | ||||
|       this.hassOsInfo | ||||
|     ); | ||||
|     if (!this.supervisor) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
|     const updatesAvailable = this._pendingUpdates(this.supervisor); | ||||
|     if (!updatesAvailable) { | ||||
|       return html``; | ||||
|     } | ||||
| @@ -74,26 +64,24 @@ export class HassioUpdate extends LitElement { | ||||
|         <div class="card-group"> | ||||
|           ${this._renderUpdateCard( | ||||
|             "Home Assistant Core", | ||||
|             this.hassInfo!, | ||||
|             this.supervisor.core, | ||||
|             "hassio/homeassistant/update", | ||||
|             `https://${ | ||||
|               this.hassInfo?.version_latest.includes("b") ? "rc" : "www" | ||||
|               this.supervisor.core.version_latest.includes("b") ? "rc" : "www" | ||||
|             }.home-assistant.io/latest-release-notes/` | ||||
|           )} | ||||
|           ${this._renderUpdateCard( | ||||
|             "Supervisor", | ||||
|             this.supervisorInfo!, | ||||
|             this.supervisor.supervisor, | ||||
|             "hassio/supervisor/update", | ||||
|             `https://github.com//home-assistant/hassio/releases/tag/${ | ||||
|               this.supervisorInfo!.version_latest | ||||
|             }` | ||||
|             `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}` | ||||
|           )} | ||||
|           ${this.hassOsInfo | ||||
|           ${this.supervisor.host.features.includes("hassos") | ||||
|             ? this._renderUpdateCard( | ||||
|                 "Operating System", | ||||
|                 this.hassOsInfo, | ||||
|                 this.supervisor.os, | ||||
|                 "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> | ||||
|   | ||||
| @@ -11,10 +11,7 @@ export const showHassioMarkdownDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-markdown", | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown" | ||||
|       ), | ||||
|     dialogImport: () => import("./dialog-hassio-markdown"), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -1,5 +1,7 @@ | ||||
| import "@material/mwc-button/mwc-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-bar"; | ||||
| import { mdiClose } from "@mdi/js"; | ||||
| @@ -16,18 +18,22 @@ import { | ||||
| } from "lit-element"; | ||||
| import { cache } from "lit-html/directives/cache"; | ||||
| import { fireEvent } from "../../../../src/common/dom/fire_event"; | ||||
| import "../../../../src/components/ha-chips"; | ||||
| import "../../../../src/components/ha-circular-progress"; | ||||
| import "../../../../src/components/ha-dialog"; | ||||
| import "../../../../src/components/ha-expansion-panel"; | ||||
| import "../../../../src/components/ha-formfield"; | ||||
| import "../../../../src/components/ha-header-bar"; | ||||
| import "../../../../src/components/ha-radio"; | ||||
| import type { HaRadio } from "../../../../src/components/ha-radio"; | ||||
| import "../../../../src/components/ha-related-items"; | ||||
| import "../../../../src/components/ha-svg-icon"; | ||||
| import { extractApiErrorMessage } from "../../../../src/data/hassio/common"; | ||||
| import { | ||||
|   AccessPoints, | ||||
|   accesspointScan, | ||||
|   NetworkInterface, | ||||
|   updateNetworkInterface, | ||||
|   WifiConfiguration, | ||||
| } from "../../../../src/data/hassio/network"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
| @@ -38,54 +44,51 @@ import { haStyleDialog } from "../../../../src/resources/styles"; | ||||
| import type { HomeAssistant } from "../../../../src/types"; | ||||
| import { HassioNetworkDialogParams } from "./show-dialog-network"; | ||||
|  | ||||
| const IP_VERSIONS = ["ipv4", "ipv6"]; | ||||
|  | ||||
| @customElement("dialog-hassio-network") | ||||
| export class DialogHassioNetwork extends LitElement | ||||
|   implements HassDialog<HassioNetworkDialogParams> { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @internalProperty() private _prosessing = false; | ||||
|  | ||||
|   @internalProperty() private _params?: HassioNetworkDialogParams; | ||||
|  | ||||
|   @internalProperty() private _network!: { | ||||
|     interface: string; | ||||
|     data: NetworkInterface; | ||||
|   }[]; | ||||
|   @internalProperty() private _accessPoints?: AccessPoints; | ||||
|  | ||||
|   @internalProperty() private _curTabIndex = 0; | ||||
|  | ||||
|   @internalProperty() private _device?: { | ||||
|     interface: string; | ||||
|     data: NetworkInterface; | ||||
|   }; | ||||
|  | ||||
|   @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> { | ||||
|     this._params = params; | ||||
|     this._dirty = false; | ||||
|     this._curTabIndex = 0; | ||||
|     this._network = Object.keys(params.network?.interfaces) | ||||
|       .map((device) => ({ | ||||
|         interface: device, | ||||
|         data: params.network.interfaces[device], | ||||
|       })) | ||||
|       .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); | ||||
|     this._interfaces = params.network.interfaces.sort((a, b) => { | ||||
|       return a.primary > b.primary ? -1 : 1; | ||||
|     }); | ||||
|     this._interface = { ...this._interfaces[this._curTabIndex] }; | ||||
|  | ||||
|     await this.updateComplete; | ||||
|   } | ||||
|  | ||||
|   public closeDialog(): void { | ||||
|     this._params = undefined; | ||||
|     this._prosessing = false; | ||||
|     this._processing = false; | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this._params || !this._network) { | ||||
|     if (!this._params || !this._interface) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
| @@ -107,11 +110,11 @@ export class DialogHassioNetwork extends LitElement | ||||
|               <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||
|             </mwc-icon-button> | ||||
|           </ha-header-bar> | ||||
|           ${this._network.length > 1 | ||||
|           ${this._interfaces.length > 1 | ||||
|             ? html` <mwc-tab-bar | ||||
|                 .activeIndex=${this._curTabIndex} | ||||
|                 @MDCTabBar:activated=${this._handleTabActivated} | ||||
|                 >${this._network.map( | ||||
|                 >${this._interfaces.map( | ||||
|                   (device) => | ||||
|                     html`<mwc-tab | ||||
|                       .id=${device.interface} | ||||
| @@ -129,81 +132,302 @@ export class DialogHassioNetwork extends LitElement | ||||
|  | ||||
|   private _renderTab() { | ||||
|     return html` <div class="form container"> | ||||
|         <ha-formfield label="DHCP"> | ||||
|           <ha-radio | ||||
|             @change=${this._handleRadioValueChanged} | ||||
|             value="dhcp" | ||||
|             name="method" | ||||
|             ?checked=${this._device!.data.method === "dhcp"} | ||||
|           > | ||||
|           </ha-radio> | ||||
|         </ha-formfield> | ||||
|         <ha-formfield label="Static"> | ||||
|           <ha-radio | ||||
|             @change=${this._handleRadioValueChanged} | ||||
|             value="static" | ||||
|             name="method" | ||||
|             ?checked=${this._device!.data.method === "static"} | ||||
|           > | ||||
|           </ha-radio> | ||||
|         </ha-formfield> | ||||
|         ${this._device!.data.method !== "dhcp" | ||||
|           ? html` <paper-input | ||||
|                 class="flex-auto" | ||||
|                 id="ip_address" | ||||
|                 label="IP address/Netmask" | ||||
|                 .value="${this._device!.data.ip_address}" | ||||
|                 @value-changed=${this._handleInputValueChanged} | ||||
|               ></paper-input> | ||||
|               <paper-input | ||||
|                 class="flex-auto" | ||||
|                 id="gateway" | ||||
|                 label="Gateway address" | ||||
|                 .value="${this._device!.data.gateway}" | ||||
|                 @value-changed=${this._handleInputValueChanged} | ||||
|               ></paper-input> | ||||
|               <paper-input | ||||
|                 class="flex-auto" | ||||
|                 id="nameservers" | ||||
|                 label="DNS servers" | ||||
|                 .value="${this._device!.data.nameservers as string}" | ||||
|                 @value-changed=${this._handleInputValueChanged} | ||||
|               ></paper-input> | ||||
|               NB!: If you are changing IP or gateway addresses, you might lose | ||||
|               the connection.` | ||||
|         ${IP_VERSIONS.map((version) => | ||||
|           this._interface![version] ? this._renderIPConfiguration(version) : "" | ||||
|         )} | ||||
|         ${this._interface?.type === "wireless" | ||||
|           ? html` | ||||
|               <ha-expansion-panel header="Wi-Fi" outlined> | ||||
|                 ${this._interface?.wifi?.ssid | ||||
|                   ? html`<p>Connected to: ${this._interface?.wifi?.ssid}</p>` | ||||
|                   : ""} | ||||
|                 <mwc-button | ||||
|                   class="scan" | ||||
|                   @click=${this._scanForAP} | ||||
|                   .disabled=${this._scanning} | ||||
|                 > | ||||
|                   ${this._scanning | ||||
|                     ? html`<ha-circular-progress active size="small"> | ||||
|                       </ha-circular-progress>` | ||||
|                     : "Scan for accesspoints"} | ||||
|                 </mwc-button> | ||||
|                 ${this._accessPoints && | ||||
|                 this._accessPoints.accesspoints && | ||||
|                 this._accessPoints.accesspoints.length !== 0 | ||||
|                   ? html` | ||||
|                       <mwc-list> | ||||
|                         ${this._accessPoints.accesspoints | ||||
|                           .filter((ap) => ap.ssid) | ||||
|                           .map( | ||||
|                             (ap) => | ||||
|                               html` | ||||
|                                 <mwc-list-item | ||||
|                                   twoline | ||||
|                                   @click=${this._selectAP} | ||||
|                                   .activated=${ap.ssid === | ||||
|                                   this._wifiConfiguration?.ssid} | ||||
|                                   .ap=${ap} | ||||
|                                 > | ||||
|                                   <span>${ap.ssid}</span> | ||||
|                                   <span slot="secondary"> | ||||
|                                     ${ap.mac} - Strength: ${ap.signal} | ||||
|                                   </span> | ||||
|                                 </mwc-list-item> | ||||
|                               ` | ||||
|                           )} | ||||
|                       </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 class="buttons"> | ||||
|         <mwc-button label="close" @click=${this.closeDialog}> </mwc-button> | ||||
|         <mwc-button @click=${this._updateNetwork} ?disabled=${!this._dirty}> | ||||
|           ${this._prosessing | ||||
|             ? html`<ha-circular-progress active></ha-circular-progress>` | ||||
|             : "Update"} | ||||
|         <mwc-button @click=${this._updateNetwork} .disabled=${!this._dirty}> | ||||
|           ${this._processing | ||||
|             ? html`<ha-circular-progress active size="small"> | ||||
|               </ha-circular-progress>` | ||||
|             : "Save"} | ||||
|         </mwc-button> | ||||
|       </div>`; | ||||
|   } | ||||
|  | ||||
|   private async _updateNetwork() { | ||||
|     this._prosessing = true; | ||||
|     let options: Partial<NetworkInterface> = { | ||||
|       method: this._device!.data.method, | ||||
|     }; | ||||
|     if (options.method !== "dhcp") { | ||||
|       options = { | ||||
|         ...options, | ||||
|         address: this._device!.data.ip_address, | ||||
|         gateway: this._device!.data.gateway, | ||||
|         dns: String(this._device!.data.nameservers).split(","), | ||||
|       }; | ||||
|   private _selectAP(event) { | ||||
|     this._wifiConfiguration = event.currentTarget.ap; | ||||
|     this._dirty = true; | ||||
|   } | ||||
|  | ||||
|   private async _scanForAP() { | ||||
|     if (!this._interface) { | ||||
|       return; | ||||
|     } | ||||
|     this._scanning = true; | ||||
|     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) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "Failed to change network settings", | ||||
|         text: extractApiErrorMessage(err), | ||||
|       }); | ||||
|       this._prosessing = false; | ||||
|       this._processing = false; | ||||
|       return; | ||||
|     } | ||||
|     this._params?.loadData(); | ||||
| @@ -219,40 +443,73 @@ export class DialogHassioNetwork extends LitElement | ||||
|         dismissText: "no", | ||||
|       }); | ||||
|       if (!confirm) { | ||||
|         this.requestUpdate("_device"); | ||||
|         this.requestUpdate("_interface"); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     this._curTabIndex = ev.detail.index; | ||||
|     this._device = this._network[ev.detail.index]; | ||||
|     this._device.data.nameservers = String(this._device.data.nameservers); | ||||
|     this._interface = { ...this._interfaces[ev.detail.index] }; | ||||
|   } | ||||
|  | ||||
|   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; | ||||
|     } | ||||
|  | ||||
|     this._dirty = true; | ||||
|  | ||||
|     this._device!.data.method = value; | ||||
|     this.requestUpdate("_device"); | ||||
|     this._interface[version]!.method = value; | ||||
|     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 { | ||||
|     const value: string | null | undefined = (ev.target as PaperInputElement) | ||||
|       .value; | ||||
|     const version = (ev.target as any).version as "ipv4" | "ipv6"; | ||||
|     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; | ||||
|     } | ||||
|  | ||||
|     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[] { | ||||
| @@ -299,12 +556,16 @@ export class DialogHassioNetwork extends LitElement | ||||
|           --mdc-theme-primary: var(--error-color); | ||||
|         } | ||||
|  | ||||
|         mwc-button.scan { | ||||
|           margin-left: 8px; | ||||
|         } | ||||
|  | ||||
|         :host([rtl]) app-toolbar { | ||||
|           direction: rtl; | ||||
|           text-align: right; | ||||
|         } | ||||
|         .container { | ||||
|           padding: 20px 24px; | ||||
|           padding: 0 8px 4px; | ||||
|         } | ||||
|         .form { | ||||
|           margin-bottom: 53px; | ||||
| @@ -322,6 +583,24 @@ export class DialogHassioNetwork extends LitElement | ||||
|           padding-bottom: max(env(safe-area-inset-bottom), 8px); | ||||
|           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; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -13,10 +13,7 @@ export const showNetworkDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-network", | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network" | ||||
|       ), | ||||
|     dialogImport: () => import("./dialog-hassio-network"), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -4,10 +4,7 @@ import "./dialog-hassio-registries"; | ||||
| export const showRegistriesDialog = (element: HTMLElement): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-registries", | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries" | ||||
|       ), | ||||
|     dialogImport: () => import("./dialog-hassio-registries"), | ||||
|     dialogParams: {}, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -13,10 +13,7 @@ export const showRepositoriesDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-repositories", | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-repositories" */ "./dialog-hassio-repositories" | ||||
|       ), | ||||
|     dialogImport: () => import("./dialog-hassio-repositories"), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -109,7 +109,7 @@ class HassioSnapshotDialog extends LitElement { | ||||
|       return html``; | ||||
|     } | ||||
|     return html` | ||||
|       <ha-dialog open stacked @closing=${this._closeDialog} .heading=${true}> | ||||
|       <ha-dialog open @closing=${this._closeDialog} .heading=${true}> | ||||
|         <div slot="heading"> | ||||
|           <ha-header-bar> | ||||
|             <span slot="title"> | ||||
| @@ -191,47 +191,37 @@ class HassioSnapshotDialog extends LitElement { | ||||
|           : ""} | ||||
|         ${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""} | ||||
|  | ||||
|         <div>Actions:</div> | ||||
|         ${!this._onboarding | ||||
|           ? html`<mwc-button | ||||
|               @click=${this._downloadClicked} | ||||
|               slot="primaryAction" | ||||
|             > | ||||
|               <ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon> | ||||
|               Download Snapshot | ||||
|             </mwc-button>` | ||||
|           : ""} | ||||
|  | ||||
|         <mwc-button | ||||
|           @click=${this._partialRestoreClicked} | ||||
|           slot="secondaryAction" | ||||
|         > | ||||
|           <ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon> | ||||
|           Restore Selected | ||||
|         </mwc-button> | ||||
|         ${this._snapshot.type === "full" | ||||
|           ? html` | ||||
|               <mwc-button | ||||
|                 @click=${this._fullRestoreClicked} | ||||
|                 slot="secondaryAction" | ||||
|               > | ||||
|                 <ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon> | ||||
|                 Wipe & restore | ||||
|               </mwc-button> | ||||
|             ` | ||||
|           : ""} | ||||
|         ${!this._onboarding | ||||
|           ? html`<mwc-button | ||||
|               @click=${this._deleteClicked} | ||||
|               slot="secondaryAction" | ||||
|             > | ||||
|               <ha-svg-icon | ||||
|                 .path=${mdiDelete} | ||||
|                 class="icon warning" | ||||
|               ></ha-svg-icon> | ||||
|               <span class="warning">Delete Snapshot</span> | ||||
|             </mwc-button>` | ||||
|           : ""} | ||||
|         <div class="button-row" slot="primaryAction"> | ||||
|           <mwc-button @click=${this._partialRestoreClicked}> | ||||
|             <ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon> | ||||
|             Restore Selected | ||||
|           </mwc-button> | ||||
|           ${!this._onboarding | ||||
|             ? html` | ||||
|                 <mwc-button @click=${this._deleteClicked}> | ||||
|                   <ha-svg-icon .path=${mdiDelete} class="icon warning"> | ||||
|                   </ha-svg-icon> | ||||
|                   <span class="warning">Delete Snapshot</span> | ||||
|                 </mwc-button> | ||||
|               ` | ||||
|             : ""} | ||||
|         </div> | ||||
|         <div class="button-row" slot="secondaryAction"> | ||||
|           ${this._snapshot.type === "full" | ||||
|             ? html` | ||||
|                 <mwc-button @click=${this._fullRestoreClicked}> | ||||
|                   <ha-svg-icon .path=${mdiHistory} class="icon"></ha-svg-icon> | ||||
|                   Restore Everything | ||||
|                 </mwc-button> | ||||
|               ` | ||||
|             : ""} | ||||
|           ${!this._onboarding | ||||
|             ? html`<mwc-button @click=${this._downloadClicked}> | ||||
|                 <ha-svg-icon .path=${mdiDownload} class="icon"></ha-svg-icon> | ||||
|                 Download Snapshot | ||||
|               </mwc-button>` | ||||
|             : ""} | ||||
|         </div> | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
| @@ -245,6 +235,14 @@ class HassioSnapshotDialog extends LitElement { | ||||
|           display: block; | ||||
|           margin: 4px; | ||||
|         } | ||||
|         mwc-button ha-svg-icon { | ||||
|           margin-right: 4px; | ||||
|         } | ||||
|         .button-row { | ||||
|           display: grid; | ||||
|           gap: 8px; | ||||
|           margin-right: 8px; | ||||
|         } | ||||
|         .details { | ||||
|           color: var(--secondary-text-color); | ||||
|         } | ||||
| @@ -252,10 +250,6 @@ class HassioSnapshotDialog extends LitElement { | ||||
|         .error { | ||||
|           color: var(--error-color); | ||||
|         } | ||||
|         .buttons { | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|         } | ||||
|         .buttons li { | ||||
|           list-style-type: none; | ||||
|         } | ||||
|   | ||||
| @@ -12,10 +12,7 @@ export const showHassioSnapshotDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-snapshot", | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot" | ||||
|       ), | ||||
|     dialogImport: () => import("./dialog-hassio-snapshot"), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -13,10 +13,7 @@ export const showSnapshotUploadDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-snapshot-upload", | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-snapshot-upload" */ "./dialog-hassio-snapshot-upload" | ||||
|       ), | ||||
|     dialogImport: () => import("./dialog-hassio-snapshot-upload"), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -1,29 +1,22 @@ | ||||
| import { | ||||
|   html, | ||||
|   PropertyValues, | ||||
|   customElement, | ||||
|   LitElement, | ||||
|   property, | ||||
| } from "lit-element"; | ||||
| import { html, PropertyValues, customElement, property } from "lit-element"; | ||||
| 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 { HassioPanelInfo } from "../../src/data/hassio/supervisor"; | ||||
| import { applyThemesOnElement } from "../../src/common/dom/apply_themes_on_element"; | ||||
| import { fireEvent } from "../../src/common/dom/fire_event"; | ||||
| import { makeDialogManager } from "../../src/dialogs/make-dialog-manager"; | ||||
| import { atLeastVersion } from "../../src/common/config/version"; | ||||
| import { SupervisorBaseElement } from "./supervisor-base-element"; | ||||
|  | ||||
| @customElement("hassio-main") | ||||
| export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) { | ||||
| export class HassioMain extends SupervisorBaseElement { | ||||
|   @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) { | ||||
|     super.firstUpdated(changedProps); | ||||
| @@ -77,9 +70,13 @@ export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) { | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this.supervisor || !this.hass) { | ||||
|       return html``; | ||||
|     } | ||||
|     return html` | ||||
|       <hassio-router | ||||
|         .hass=${this.hass} | ||||
|         .supervisor=${this.supervisor} | ||||
|         .route=${this.route} | ||||
|         .panel=${this.panel} | ||||
|         .narrow=${this.narrow} | ||||
|   | ||||
| @@ -1,10 +1,5 @@ | ||||
| import { customElement, property } from "lit-element"; | ||||
| import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host"; | ||||
| import { | ||||
|   HassioHomeAssistantInfo, | ||||
|   HassioSupervisorInfo, | ||||
|   HassioInfo, | ||||
| } from "../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../src/data/supervisor/supervisor"; | ||||
| import { | ||||
|   HassRouterPage, | ||||
|   RouterOptions, | ||||
| @@ -21,20 +16,12 @@ import "./system/hassio-system"; | ||||
| class HassioPanelRouter extends HassRouterPage { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   @property({ attribute: false }) public route!: Route; | ||||
|  | ||||
|   @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 = { | ||||
|     routes: { | ||||
|       dashboard: { | ||||
| @@ -54,13 +41,9 @@ class HassioPanelRouter extends HassRouterPage { | ||||
|  | ||||
|   protected updatePageEl(el) { | ||||
|     el.hass = this.hass; | ||||
|     el.supervisor = this.supervisor; | ||||
|     el.route = this.route; | ||||
|     el.narrow = this.narrow; | ||||
|     el.supervisorInfo = this.supervisorInfo; | ||||
|     el.hassioInfo = this.hassioInfo; | ||||
|     el.hostInfo = this.hostInfo; | ||||
|     el.hassInfo = this.hassInfo; | ||||
|     el.hassOsInfo = this.hassOsInfo; | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,18 +1,13 @@ | ||||
| import { | ||||
|   css, | ||||
|   CSSResult, | ||||
|   customElement, | ||||
|   html, | ||||
|   LitElement, | ||||
|   property, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   CSSResult, | ||||
| } from "lit-element"; | ||||
| import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host"; | ||||
| import { | ||||
|   HassioHomeAssistantInfo, | ||||
|   HassioSupervisorInfo, | ||||
|   HassioInfo, | ||||
| } from "../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../src/data/supervisor/supervisor"; | ||||
| import { HomeAssistant, Route } from "../../src/types"; | ||||
| import "./hassio-panel-router"; | ||||
|  | ||||
| @@ -20,34 +15,19 @@ import "./hassio-panel-router"; | ||||
| class HassioPanel extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   @property({ type: Boolean }) public narrow!: boolean; | ||||
|  | ||||
|   @property({ attribute: false }) public route!: Route; | ||||
|  | ||||
|   @property({ attribute: false }) 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 { | ||||
|     if (!this.supervisorInfo) { | ||||
|       return html``; | ||||
|     } | ||||
|     return html` | ||||
|       <hassio-panel-router | ||||
|         .route=${this.route} | ||||
|         .hass=${this.hass} | ||||
|         .supervisor=${this.supervisor} | ||||
|         .route=${this.route} | ||||
|         .narrow=${this.narrow} | ||||
|         .supervisorInfo=${this.supervisorInfo} | ||||
|         .hassioInfo=${this.hassioInfo} | ||||
|         .hostInfo=${this.hostInfo} | ||||
|         .hassInfo=${this.hassInfo} | ||||
|         .hassOsInfo=${this.hassOsInfo} | ||||
|       ></hassio-panel-router> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -1,24 +1,6 @@ | ||||
| import { | ||||
|   customElement, | ||||
|   property, | ||||
|   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 { customElement, property } from "lit-element"; | ||||
| import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../src/data/supervisor/supervisor"; | ||||
| import { | ||||
|   HassRouterPage, | ||||
|   RouterOptions, | ||||
| @@ -32,9 +14,11 @@ import "./hassio-panel"; | ||||
| class HassioRouter extends HassRouterPage { | ||||
|   @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 = { | ||||
|     // 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", | ||||
|       addon: { | ||||
|         tag: "hassio-addon-dashboard", | ||||
|         load: () => | ||||
|           import( | ||||
|             /* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard" | ||||
|           ), | ||||
|         load: () => import("./addon-view/hassio-addon-dashboard"), | ||||
|       }, | ||||
|       ingress: { | ||||
|         tag: "hassio-ingress-view", | ||||
|         load: () => | ||||
|           import( | ||||
|             /* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view" | ||||
|           ), | ||||
|         load: () => import("./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) { | ||||
|     // the tabs page does its own routing so needs full route. | ||||
|     const route = el.nodeName === "HASSIO-PANEL" ? this.route : this.routeTail; | ||||
|  | ||||
|     el.hass = this.hass; | ||||
|     el.supervisor = this.supervisor; | ||||
|     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; | ||||
|  | ||||
|     if (el.localName === "hassio-ingress-view") { | ||||
| @@ -102,45 +61,12 @@ class HassioRouter extends HassRouterPage { | ||||
|   private async _fetchData() { | ||||
|     if (this.panel.config && 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) { | ||||
|     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 { | ||||
|   | ||||
| @@ -26,7 +26,6 @@ import { | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import { atLeastVersion } from "../../../src/common/config/version"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
| import "../../../src/components/buttons/ha-progress-button"; | ||||
| import "../../../src/components/ha-button-menu"; | ||||
| import "../../../src/components/ha-card"; | ||||
| @@ -41,7 +40,7 @@ import { | ||||
|   HassioSnapshot, | ||||
|   reloadHassioSnapshots, | ||||
| } 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 { PolymerChangedEvent } from "../../../src/polymer-types"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| @@ -67,7 +66,7 @@ class HassioSnapshots extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) public route!: Route; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo; | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   @internalProperty() private _snapshotName = ""; | ||||
|  | ||||
| @@ -266,7 +265,7 @@ class HassioSnapshots extends LitElement { | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if (changedProps.has("supervisorInfo")) { | ||||
|       this._addonList = this.supervisorInfo.addons | ||||
|       this._addonList = this.supervisor.supervisor.addons | ||||
|         .map((addon) => ({ | ||||
|           slug: addon.slug, | ||||
|           name: addon.name, | ||||
| @@ -372,7 +371,6 @@ class HassioSnapshots extends LitElement { | ||||
|         await createHassioPartialSnapshot(this.hass, data); | ||||
|       } | ||||
|       this._updateSnapshots(); | ||||
|       fireEvent(this, "hass-api-called", { success: true, response: null }); | ||||
|     } catch (err) { | ||||
|       this._error = extractApiErrorMessage(err); | ||||
|     } | ||||
|   | ||||
							
								
								
									
										69
									
								
								hassio/src/supervisor-base-element.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								hassio/src/supervisor-base-element.ts
									
									
									
									
									
										Normal 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, | ||||
|     }; | ||||
|   } | ||||
| } | ||||
| @@ -8,12 +8,12 @@ import { | ||||
|   CSSResult, | ||||
|   customElement, | ||||
|   html, | ||||
|   internalProperty, | ||||
|   LitElement, | ||||
|   property, | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../src/common/dom/fire_event"; | ||||
| import "../../../src/components/buttons/ha-progress-button"; | ||||
| import "../../../src/components/ha-button-menu"; | ||||
| import "../../../src/components/ha-card"; | ||||
| @@ -27,8 +27,6 @@ import { | ||||
|   changeHostOptions, | ||||
|   configSyncOS, | ||||
|   fetchHassioHostInfo, | ||||
|   HassioHassOSInfo, | ||||
|   HassioHostInfo as HassioHostInfoType, | ||||
|   rebootHost, | ||||
|   shutdownHost, | ||||
|   updateOS, | ||||
| @@ -37,7 +35,7 @@ import { | ||||
|   fetchNetworkInfo, | ||||
|   NetworkInfo, | ||||
| } from "../../../src/data/hassio/network"; | ||||
| import { HassioInfo } from "../../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| @@ -53,28 +51,22 @@ import { hassioStyle } from "../resources/hassio-style"; | ||||
| class HassioHostInfo extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public hostInfo!: HassioHostInfoType; | ||||
|  | ||||
|   @property({ attribute: false }) public hassioInfo!: HassioInfo; | ||||
|  | ||||
|   @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; | ||||
|  | ||||
|   @internalProperty() public _networkInfo?: NetworkInfo; | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   protected render(): TemplateResult | void { | ||||
|     const primaryIpAddress = this.hostInfo.features.includes("network") | ||||
|       ? this._primaryIpAddress(this._networkInfo!) | ||||
|     const primaryIpAddress = this.supervisor.host.features.includes("network") | ||||
|       ? this._primaryIpAddress(this.supervisor.network!) | ||||
|       : ""; | ||||
|     return html` | ||||
|       <ha-card header="Host System"> | ||||
|         <div class="card-content"> | ||||
|           ${this.hostInfo.features.includes("hostname") | ||||
|           ${this.supervisor.host.features.includes("hostname") | ||||
|             ? html`<ha-settings-row> | ||||
|                 <span slot="heading"> | ||||
|                   Hostname | ||||
|                 </span> | ||||
|                 <span slot="description"> | ||||
|                   ${this.hostInfo.hostname} | ||||
|                   ${this.supervisor.host.hostname} | ||||
|                 </span> | ||||
|                 <mwc-button | ||||
|                   title="Change the hostname" | ||||
| @@ -84,7 +76,7 @@ class HassioHostInfo extends LitElement { | ||||
|                 </mwc-button> | ||||
|               </ha-settings-row>` | ||||
|             : ""} | ||||
|           ${this.hostInfo.features.includes("network") | ||||
|           ${this.supervisor.host.features.includes("network") | ||||
|             ? html` <ha-settings-row> | ||||
|                 <span slot="heading"> | ||||
|                   IP Address | ||||
| @@ -106,10 +98,9 @@ class HassioHostInfo extends LitElement { | ||||
|               Operating System | ||||
|             </span> | ||||
|             <span slot="description"> | ||||
|               ${this.hostInfo.operating_system} | ||||
|               ${this.supervisor.host.operating_system} | ||||
|             </span> | ||||
|             ${this.hostInfo.features.includes("hassos") && | ||||
|             this.hassOsInfo.update_available | ||||
|             ${this.supervisor.os.update_available | ||||
|               ? html` | ||||
|                   <ha-progress-button | ||||
|                     title="Update the host OS" | ||||
| @@ -120,29 +111,29 @@ class HassioHostInfo extends LitElement { | ||||
|                 ` | ||||
|               : ""} | ||||
|           </ha-settings-row> | ||||
|           ${!this.hostInfo.features.includes("hassos") | ||||
|           ${!this.supervisor.host.features.includes("hassos") | ||||
|             ? html`<ha-settings-row> | ||||
|                 <span slot="heading"> | ||||
|                   Docker version | ||||
|                 </span> | ||||
|                 <span slot="description"> | ||||
|                   ${this.hassioInfo.docker} | ||||
|                   ${this.supervisor.info.docker} | ||||
|                 </span> | ||||
|               </ha-settings-row>` | ||||
|             : ""} | ||||
|           ${this.hostInfo.deployment | ||||
|           ${this.supervisor.host.deployment | ||||
|             ? html`<ha-settings-row> | ||||
|                 <span slot="heading"> | ||||
|                   Deployment | ||||
|                 </span> | ||||
|                 <span slot="description"> | ||||
|                   ${this.hostInfo.deployment} | ||||
|                   ${this.supervisor.host.deployment} | ||||
|                 </span> | ||||
|               </ha-settings-row>` | ||||
|             : ""} | ||||
|         </div> | ||||
|         <div class="card-actions"> | ||||
|           ${this.hostInfo.features.includes("reboot") | ||||
|           ${this.supervisor.host.features.includes("reboot") | ||||
|             ? html` | ||||
|                 <ha-progress-button | ||||
|                   title="Reboot the host OS" | ||||
| @@ -153,7 +144,7 @@ class HassioHostInfo extends LitElement { | ||||
|                 </ha-progress-button> | ||||
|               ` | ||||
|             : ""} | ||||
|           ${this.hostInfo.features.includes("shutdown") | ||||
|           ${this.supervisor.host.features.includes("shutdown") | ||||
|             ? html` | ||||
|                 <ha-progress-button | ||||
|                   title="Shutdown the host OS" | ||||
| @@ -175,7 +166,7 @@ class HassioHostInfo extends LitElement { | ||||
|             <mwc-list-item title="Show a list of hardware"> | ||||
|               Hardware | ||||
|             </mwc-list-item> | ||||
|             ${this.hostInfo.features.includes("hassos") | ||||
|             ${this.supervisor.host.features.includes("hassos") | ||||
|               ? html`<mwc-list-item | ||||
|                   title="Load HassOS configs or updates from USB" | ||||
|                 > | ||||
| @@ -193,12 +184,10 @@ class HassioHostInfo extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { | ||||
|     if (!network_info) { | ||||
|     if (!network_info || !network_info.interfaces) { | ||||
|       return ""; | ||||
|     } | ||||
|     return Object.keys(network_info?.interfaces) | ||||
|       .map((device) => network_info.interfaces[device]) | ||||
|       .find((device) => device.primary)?.ip_address; | ||||
|     return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0]; | ||||
|   }); | ||||
|  | ||||
|   private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { | ||||
| @@ -316,13 +305,13 @@ class HassioHostInfo extends LitElement { | ||||
|  | ||||
|   private async _changeNetworkClicked(): Promise<void> { | ||||
|     showNetworkDialog(this, { | ||||
|       network: this._networkInfo!, | ||||
|       network: this.supervisor.network!, | ||||
|       loadData: () => this._loadData(), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async _changeHostnameClicked(): Promise<void> { | ||||
|     const curHostname: string = this.hostInfo.hostname; | ||||
|     const curHostname: string = this.supervisor.host.hostname; | ||||
|     const hostname = await showPromptDialog(this, { | ||||
|       title: "Change Hostname", | ||||
|       inputLabel: "Please enter a new hostname:", | ||||
| @@ -333,7 +322,8 @@ class HassioHostInfo extends LitElement { | ||||
|     if (hostname && hostname !== curHostname) { | ||||
|       try { | ||||
|         await changeHostOptions(this.hass, { hostname }); | ||||
|         this.hostInfo = await fetchHassioHostInfo(this.hass); | ||||
|         const host = await fetchHassioHostInfo(this.hass); | ||||
|         fireEvent(this, "supervisor-update", { host }); | ||||
|       } catch (err) { | ||||
|         showAlertDialog(this, { | ||||
|           title: "Setting hostname failed", | ||||
| @@ -346,7 +336,8 @@ class HassioHostInfo extends LitElement { | ||||
|   private async _importFromUSB(): Promise<void> { | ||||
|     try { | ||||
|       await configSyncOS(this.hass); | ||||
|       this.hostInfo = await fetchHassioHostInfo(this.hass); | ||||
|       const host = await fetchHassioHostInfo(this.hass); | ||||
|       fireEvent(this, "supervisor-update", { host }); | ||||
|     } catch (err) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "Failed to import from USB", | ||||
| @@ -356,7 +347,8 @@ class HassioHostInfo extends LitElement { | ||||
|   } | ||||
|  | ||||
|   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[] { | ||||
|   | ||||
| @@ -13,16 +13,15 @@ import "../../../src/components/ha-card"; | ||||
| import "../../../src/components/ha-settings-row"; | ||||
| import "../../../src/components/ha-switch"; | ||||
| import { extractApiErrorMessage } from "../../../src/data/hassio/common"; | ||||
| import { HassioHostInfo as HassioHostInfoType } from "../../../src/data/hassio/host"; | ||||
| import { fetchHassioResolution } from "../../../src/data/hassio/resolution"; | ||||
| import { | ||||
|   fetchHassioSupervisorInfo, | ||||
|   HassioSupervisorInfo as HassioSupervisorInfoType, | ||||
|   reloadSupervisor, | ||||
|   restartSupervisor, | ||||
|   setSupervisorOption, | ||||
|   SupervisorOptions, | ||||
|   updateSupervisor, | ||||
| } from "../../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| @@ -32,7 +31,7 @@ import { HomeAssistant } from "../../../src/types"; | ||||
| import { documentationUrl } from "../../../src/util/documentation-url"; | ||||
| import { hassioStyle } from "../resources/hassio-style"; | ||||
|  | ||||
| const ISSUES = { | ||||
| const UNSUPPORTED_REASON = { | ||||
|   container: { | ||||
|     title: "Containers known to cause issues", | ||||
|     url: "/more-info/unsupported/container", | ||||
| @@ -46,6 +45,10 @@ const ISSUES = { | ||||
|     title: "Docker Version", | ||||
|     url: "/more-info/unsupported/docker_version", | ||||
|   }, | ||||
|   job_conditions: { | ||||
|     title: "Ignored job conditions", | ||||
|     url: "/more-info/unsupported/job_conditions", | ||||
|   }, | ||||
|   lxc: { title: "LXC", url: "/more-info/unsupported/lxc" }, | ||||
|   network_manager: { | ||||
|     title: "Network Manager", | ||||
| @@ -59,14 +62,30 @@ const ISSUES = { | ||||
|   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") | ||||
| class HassioSupervisorInfo extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) | ||||
|   public supervisorInfo!: HassioSupervisorInfoType; | ||||
|  | ||||
|   @property() public hostInfo!: HassioHostInfoType; | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   protected render(): TemplateResult | void { | ||||
|     return html` | ||||
| @@ -77,7 +96,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|               Version | ||||
|             </span> | ||||
|             <span slot="description"> | ||||
|               ${this.supervisorInfo.version} | ||||
|               ${this.supervisor.supervisor.version} | ||||
|             </span> | ||||
|           </ha-settings-row> | ||||
|           <ha-settings-row> | ||||
| @@ -85,9 +104,9 @@ class HassioSupervisorInfo extends LitElement { | ||||
|               Newest Version | ||||
|             </span> | ||||
|             <span slot="description"> | ||||
|               ${this.supervisorInfo.version_latest} | ||||
|               ${this.supervisor.supervisor.version_latest} | ||||
|             </span> | ||||
|             ${this.supervisorInfo.update_available | ||||
|             ${this.supervisor.supervisor.update_available | ||||
|               ? html` | ||||
|                   <ha-progress-button | ||||
|                     title="Update the supervisor" | ||||
| @@ -103,9 +122,9 @@ class HassioSupervisorInfo extends LitElement { | ||||
|               Channel | ||||
|             </span> | ||||
|             <span slot="description"> | ||||
|               ${this.supervisorInfo.channel} | ||||
|               ${this.supervisor.supervisor.channel} | ||||
|             </span> | ||||
|             ${this.supervisorInfo.channel === "beta" | ||||
|             ${this.supervisor.supervisor.channel === "beta" | ||||
|               ? html` | ||||
|                   <ha-progress-button | ||||
|                     @click=${this._toggleBeta} | ||||
| @@ -114,7 +133,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|                     Leave beta channel | ||||
|                   </ha-progress-button> | ||||
|                 ` | ||||
|               : this.supervisorInfo.channel === "stable" | ||||
|               : this.supervisor.supervisor.channel === "stable" | ||||
|               ? html` | ||||
|                   <ha-progress-button | ||||
|                     @click=${this._toggleBeta} | ||||
| @@ -126,7 +145,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|               : ""} | ||||
|           </ha-settings-row> | ||||
|  | ||||
|           ${this.supervisorInfo?.supported | ||||
|           ${this.supervisor.supervisor.supported | ||||
|             ? html` <ha-settings-row three-line> | ||||
|                 <span slot="heading"> | ||||
|                   Share Diagnostics | ||||
| @@ -143,7 +162,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|                 </div> | ||||
|                 <ha-switch | ||||
|                   haptic | ||||
|                   .checked=${this.supervisorInfo.diagnostics} | ||||
|                   .checked=${this.supervisor.supervisor.diagnostics} | ||||
|                   @change=${this._toggleDiagnostics} | ||||
|                 ></ha-switch> | ||||
|               </ha-settings-row>` | ||||
| @@ -157,14 +176,33 @@ class HassioSupervisorInfo extends LitElement { | ||||
|                   Learn more | ||||
|                 </button> | ||||
|               </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 class="card-actions"> | ||||
|           <ha-progress-button | ||||
|             @click=${this._supervisorReload} | ||||
|             title="Reload parts of the supervisor" | ||||
|             title="Reload parts of the Supervisor" | ||||
|           > | ||||
|             Reload | ||||
|           </ha-progress-button> | ||||
|           <ha-progress-button | ||||
|             class="warning" | ||||
|             @click=${this._supervisorRestart} | ||||
|             title="Restart the Supervisor" | ||||
|           > | ||||
|             Restart | ||||
|           </ha-progress-button> | ||||
|         </div> | ||||
|       </ha-card> | ||||
|     `; | ||||
| @@ -174,7 +212,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|     const button = ev.currentTarget as any; | ||||
|     button.progress = true; | ||||
|  | ||||
|     if (this.supervisorInfo.channel === "stable") { | ||||
|     if (this.supervisor.supervisor.channel === "stable") { | ||||
|       const confirmed = await showConfirmationDialog(this, { | ||||
|         title: "WARNING", | ||||
|         text: html` Beta releases are for testers and early adopters and can | ||||
| @@ -203,18 +241,19 @@ class HassioSupervisorInfo extends LitElement { | ||||
|  | ||||
|     try { | ||||
|       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 reloadSupervisor(this.hass); | ||||
|       fireEvent(this, "hass-api-called", { success: true, response: null }); | ||||
|       await this._reloadSupervisor(); | ||||
|     } catch (err) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "Failed to set supervisor option", | ||||
|         text: extractApiErrorMessage(err), | ||||
|       }); | ||||
|     } finally { | ||||
|       button.progress = false; | ||||
|     } | ||||
|     button.progress = false; | ||||
|   } | ||||
|  | ||||
|   private async _supervisorReload(ev: CustomEvent): Promise<void> { | ||||
| @@ -222,15 +261,37 @@ class HassioSupervisorInfo extends LitElement { | ||||
|     button.progress = true; | ||||
|  | ||||
|     try { | ||||
|       await reloadSupervisor(this.hass); | ||||
|       this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); | ||||
|       await this._reloadSupervisor(); | ||||
|     } catch (err) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "Failed to reload the supervisor", | ||||
|         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> { | ||||
| @@ -239,7 +300,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|  | ||||
|     const confirmed = await showConfirmationDialog(this, { | ||||
|       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", | ||||
|       dismissText: "cancel", | ||||
|     }); | ||||
| @@ -256,8 +317,9 @@ class HassioSupervisorInfo extends LitElement { | ||||
|         title: "Failed to update the supervisor", | ||||
|         text: extractApiErrorMessage(err), | ||||
|       }); | ||||
|     } finally { | ||||
|       button.progress = false; | ||||
|     } | ||||
|     button.progress = false; | ||||
|   } | ||||
|  | ||||
|   private async _diagnosticsInformationDialog(): Promise<void> { | ||||
| @@ -276,22 +338,53 @@ class HassioSupervisorInfo extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _unsupportedDialog(): Promise<void> { | ||||
|     const resolution = await fetchHassioResolution(this.hass); | ||||
|     await showAlertDialog(this, { | ||||
|       title: "You are running an unsupported installation", | ||||
|       text: html`Below is a list of issues found with your installation, click | ||||
|         on the links to learn how you can resolve the issues. <br /><br /> | ||||
|         <ul> | ||||
|           ${resolution.unsupported.map( | ||||
|           ${this.supervisor.resolution.unsupported.map( | ||||
|             (issue) => html` | ||||
|               <li> | ||||
|                 ${ISSUES[issue] | ||||
|                 ${UNSUPPORTED_REASON[issue] | ||||
|                   ? html`<a | ||||
|                       href="${documentationUrl(this.hass, ISSUES[issue].url)}" | ||||
|                       href="${documentationUrl( | ||||
|                         this.hass, | ||||
|                         UNSUPPORTED_REASON[issue].url | ||||
|                       )}" | ||||
|                       target="_blank" | ||||
|                       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>` | ||||
|                   : issue} | ||||
|               </li> | ||||
| @@ -304,7 +397,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|   private async _toggleDiagnostics(): Promise<void> { | ||||
|     try { | ||||
|       const data: SupervisorOptions = { | ||||
|         diagnostics: !this.supervisorInfo?.diagnostics, | ||||
|         diagnostics: !this.supervisor.supervisor?.diagnostics, | ||||
|       }; | ||||
|       await setSupervisorOption(this.hass, data); | ||||
|     } catch (err) { | ||||
|   | ||||
| @@ -19,6 +19,7 @@ import "../../../src/components/ha-card"; | ||||
| import "../../../src/components/ha-settings-row"; | ||||
| import { fetchHassioStats, HassioStats } from "../../../src/data/hassio/common"; | ||||
| import { HassioHostInfo } from "../../../src/data/hassio/host"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| import { HomeAssistant } from "../../../src/types"; | ||||
| import { bytesToString } from "../../../src/util/bytes-to-string"; | ||||
| @@ -32,7 +33,7 @@ import { hassioStyle } from "../resources/hassio-style"; | ||||
| class HassioSystemMetrics extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public hostInfo!: HassioHostInfo; | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   @internalProperty() private _supervisorMetrics?: HassioStats; | ||||
|  | ||||
| @@ -64,8 +65,8 @@ class HassioSystemMetrics extends LitElement { | ||||
|       }, | ||||
|       { | ||||
|         description: "Used Space", | ||||
|         value: this._getUsedSpace(this.hostInfo), | ||||
|         tooltip: `${this.hostInfo.disk_used} GB/${this.hostInfo.disk_total} GB`, | ||||
|         value: this._getUsedSpace(this.supervisor.host), | ||||
|         tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`, | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|   | ||||
| @@ -7,14 +7,7 @@ import { | ||||
|   property, | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import { | ||||
|   HassioHassOSInfo, | ||||
|   HassioHostInfo, | ||||
| } from "../../../src/data/hassio/host"; | ||||
| import { | ||||
|   HassioInfo, | ||||
|   HassioSupervisorInfo, | ||||
| } from "../../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| @@ -29,18 +22,12 @@ import "./hassio-system-metrics"; | ||||
| class HassioSystem extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|  | ||||
|   @property({ type: Boolean }) public narrow!: boolean; | ||||
|  | ||||
|   @property({ attribute: false }) public route!: Route; | ||||
|  | ||||
|   @property() public supervisorInfo!: HassioSupervisorInfo; | ||||
|  | ||||
|   @property({ attribute: false }) public hassioInfo!: HassioInfo; | ||||
|  | ||||
|   @property() public hostInfo!: HassioHostInfo; | ||||
|  | ||||
|   @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; | ||||
|  | ||||
|   protected render(): TemplateResult | void { | ||||
|     return html` | ||||
|       <hass-tabs-subpage | ||||
| @@ -56,18 +43,15 @@ class HassioSystem extends LitElement { | ||||
|           <div class="card-group"> | ||||
|             <hassio-supervisor-info | ||||
|               .hass=${this.hass} | ||||
|               .hostInfo=${this.hostInfo} | ||||
|               .supervisorInfo=${this.supervisorInfo} | ||||
|               .supervisor=${this.supervisor} | ||||
|             ></hassio-supervisor-info> | ||||
|             <hassio-host-info | ||||
|               .hass=${this.hass} | ||||
|               .hassioInfo=${this.hassioInfo} | ||||
|               .hostInfo=${this.hostInfo} | ||||
|               .hassOsInfo=${this.hassOsInfo} | ||||
|               .supervisor=${this.supervisor} | ||||
|             ></hassio-host-info> | ||||
|             <hassio-system-metrics | ||||
|               .hass=${this.hass} | ||||
|               .hostInfo=${this.hostInfo} | ||||
|               .supervisor=${this.supervisor} | ||||
|             ></hassio-system-metrics> | ||||
|           </div> | ||||
|           <hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log> | ||||
|   | ||||
							
								
								
									
										15
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										15
									
								
								package.json
									
									
									
									
									
								
							| @@ -83,6 +83,9 @@ | ||||
|     "@types/sortablejs": "^1.10.6", | ||||
|     "@vaadin/vaadin-combo-box": "^5.0.10", | ||||
|     "@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", | ||||
|     "@webcomponents/webcomponentsjs": "^2.2.7", | ||||
|     "chart.js": "~2.8.0", | ||||
| @@ -90,7 +93,6 @@ | ||||
|     "codemirror": "^5.49.0", | ||||
|     "comlink": "^4.3.0", | ||||
|     "core-js": "^3.6.5", | ||||
|     "cpx": "^1.5.0", | ||||
|     "cropperjs": "^1.5.7", | ||||
|     "deep-clone-simple": "^1.1.1", | ||||
|     "deep-freeze": "^0.0.1", | ||||
| @@ -110,7 +112,7 @@ | ||||
|     "marked": "^1.1.1", | ||||
|     "mdn-polyfills": "^5.16.0", | ||||
|     "memoize-one": "^5.0.2", | ||||
|     "node-vibrant": "^3.1.6", | ||||
|     "node-vibrant": "3.2.1-alpha.1", | ||||
|     "proxy-polyfill": "^0.3.1", | ||||
|     "punycode": "^2.1.1", | ||||
|     "qrcode": "^1.4.4", | ||||
| @@ -121,6 +123,8 @@ | ||||
|     "superstruct": "^0.10.12", | ||||
|     "tinykeys": "^1.1.1", | ||||
|     "unfetch": "^4.1.0", | ||||
|     "vis-data": "^7.1.1", | ||||
|     "vis-network": "^8.5.4", | ||||
|     "vue": "^2.6.11", | ||||
|     "vue2-daterange-picker": "^0.5.1", | ||||
|     "web-animations-js": "^2.3.2", | ||||
| @@ -142,6 +146,9 @@ | ||||
|     "@babel/plugin-syntax-import-meta": "^7.10.4", | ||||
|     "@babel/preset-env": "^7.11.5", | ||||
|     "@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-json": "^4.0.3", | ||||
|     "@rollup/plugin-node-resolve": "^7.1.3", | ||||
| @@ -160,8 +167,11 @@ | ||||
|     "@types/webspeechapi": "^0.0.29", | ||||
|     "@typescript-eslint/eslint-plugin": "^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", | ||||
|     "chai": "^4.2.0", | ||||
|     "cpx": "^1.5.0", | ||||
|     "del": "^4.0.0", | ||||
|     "eslint": "^6.8.0", | ||||
|     "eslint-config-airbnb-typescript": "^7.2.1", | ||||
| @@ -195,7 +205,6 @@ | ||||
|     "raw-loader": "^2.0.0", | ||||
|     "require-dir": "^1.2.0", | ||||
|     "rollup": "^2.8.2", | ||||
|     "rollup-plugin-babel": "^4.4.0", | ||||
|     "rollup-plugin-string": "^3.0.0", | ||||
|     "rollup-plugin-terser": "^5.3.0", | ||||
|     "rollup-plugin-visualizer": "^4.0.4", | ||||
|   | ||||
							
								
								
									
										40
									
								
								polymer.json
									
									
									
									
									
								
							
							
						
						
									
										40
									
								
								polymer.json
									
									
									
									
									
								
							| @@ -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 | 
							
								
								
									
										55
									
								
								script/core
									
									
									
									
									
										Executable file
									
								
							
							
						
						
									
										55
									
								
								script/core
									
									
									
									
									
										Executable file
									
								
							| @@ -0,0 +1,55 @@ | ||||
| #!/bin/sh | ||||
| # Helper to start Home Assistant Core inside the devcontainer | ||||
|  | ||||
| # Stop on errors | ||||
| set -e | ||||
|  | ||||
| if [ -z "${DEVCONTAINER}" ]; then | ||||
|   echo "This task should only run inside a devcontainer, for local install HA Core in a venv." | ||||
|   exit 1 | ||||
| fi | ||||
|  | ||||
| if [ ! -z "${CODESPACES}" ]; then | ||||
|   WORKSPACE="/root/workspace/frontend" | ||||
| else | ||||
|   WORKSPACE="/workspaces/frontend" | ||||
| fi | ||||
|  | ||||
| if [ -z $(which hass) ]; then | ||||
|   echo "Installing Home Asstant core from dev." | ||||
|   python3 -m pip install --upgrade \ | ||||
|     colorlog \ | ||||
|     git+git://github.com/home-assistant/home-assistant.git@dev | ||||
| fi | ||||
|  | ||||
| if [ ! -d "${WORKSPACE}/config" ]; then | ||||
|   echo "Creating default configuration." | ||||
|   mkdir -p "${WORKSPACE}/config"; | ||||
|   hass --script ensure_config -c config | ||||
|   echo "demo: | ||||
|  | ||||
| logger: | ||||
|   default: info | ||||
|   logs: | ||||
|     homeassistant.components.frontend: debug | ||||
| " >> "${WORKSPACE}/config/configuration.yaml" | ||||
|  | ||||
|   if [ ! -z "${HASSIO}" ]; then | ||||
|   echo " | ||||
| # frontend: | ||||
| #   development_repo: ${WORKSPACE} | ||||
|  | ||||
| hassio: | ||||
|   development_repo: ${WORKSPACE}" >> "${WORKSPACE}/config/configuration.yaml" | ||||
|   else | ||||
|   echo " | ||||
| frontend: | ||||
|   development_repo: ${WORKSPACE} | ||||
|  | ||||
| # hassio: | ||||
| #   development_repo: ${WORKSPACE}" >> "${WORKSPACE}/config/configuration.yaml" | ||||
|   fi | ||||
|  | ||||
| fi | ||||
|  | ||||
| hass -c "${WORKSPACE}/config" | ||||
							
								
								
									
										2
									
								
								setup.py
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								setup.py
									
									
									
									
									
								
							| @@ -2,7 +2,7 @@ from setuptools import setup, find_packages | ||||
|  | ||||
| setup( | ||||
|     name="home-assistant-frontend", | ||||
|     version="20201111.0", | ||||
|     version="20201212.0", | ||||
|     description="The Home Assistant frontend", | ||||
|     url="https://github.com/home-assistant/home-assistant-polymer", | ||||
|     author="The Home Assistant Authors", | ||||
|   | ||||
| @@ -18,7 +18,7 @@ import "./ha-auth-flow"; | ||||
| import { extractSearchParamsObject } from "../common/url/search-params"; | ||||
| import punycode from "punycode"; | ||||
|  | ||||
| import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider"); | ||||
| import("./ha-pick-auth-provider"); | ||||
|  | ||||
| class HaAuthorize extends litLocalizeLiteMixin(LitElement) { | ||||
|   @property() public clientId?: string; | ||||
|   | ||||
| @@ -22,3 +22,8 @@ export const rgbContrast = ( | ||||
|  | ||||
|   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; | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import { Theme } from "../../data/ws-themes"; | ||||
| import { darkStyles, derivedStyles } from "../../resources/styles"; | ||||
| import { HomeAssistant, Theme } from "../../types"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { | ||||
|   hex2rgb, | ||||
|   lab2hex, | ||||
| @@ -13,10 +14,10 @@ import { rgbContrast } from "../color/rgb"; | ||||
|  | ||||
| interface ProcessedTheme { | ||||
|   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. | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { directive, NodePart, Part } from "lit-html"; | ||||
|  | ||||
| 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)) { | ||||
|       throw new Error( | ||||
|         "dynamicElementDirective can only be used in content bindings" | ||||
|   | ||||
| @@ -13,13 +13,12 @@ export const setupLeafletMap = async ( | ||||
|     throw new Error("Cannot setup Leaflet map on disconnected element"); | ||||
|   } | ||||
|   // eslint-disable-next-line | ||||
|   const Leaflet = ((await import( | ||||
|     /* webpackChunkName: "leaflet" */ "leaflet" | ||||
|   )) as any).default as LeafletModuleType; | ||||
|   const Leaflet = ((await import("leaflet")) as any) | ||||
|     .default as LeafletModuleType; | ||||
|   Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; | ||||
|  | ||||
|   if (draw) { | ||||
|     await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw"); | ||||
|     await import("leaflet-draw"); | ||||
|   } | ||||
|  | ||||
|   const map = Leaflet.map(mapElement); | ||||
|   | ||||
							
								
								
									
										6
									
								
								src/common/ensure-array.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/common/ensure-array.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| export const ensureArray = (value?: any) => { | ||||
|   if (!value || Array.isArray(value)) { | ||||
|     return value; | ||||
|   } | ||||
|   return [value]; | ||||
| }; | ||||
| @@ -5,7 +5,7 @@ import { formatDateTime } from "../datetime/format_date_time"; | ||||
| import { formatTime } from "../datetime/format_time"; | ||||
| import { LocalizeFunc } from "../translations/localize"; | ||||
| import { computeStateDomain } from "./compute_state_domain"; | ||||
| import { numberFormat } from "../string/number-format"; | ||||
| import { formatNumber } from "../string/format_number"; | ||||
|  | ||||
| export const computeStateDisplay = ( | ||||
|   localize: LocalizeFunc, | ||||
| @@ -20,7 +20,7 @@ export const computeStateDisplay = ( | ||||
|   } | ||||
|  | ||||
|   if (stateObj.attributes.unit_of_measurement) { | ||||
|     return `${numberFormat(compareState, language)} ${ | ||||
|     return `${formatNumber(compareState, language)} ${ | ||||
|       stateObj.attributes.unit_of_measurement | ||||
|     }`; | ||||
|   } | ||||
| @@ -67,6 +67,10 @@ export const computeStateDisplay = ( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (domain === "counter") { | ||||
|     return formatNumber(compareState, language); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     // Return device class translation | ||||
|     (stateObj.attributes.device_class && | ||||
|   | ||||
| @@ -77,6 +77,11 @@ export const domainIcon = ( | ||||
|         return "hass:calendar"; | ||||
|       } | ||||
|       break; | ||||
|  | ||||
|     case "sun": | ||||
|       return stateObj?.state === "above_horizon" | ||||
|         ? FIXED_DOMAIN_ICONS[domain] | ||||
|         : "hass:weather-night"; | ||||
|   } | ||||
|  | ||||
|   if (domain in FIXED_DOMAIN_ICONS) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { HassEntities } from "home-assistant-js-websocket"; | ||||
| import { GroupEntity } from "../../types"; | ||||
| import type { GroupEntity } from "../../data/group"; | ||||
| import { DEFAULT_VIEW_ENTITY_ID } from "../const"; | ||||
|  | ||||
| // Return an ordered array of available views | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { HassEntities } from "home-assistant-js-websocket"; | ||||
| import { GroupEntity } from "../../types"; | ||||
| import { GroupEntity } from "../../data/group"; | ||||
|  | ||||
| export const getGroupEntities = ( | ||||
|   entities: HassEntities, | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { HassEntities } from "home-assistant-js-websocket"; | ||||
| import { GroupEntity } from "../../types"; | ||||
| import { GroupEntity } from "../../data/group"; | ||||
| import { computeDomain } from "./compute_domain"; | ||||
| import { getGroupEntities } from "./get_group_entities"; | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { HassEntities } from "home-assistant-js-websocket"; | ||||
| import { GroupEntity } from "../../types"; | ||||
| import { GroupEntity } from "../../data/group"; | ||||
| import { computeDomain } from "./compute_domain"; | ||||
|  | ||||
| // Split a collection into a list of groups and a 'rest' list of ungrouped | ||||
|   | ||||
							
								
								
									
										132
									
								
								src/common/image/extract_color.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										132
									
								
								src/common/image/extract_color.ts
									
									
									
									
									
										Normal 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! }; | ||||
|     }); | ||||
							
								
								
									
										54
									
								
								src/common/string/format_number.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										54
									
								
								src/common/string/format_number.ts
									
									
									
									
									
										Normal 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; | ||||
| }; | ||||
| @@ -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(); | ||||
| }; | ||||
| @@ -102,7 +102,7 @@ export const computeLocalize = async ( | ||||
| export const localizeKey = ( | ||||
|   localize: LocalizeFunc, | ||||
|   key: string, | ||||
|   placeholders?: { [key: string]: string } | ||||
|   placeholders?: Record<string, string> | ||||
| ) => { | ||||
|   const args: [string, ...string[]] = [key]; | ||||
|   if (placeholders) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| export const extractSearchParamsObject = (): { [key: string]: string } => { | ||||
| export const extractSearchParamsObject = (): Record<string, string> => { | ||||
|   const query = {}; | ||||
|   const searchParams = new URLSearchParams(location.search); | ||||
|   for (const [key, value] of searchParams.entries()) { | ||||
|   | ||||
| @@ -1,8 +1,12 @@ | ||||
| export const copyToClipboard = (str) => { | ||||
|   const el = document.createElement("textarea"); | ||||
|   el.value = str; | ||||
|   document.body.appendChild(el); | ||||
|   el.select(); | ||||
|   document.execCommand("copy"); | ||||
|   document.body.removeChild(el); | ||||
|   if (navigator.clipboard) { | ||||
|     navigator.clipboard.writeText(str); | ||||
|   } else { | ||||
|     const el = document.createElement("textarea"); | ||||
|     el.value = str; | ||||
|     document.body.appendChild(el); | ||||
|     el.select(); | ||||
|     document.execCommand("copy"); | ||||
|     document.body.removeChild(el); | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -98,6 +98,12 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   @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" }) | ||||
|   public autoHeight = false; | ||||
|  | ||||
| @@ -126,6 +132,8 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   @query("slot[name='header']") private _header!: HTMLSlotElement; | ||||
|  | ||||
|   private _items: DataTableRowData[] = []; | ||||
|  | ||||
|   private _checkableRowsCount?: number; | ||||
|  | ||||
|   private _checkedRows: string[] = []; | ||||
| @@ -318,10 +326,13 @@ export class HaDataTable extends LitElement { | ||||
|                   @scroll=${this._saveScrollPos} | ||||
|                 > | ||||
|                   ${scroll({ | ||||
|                     items: !this.hasFab | ||||
|                       ? this._filteredData | ||||
|                       : [...this._filteredData, ...[{ empty: true }]], | ||||
|                     items: this._items, | ||||
|                     renderItem: (row: DataTableRowData, index) => { | ||||
|                       if (row.append) { | ||||
|                         return html` | ||||
|                           <div class="mdc-data-table__row">${row.content}</div> | ||||
|                         `; | ||||
|                       } | ||||
|                       if (row.empty) { | ||||
|                         return html` <div class="mdc-data-table__row"></div> `; | ||||
|                       } | ||||
| @@ -447,6 +458,20 @@ export class HaDataTable extends LitElement { | ||||
|     if (this.curRequest !== curRequest) { | ||||
|       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; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -139,7 +139,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   private _filteredDevices: DeviceRegistryEntry[] = []; | ||||
|  | ||||
|   private _getDevices = memoizeOne( | ||||
|   private _getAreasWithDevices = memoizeOne( | ||||
|     ( | ||||
|       devices: DeviceRegistryEntry[], | ||||
|       areas: AreaRegistryEntry[], | ||||
| @@ -277,7 +277,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | ||||
|     if (!this._devices || !this._areas || !this._entities) { | ||||
|       return html``; | ||||
|     } | ||||
|     const areas = this._getDevices( | ||||
|     const areas = this._getAreasWithDevices( | ||||
|       this._devices, | ||||
|       this._areas, | ||||
|       this._entities, | ||||
|   | ||||
| @@ -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-item/paper-item"; | ||||
| import "@polymer/paper-item/paper-item-body"; | ||||
| @@ -12,6 +13,8 @@ import { | ||||
|   html, | ||||
|   LitElement, | ||||
|   property, | ||||
|   PropertyValues, | ||||
|   query, | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| @@ -35,6 +38,7 @@ import { | ||||
| import { SubscribeMixin } from "../../mixins/subscribe-mixin"; | ||||
| import { PolymerChangedEvent } from "../../polymer-types"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import { mdiClose, mdiMenuUp, mdiMenuDown } from "@mdi/js"; | ||||
|  | ||||
| interface Device { | ||||
|   name: string; | ||||
| @@ -42,6 +46,10 @@ interface Device { | ||||
|   id: string; | ||||
| } | ||||
|  | ||||
| export type HaDevicePickerDeviceFilterFunc = ( | ||||
|   device: DeviceRegistryEntry | ||||
| ) => boolean; | ||||
|  | ||||
| const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => { | ||||
|   if (!root.firstElementChild) { | ||||
|     root.innerHTML = ` | ||||
| @@ -102,9 +110,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|   @property({ type: Array, attribute: "include-device-classes" }) | ||||
|   public includeDeviceClasses?: string[]; | ||||
|  | ||||
|   @property() public deviceFilter?: HaDevicePickerDeviceFilterFunc; | ||||
|  | ||||
|   @property({ type: Boolean }) | ||||
|   private _opened?: boolean; | ||||
|  | ||||
|   @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; | ||||
|  | ||||
|   private _init = false; | ||||
|  | ||||
|   private _getDevices = memoizeOne( | ||||
|     ( | ||||
|       devices: DeviceRegistryEntry[], | ||||
| @@ -112,21 +126,31 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|       entities: EntityRegistryEntry[], | ||||
|       includeDomains: this["includeDomains"], | ||||
|       excludeDomains: this["excludeDomains"], | ||||
|       includeDeviceClasses: this["includeDeviceClasses"] | ||||
|       includeDeviceClasses: this["includeDeviceClasses"], | ||||
|       deviceFilter: this["deviceFilter"] | ||||
|     ): Device[] => { | ||||
|       if (!devices.length) { | ||||
|         return []; | ||||
|         return [ | ||||
|           { | ||||
|             id: "", | ||||
|             area: "", | ||||
|             name: this.hass.localize("ui.components.device-picker.no_devices"), | ||||
|           }, | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|       const deviceEntityLookup: DeviceEntityLookup = {}; | ||||
|       for (const entity of entities) { | ||||
|         if (!entity.device_id) { | ||||
|           continue; | ||||
|  | ||||
|       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); | ||||
|         } | ||||
|         if (!(entity.device_id in deviceEntityLookup)) { | ||||
|           deviceEntityLookup[entity.device_id] = []; | ||||
|         } | ||||
|         deviceEntityLookup[entity.device_id].push(entity); | ||||
|       } | ||||
|  | ||||
|       const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; | ||||
| @@ -134,7 +158,9 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|         areaLookup[area.area_id] = area; | ||||
|       } | ||||
|  | ||||
|       let inputDevices = [...devices]; | ||||
|       let inputDevices = devices.filter( | ||||
|         (device) => device.id === this.value || !device.disabled_by | ||||
|       ); | ||||
|  | ||||
|       if (includeDomains) { | ||||
|         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) => { | ||||
|         return { | ||||
|           id: device.id, | ||||
| @@ -193,6 +227,15 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|             : 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) { | ||||
|         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[] { | ||||
|     return [ | ||||
|       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 { | ||||
|     if (!this.devices || !this.areas || !this.entities) { | ||||
|       return html``; | ||||
|     } | ||||
|     const devices = this._getDevices( | ||||
|       this.devices, | ||||
|       this.areas, | ||||
|       this.entities, | ||||
|       this.includeDomains, | ||||
|       this.excludeDomains, | ||||
|       this.includeDeviceClasses | ||||
|     ); | ||||
|     return html` | ||||
|       <vaadin-combo-box-light | ||||
|         item-value-path="id" | ||||
|         item-id-path="id" | ||||
|         item-label-path="name" | ||||
|         .items=${devices} | ||||
|         .value=${this._value} | ||||
|         .renderer=${rowRenderer} | ||||
|         @opened-changed=${this._openedChanged} | ||||
| @@ -249,34 +313,30 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|         > | ||||
|           ${this.value | ||||
|             ? html` | ||||
|                 <ha-icon-button | ||||
|                   aria-label=${this.hass.localize( | ||||
|                 <mwc-icon-button | ||||
|                   .label=${this.hass.localize( | ||||
|                     "ui.components.device-picker.clear" | ||||
|                   )} | ||||
|                   slot="suffix" | ||||
|                   class="clear-button" | ||||
|                   icon="hass:close" | ||||
|                   @click=${this._clearValue} | ||||
|                   no-ripple | ||||
|                 > | ||||
|                   Clear | ||||
|                 </ha-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> | ||||
|                   <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||
|                 </mwc-icon-button> | ||||
|               ` | ||||
|             : ""} | ||||
|  | ||||
|           <mwc-icon-button | ||||
|             .label=${this.hass.localize( | ||||
|               "ui.components.device-picker.show_devices" | ||||
|             )} | ||||
|             slot="suffix" | ||||
|             class="toggle-button" | ||||
|           > | ||||
|             <ha-svg-icon | ||||
|               .path=${this._opened ? mdiMenuUp : mdiMenuDown} | ||||
|             ></ha-svg-icon> | ||||
|           </mwc-icon-button> | ||||
|         </paper-input> | ||||
|       </vaadin-combo-box-light> | ||||
|     `; | ||||
| @@ -313,7 +373,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   static get styles(): CSSResult { | ||||
|     return css` | ||||
|       paper-input > ha-icon-button { | ||||
|       paper-input > mwc-icon-button { | ||||
|         --mdc-icon-button-size: 24px; | ||||
|         padding: 2px; | ||||
|         color: var(--secondary-text-color); | ||||
|   | ||||
| @@ -230,9 +230,7 @@ class HaChartBase extends mixinBehaviors( | ||||
|     } | ||||
|  | ||||
|     if (scriptsLoaded === null) { | ||||
|       scriptsLoaded = import( | ||||
|         /* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js" | ||||
|       ); | ||||
|       scriptsLoaded = import("../../resources/ha-chart-scripts.js"); | ||||
|     } | ||||
|     scriptsLoaded.then((ChartModule) => { | ||||
|       this.ChartClass = ChartModule.default; | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import "@material/mwc-icon-button/mwc-icon-button"; | ||||
| import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import "@polymer/paper-item/paper-item"; | ||||
| @@ -15,14 +14,13 @@ import { | ||||
|   query, | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { computeDomain } from "../../common/entity/compute_domain"; | ||||
| import { PolymerChangedEvent } from "../../polymer-types"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import { formatAttributeName } from "../../util/hass-attributes-util"; | ||||
| import "../ha-svg-icon"; | ||||
| import "./state-badge"; | ||||
| import { formatAttributeName } from "../../util/hass-attributes-util"; | ||||
| import "@material/mwc-icon-button/mwc-icon-button"; | ||||
|  | ||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | ||||
|  | ||||
| @@ -43,41 +41,6 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => { | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const SELECTABLE_ATTRIBUTES: { [key: string]: string[] } = { | ||||
|   light: ["brightness"], | ||||
|   climate: [ | ||||
|     "current_temperature", | ||||
|     "fan_mode", | ||||
|     "preset_mode", | ||||
|     "swing_mode", | ||||
|     "temperature", | ||||
|     "current_hundity", | ||||
|     "humidity", | ||||
|     "hvac_action", | ||||
|   ], | ||||
|   fan: ["speed"], | ||||
|   air_quality: [ | ||||
|     "nitrogen_oxide", | ||||
|     "particulate_matter_10", | ||||
|     "particulate_matter_2_5", | ||||
|   ], | ||||
|   cover: ["current_position", "current_tilt_position"], | ||||
|   device_tracker: ["battery"], | ||||
|   humidifier: ["humidty"], | ||||
|   media_player: ["media_title"], | ||||
|   vacuum: ["battery_level", "status"], | ||||
|   water_heater: ["current_temperature", "temperature", "operation_mode"], | ||||
|   weather: [ | ||||
|     "temperature", | ||||
|     "humidity", | ||||
|     "ozone", | ||||
|     "pressure", | ||||
|     "wind_bearing", | ||||
|     "wind_speed", | ||||
|     "visibility", | ||||
|   ], | ||||
| }; | ||||
|  | ||||
| @customElement("ha-entity-attribute-picker") | ||||
| class HaEntityAttributePicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -105,8 +68,9 @@ class HaEntityAttributePicker extends LitElement { | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if (changedProps.has("_opened") && this._opened) { | ||||
|       (this._comboBox as any).items = this.entityId | ||||
|         ? this._selectableAttributes(this.entityId) | ||||
|       const state = this.entityId ? this.hass.states[this.entityId] : undefined; | ||||
|       (this._comboBox as any).items = state | ||||
|         ? Object.keys(state.attributes) | ||||
|         : []; | ||||
|     } | ||||
|   } | ||||
| @@ -121,6 +85,7 @@ class HaEntityAttributePicker extends LitElement { | ||||
|         .value=${this._value} | ||||
|         .allowCustomValue=${this.allowCustomValue} | ||||
|         .renderer=${rowRenderer} | ||||
|         attr-for-value="bind-value" | ||||
|         @opened-changed=${this._openedChanged} | ||||
|         @value-changed=${this._valueChanged} | ||||
|       > | ||||
| @@ -172,17 +137,6 @@ class HaEntityAttributePicker extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _selectableAttributes = memoizeOne((entity: string) => { | ||||
|     const stateObj = this.hass.states[entity]; | ||||
|     if (!stateObj) { | ||||
|       return []; | ||||
|     } | ||||
|  | ||||
|     return Object.keys(stateObj.attributes).filter((attr) => | ||||
|       SELECTABLE_ATTRIBUTES[computeDomain(entity)].includes(attr) | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   private _clearValue(ev: Event) { | ||||
|     ev.stopPropagation(); | ||||
|     this._setValue(""); | ||||
|   | ||||
| @@ -101,6 +101,18 @@ export class HaEntityPicker extends LitElement { | ||||
|  | ||||
|   @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 _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; | ||||
|     } | ||||
|   ); | ||||
| @@ -203,7 +233,6 @@ export class HaEntityPicker extends LitElement { | ||||
|           .label=${this.label === undefined | ||||
|             ? this.hass.localize("ui.components.entity.entity-picker.entity") | ||||
|             : this.label} | ||||
|           .value=${this._value} | ||||
|           .disabled=${this.disabled} | ||||
|           class="input" | ||||
|           autocapitalize="none" | ||||
|   | ||||
| @@ -21,6 +21,7 @@ import { timerTimeRemaining } from "../../common/entity/timer_time_remaining"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import "../ha-label-badge"; | ||||
| import { UNAVAILABLE, UNKNOWN } from "../../data/entity"; | ||||
| import { formatNumber } from "../../common/string/format_number"; | ||||
|  | ||||
| @customElement("ha-state-label-badge") | ||||
| export class HaStateLabelBadge extends LitElement { | ||||
| @@ -115,7 +116,7 @@ export class HaStateLabelBadge extends LitElement { | ||||
|           : state.state === UNKNOWN | ||||
|           ? "-" | ||||
|           : state.attributes.unit_of_measurement | ||||
|           ? state.state | ||||
|           ? formatNumber(state.state, this.hass!.language) | ||||
|           : computeStateDisplay( | ||||
|               this.hass!.localize, | ||||
|               state, | ||||
| @@ -154,11 +155,8 @@ export class HaStateLabelBadge extends LitElement { | ||||
|       case "device_tracker": | ||||
|       case "updater": | ||||
|       case "person": | ||||
|         return stateIcon(state); | ||||
|       case "sun": | ||||
|         return state.state === "above_horizon" | ||||
|           ? domainIcon(domain) | ||||
|           : "hass:brightness-3"; | ||||
|         return stateIcon(state); | ||||
|       case "timer": | ||||
|         return state.state === "active" | ||||
|           ? "hass:timer-outline" | ||||
|   | ||||
| @@ -94,7 +94,6 @@ class StateInfo extends LitElement { | ||||
|   static get styles(): CSSResult { | ||||
|     return css` | ||||
|       :host { | ||||
|         @apply --paper-font-body1; | ||||
|         min-width: 120px; | ||||
|         white-space: nowrap; | ||||
|       } | ||||
| @@ -118,9 +117,10 @@ class StateInfo extends LitElement { | ||||
|       } | ||||
|  | ||||
|       .name { | ||||
|         @apply --paper-font-common-nowrap; | ||||
|         color: var(--primary-text-color); | ||||
|         line-height: 40px; | ||||
|         white-space: nowrap; | ||||
|         overflow: hidden; | ||||
|         text-overflow: ellipsis; | ||||
|       } | ||||
|  | ||||
|       .name[in-dialog], | ||||
| @@ -131,8 +131,10 @@ class StateInfo extends LitElement { | ||||
|       .time-ago, | ||||
|       .extra-info, | ||||
|       .extra-info > * { | ||||
|         @apply --paper-font-common-nowrap; | ||||
|         color: var(--secondary-text-color); | ||||
|         white-space: nowrap; | ||||
|         overflow: hidden; | ||||
|         text-overflow: ellipsis; | ||||
|       } | ||||
|  | ||||
|       .row { | ||||
|   | ||||
| @@ -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-item/paper-item"; | ||||
| import "@polymer/paper-item/paper-item-body"; | ||||
| @@ -14,6 +15,8 @@ import { | ||||
|   property, | ||||
|   internalProperty, | ||||
|   TemplateResult, | ||||
|   PropertyValues, | ||||
|   query, | ||||
| } from "lit-element"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { | ||||
| @@ -28,6 +31,19 @@ import { | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { PolymerChangedEvent } from "../polymer-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 = ( | ||||
|   root: HTMLElement, | ||||
| @@ -68,31 +84,252 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   @property() public value?: string; | ||||
|  | ||||
|   @property() public _areas?: AreaRegistryEntry[]; | ||||
|   @property() public placeholder?: string; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "no-add" }) | ||||
|   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; | ||||
|  | ||||
|   @query("vaadin-combo-box-light", true) private _comboBox!: HTMLElement; | ||||
|  | ||||
|   private _init = false; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
|     return [ | ||||
|       subscribeAreaRegistry(this.hass.connection!, (areas) => { | ||||
|         this._areas = this.noAdd | ||||
|           ? areas | ||||
|           : [ | ||||
|               ...areas, | ||||
|               { | ||||
|                 area_id: "add_new", | ||||
|                 name: this.hass.localize("ui.components.area-picker.add_new"), | ||||
|               }, | ||||
|             ]; | ||||
|         this._areas = areas; | ||||
|       }), | ||||
|       subscribeDeviceRegistry(this.hass.connection!, (devices) => { | ||||
|         this._devices = devices; | ||||
|       }), | ||||
|       subscribeEntityRegistry(this.hass.connection!, (entities) => { | ||||
|         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 { | ||||
|     if (!this._areas) { | ||||
|     if (!this._devices || !this._areas || !this._entities) { | ||||
|       return html``; | ||||
|     } | ||||
|     return html` | ||||
| @@ -100,7 +337,6 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | ||||
|         item-value-path="area_id" | ||||
|         item-id-path="area_id" | ||||
|         item-label-path="name" | ||||
|         .items=${this._areas} | ||||
|         .value=${this._value} | ||||
|         .renderer=${rowRenderer} | ||||
|         @opened-changed=${this._openedChanged} | ||||
| @@ -110,6 +346,9 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | ||||
|           .label=${this.label === undefined && this.hass | ||||
|             ? this.hass.localize("ui.components.area-picker.area") | ||||
|             : this.label} | ||||
|           .placeholder=${this.placeholder | ||||
|             ? this._area(this.placeholder)?.name | ||||
|             : undefined} | ||||
|           class="input" | ||||
|           autocapitalize="none" | ||||
|           autocomplete="off" | ||||
| @@ -118,39 +357,39 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | ||||
|         > | ||||
|           ${this.value | ||||
|             ? html` | ||||
|                 <ha-icon-button | ||||
|                   aria-label=${this.hass.localize( | ||||
|                 <mwc-icon-button | ||||
|                   .label=${this.hass.localize( | ||||
|                     "ui.components.area-picker.clear" | ||||
|                   )} | ||||
|                   slot="suffix" | ||||
|                   class="clear-button" | ||||
|                   icon="hass:close" | ||||
|                   @click=${this._clearValue} | ||||
|                   no-ripple | ||||
|                 > | ||||
|                   ${this.hass.localize("ui.components.area-picker.clear")} | ||||
|                 </ha-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> | ||||
|                   <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||
|                 </mwc-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> | ||||
|       </vaadin-combo-box-light> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _area = memoizeOne((areaId: string): | ||||
|     | AreaRegistryEntry | ||||
|     | undefined => { | ||||
|     return this._areas?.find((area) => area.area_id === areaId); | ||||
|   }); | ||||
|  | ||||
|   private _clearValue(ev: Event) { | ||||
|     ev.stopPropagation(); | ||||
|     this._setValue(""); | ||||
| @@ -215,7 +454,7 @@ export class HaAreaPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   static get styles(): CSSResult { | ||||
|     return css` | ||||
|       paper-input > ha-icon-button { | ||||
|       paper-input > mwc-icon-button { | ||||
|         --mdc-icon-button-size: 24px; | ||||
|         padding: 2px; | ||||
|         color: var(--secondary-text-color); | ||||
|   | ||||
| @@ -107,7 +107,7 @@ class HaAttributes extends LitElement { | ||||
|       (!Array.isArray(value) && value instanceof Object) | ||||
|     ) { | ||||
|       if (!jsYamlPromise) { | ||||
|         jsYamlPromise = import(/* webpackChunkName: "js-yaml" */ "js-yaml"); | ||||
|         jsYamlPromise = import("js-yaml"); | ||||
|       } | ||||
|       const yaml = jsYamlPromise.then((jsYaml) => jsYaml.safeDump(value)); | ||||
|       return html` <pre>${until(yaml, "")}</pre> `; | ||||
|   | ||||
							
								
								
									
										125
									
								
								src/components/ha-blueprint-picker.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								src/components/ha-blueprint-picker.ts
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| } | ||||
| @@ -11,6 +11,7 @@ import { | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import type { ToggleButton } from "../types"; | ||||
| import "./ha-svg-icon"; | ||||
| import "@material/mwc-button/mwc-button"; | ||||
|  | ||||
| @customElement("ha-button-toggle-group") | ||||
| export class HaButtonToggleGroup extends LitElement { | ||||
| @@ -21,17 +22,22 @@ export class HaButtonToggleGroup extends LitElement { | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <div> | ||||
|         ${this.buttons.map( | ||||
|           (button) => html` | ||||
|             <mwc-icon-button | ||||
|               .label=${button.label} | ||||
|               .value=${button.value} | ||||
|               ?active=${this.active === button.value} | ||||
|               @click=${this._handleClick} | ||||
|             > | ||||
|               <ha-svg-icon .path=${button.iconPath}></ha-svg-icon> | ||||
|             </mwc-icon-button> | ||||
|           ` | ||||
|         ${this.buttons.map((button) => | ||||
|           button.iconPath | ||||
|             ? html`<mwc-icon-button | ||||
|                 .label=${button.label} | ||||
|                 .value=${button.value} | ||||
|                 ?active=${this.active === button.value} | ||||
|                 @click=${this._handleClick} | ||||
|               > | ||||
|                 <ha-svg-icon .path=${button.iconPath}></ha-svg-icon> | ||||
|               </mwc-icon-button>` | ||||
|             : html`<mwc-button | ||||
|                 .value=${button.value} | ||||
|                 ?active=${this.active === button.value} | ||||
|                 @click=${this._handleClick} | ||||
|                 >${button.label}</mwc-button | ||||
|               >` | ||||
|         )} | ||||
|       </div> | ||||
|     `; | ||||
| @@ -49,13 +55,15 @@ export class HaButtonToggleGroup extends LitElement { | ||||
|         --mdc-icon-button-size: var(--button-toggle-size, 36px); | ||||
|         --mdc-icon-size: var(--button-toggle-icon-size, 20px); | ||||
|       } | ||||
|       mwc-icon-button { | ||||
|       mwc-icon-button, | ||||
|       mwc-button { | ||||
|         border: 1px solid var(--primary-color); | ||||
|         border-right-width: 0px; | ||||
|         position: relative; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       mwc-icon-button::before { | ||||
|       mwc-icon-button::before, | ||||
|       mwc-button::before { | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         width: 100%; | ||||
| @@ -67,17 +75,21 @@ export class HaButtonToggleGroup extends LitElement { | ||||
|         content: ""; | ||||
|         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); | ||||
|       } | ||||
|       mwc-icon-button:first-child { | ||||
|       mwc-icon-button:first-child, | ||||
|       mwc-button:first-child { | ||||
|         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-right-width: 1px; | ||||
|       } | ||||
|       mwc-icon-button:only-child { | ||||
|       mwc-icon-button:only-child, | ||||
|       mwc-button:only-child { | ||||
|         border-radius: 4px; | ||||
|         border-right-width: 1px; | ||||
|       } | ||||
|   | ||||
| @@ -13,11 +13,12 @@ import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { computeStateName } from "../common/entity/compute_state_name"; | ||||
| import { supportsFeature } from "../common/entity/supports-feature"; | ||||
| import { | ||||
|   CameraEntity, | ||||
|   CAMERA_SUPPORT_STREAM, | ||||
|   computeMJPEGStreamUrl, | ||||
|   fetchStreamUrl, | ||||
| } from "../data/camera"; | ||||
| import { CameraEntity, HomeAssistant } from "../types"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./ha-hls-player"; | ||||
|  | ||||
| @customElement("ha-camera-stream") | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import { HassEntity } from "home-assistant-js-websocket"; | ||||
|  | ||||
| import { CLIMATE_PRESET_NONE } from "../data/climate"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import { formatNumber } from "../common/string/format_number"; | ||||
|  | ||||
| @customElement("ha-climate-state") | ||||
| class HaClimateState extends LitElement { | ||||
| @@ -51,11 +52,17 @@ class HaClimateState extends LitElement { | ||||
|     } | ||||
|  | ||||
|     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) { | ||||
|       return `${this.stateObj.attributes.current_humidity} %`; | ||||
|       return `${formatNumber( | ||||
|         this.stateObj.attributes.current_humidity, | ||||
|         this.hass!.language | ||||
|       )} %`; | ||||
|     } | ||||
|  | ||||
|     return undefined; | ||||
| @@ -70,21 +77,39 @@ class HaClimateState extends LitElement { | ||||
|       this.stateObj.attributes.target_temp_low != 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) { | ||||
|       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 ( | ||||
|       this.stateObj.attributes.target_humidity_low != 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) { | ||||
|       return `${this.stateObj.attributes.humidity} %`; | ||||
|       return `${formatNumber( | ||||
|         this.stateObj.attributes.humidity, | ||||
|         this.hass!.language | ||||
|       )} %`; | ||||
|     } | ||||
|  | ||||
|     return ""; | ||||
|   | ||||
| @@ -41,7 +41,7 @@ class HaCoverTiltControls extends LitElement { | ||||
|  | ||||
|     return html` <ha-icon-button | ||||
|         class=${classMap({ | ||||
|           invisible: !this._entityObj.supportsStop, | ||||
|           invisible: !this._entityObj.supportsOpenTilt, | ||||
|         })} | ||||
|         label=${this.hass.localize( | ||||
|           "ui.dialogs.more_info_control.open_tilt_cover" | ||||
| @@ -61,10 +61,10 @@ class HaCoverTiltControls extends LitElement { | ||||
|       ></ha-icon-button> | ||||
|       <ha-icon-button | ||||
|         class=${classMap({ | ||||
|           invisible: !this._entityObj.supportsStop, | ||||
|           invisible: !this._entityObj.supportsCloseTilt, | ||||
|         })} | ||||
|         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" | ||||
|         @click=${this._onCloseTiltTap} | ||||
|   | ||||
| @@ -2,6 +2,22 @@ import "@vaadin/vaadin-date-picker/theme/material/vaadin-date-picker"; | ||||
|  | ||||
| const VaadinDatePicker = customElements.get("vaadin-date-picker"); | ||||
|  | ||||
| const documentContainer = document.createElement("template"); | ||||
| documentContainer.setAttribute("style", "display: none;"); | ||||
| documentContainer.innerHTML = ` | ||||
|   <dom-module id="ha-date-input-styles" theme-for="vaadin-text-field"> | ||||
|     <template> | ||||
|       <style> | ||||
|       [part="input-field"] { | ||||
|         top: 2px; | ||||
|         height: 30px; | ||||
|       } | ||||
|       </style> | ||||
|     </template> | ||||
|   </dom-module> | ||||
| `; | ||||
| document.head.appendChild(documentContainer.content); | ||||
|  | ||||
| export class HaDateInput extends VaadinDatePicker { | ||||
|   constructor() { | ||||
|     super(); | ||||
|   | ||||
| @@ -19,12 +19,14 @@ class HaExpansionPanel extends LitElement { | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true }) outlined = false; | ||||
|  | ||||
|   @property() header?: string; | ||||
|  | ||||
|   @query(".container") private _container!: HTMLDivElement; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <div class="summary" @click=${this._toggleContainer}> | ||||
|         <slot name="title"></slot> | ||||
|         <slot name="header">${this.header}</slot> | ||||
|         <ha-svg-icon | ||||
|           .path=${mdiChevronDown} | ||||
|           class="summary-icon ${classMap({ expanded: this.expanded })}" | ||||
| @@ -76,7 +78,7 @@ class HaExpansionPanel extends LitElement { | ||||
|  | ||||
|       .summary { | ||||
|         display: flex; | ||||
|         padding: 0px 16px; | ||||
|         padding: var(--expansion-panel-summary-padding, 0); | ||||
|         min-height: 48px; | ||||
|         align-items: center; | ||||
|         cursor: pointer; | ||||
|   | ||||
							
								
								
									
										20
									
								
								src/components/ha-fab.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								src/components/ha-fab.ts
									
									
									
									
									
										Normal 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; | ||||
|   } | ||||
| } | ||||
| @@ -54,7 +54,7 @@ export interface HaFormSelectSchema extends HaFormBaseSchema { | ||||
|  | ||||
| export interface HaFormMultiSelectSchema extends HaFormBaseSchema { | ||||
|   type: "multi_select"; | ||||
|   options?: { [key: string]: string } | string[] | Array<[string, string]>; | ||||
|   options?: Record<string, string> | string[] | Array<[string, string]>; | ||||
| } | ||||
|  | ||||
| export interface HaFormFloatSchema extends HaFormBaseSchema { | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { afterNextRender } from "../common/util/render-status"; | ||||
| import { ifDefined } from "lit-html/directives/if-defined"; | ||||
|  | ||||
| import { getValueInPercentage, normalize } from "../util/calculate"; | ||||
| import { formatNumber } from "../common/string/format_number"; | ||||
|  | ||||
| const getAngle = (value: number, min: number, max: number) => { | ||||
|   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: String }) public language = ""; | ||||
|  | ||||
|   @property() public label = ""; | ||||
|  | ||||
|   @internalProperty() private _angle = 0; | ||||
| @@ -88,7 +91,7 @@ export class Gauge extends LitElement { | ||||
|       </svg> | ||||
|       <svg class="text"> | ||||
|         <text class="value-text"> | ||||
|           ${this.value} ${this.label} | ||||
|           ${formatNumber(this.value, this.language)} ${this.label} | ||||
|         </text> | ||||
|       </svg>`; | ||||
|   } | ||||
|   | ||||
| @@ -107,9 +107,7 @@ class HaHLSPlayer extends LitElement { | ||||
|     const useExoPlayerPromise = this._getUseExoPlayer(); | ||||
|     const masterPlaylistPromise = fetch(this.url); | ||||
|  | ||||
|     const hls = ((await import( | ||||
|       /* webpackChunkName: "hls.js" */ "hls.js" | ||||
|     )) as any).default as HLSModule; | ||||
|     const hls = ((await import("hls.js")) as any).default as HLSModule; | ||||
|     let hlsSupported = hls.isSupported(); | ||||
|  | ||||
|     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 | ||||
|     // 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 matchTwice = playlistRegexp.exec(masterPlaylist); | ||||
|  | ||||
| @@ -138,17 +136,13 @@ class HaHLSPlayer extends LitElement { | ||||
|     let playlist_url: string; | ||||
|     if (match !== null && matchTwice === null) { | ||||
|       // 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 { | ||||
|       playlist_url = this.url; | ||||
|     } | ||||
|  | ||||
|     // If codec is HEVC and ExoPlayer is supported, use ExoPlayer. | ||||
|     if ( | ||||
|       this._useExoPlayer && | ||||
|       match !== null && | ||||
|       match.groups!.isHevc !== undefined | ||||
|     ) { | ||||
|     if (this._useExoPlayer && match !== null && match[1] !== undefined) { | ||||
|       this._renderHLSExoPlayer(playlist_url); | ||||
|     } else if (hls.isSupported()) { | ||||
|       this._renderHLSPolyfill(videoEl, hls, playlist_url); | ||||
|   | ||||
| @@ -60,8 +60,9 @@ export class HaIconInput extends LitElement { | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       ha-icon { | ||||
|         position: relative; | ||||
|         bottom: 4px; | ||||
|         position: absolute; | ||||
|         bottom: 2px; | ||||
|         right: 0; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -39,7 +39,7 @@ checkCacheVersion(); | ||||
|  | ||||
| const debouncedWriteCache = debounce(() => writeCache(chunks), 2000); | ||||
|  | ||||
| const cachedIcons: { [key: string]: string } = {}; | ||||
| const cachedIcons: Record<string, string> = {}; | ||||
|  | ||||
| @customElement("ha-icon") | ||||
| export class HaIcon extends LitElement { | ||||
|   | ||||
| @@ -14,7 +14,7 @@ class HaLabeledSlider extends PolymerElement { | ||||
|         } | ||||
|  | ||||
|         .title { | ||||
|           margin: 4px 0 8px; | ||||
|           margin: 5px 0 8px; | ||||
|           color: var(--primary-text-color); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -58,6 +58,7 @@ export class HaRelatedItems extends SubscribeMixin(LitElement) { | ||||
|     getConfigEntries(this.hass).then((configEntries) => { | ||||
|       this._entries = configEntries; | ||||
|     }); | ||||
|     this.hass.loadBackendTranslation("title"); | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user