mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-29 21:49:45 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			layout-str
			...
			quick-bar-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 3fccd63bd3 | 
| @@ -1,13 +0,0 @@ | ||||
| # 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" | ||||
| @@ -1,31 +0,0 @@ | ||||
| { | ||||
|   "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 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										6
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/PULL_REQUEST_TEMPLATE.md
									
									
									
									
										vendored
									
									
								
							| @@ -18,8 +18,8 @@ | ||||
| <!-- | ||||
|   Describe the big picture of your changes here to communicate to the | ||||
|   maintainers why we should accept this pull request. If it fixes a bug | ||||
|   or resolves a feature request, be sure to link to that issue or discussion | ||||
|   in the additional information section. | ||||
|   or resolves a feature request, be sure to link to that issue in the | ||||
|   additional information section. | ||||
| --> | ||||
|  | ||||
| ## Type of change | ||||
| @@ -56,7 +56,7 @@ | ||||
| --> | ||||
|  | ||||
| - This PR fixes or closes issue: fixes # | ||||
| - This PR is related to issue or discussion: | ||||
| - This PR is related to issue: | ||||
| - Link to documentation pull request: | ||||
|  | ||||
| ## Checklist | ||||
|   | ||||
							
								
								
									
										27
									
								
								.github/lock.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										27
									
								
								.github/lock.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,27 @@ | ||||
| # Configuration for Lock Threads - https://github.com/dessant/lock-threads | ||||
|  | ||||
| # Number of days of inactivity before a closed issue or pull request is locked | ||||
| daysUntilLock: 1 | ||||
|  | ||||
| # Skip issues and pull requests created before a given timestamp. Timestamp must | ||||
| # follow ISO 8601 (`YYYY-MM-DD`). Set to `false` to disable | ||||
| skipCreatedBefore: 2020-01-01 | ||||
|  | ||||
| # Issues and pull requests with these labels will be ignored. Set to `[]` to disable | ||||
| exemptLabels: [] | ||||
|  | ||||
| # Label to add before locking, such as `outdated`. Set to `false` to disable | ||||
| lockLabel: false | ||||
|  | ||||
| # Comment to post before locking. Set to `false` to disable | ||||
| lockComment: false | ||||
|  | ||||
| # Assign `resolved` as the reason for locking. Set to `false` to disable | ||||
| setLockReason: false | ||||
|  | ||||
| # Limit to only `issues` or `pulls` | ||||
| only: pulls | ||||
|  | ||||
| # Optionally, specify configuration settings just for `issues` or `pulls` | ||||
| issues: | ||||
|   daysUntilLock: 30 | ||||
							
								
								
									
										56
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								.github/stale.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,56 @@ | ||||
| # Configuration for probot-stale - https://github.com/probot/stale | ||||
|  | ||||
| # Number of days of inactivity before an Issue or Pull Request becomes stale | ||||
| daysUntilStale: 90 | ||||
|  | ||||
| # Number of days of inactivity before an Issue or Pull Request with the stale label is closed. | ||||
| # Set to false to disable. If disabled, issues still need to be closed manually, but will remain marked as stale. | ||||
| daysUntilClose: 7 | ||||
|  | ||||
| # Only issues or pull requests with all of these labels are check if stale. Defaults to `[]` (disabled) | ||||
| onlyLabels: [] | ||||
|  | ||||
| # Issues or Pull Requests with these labels will never be considered stale. Set to `[]` to disable | ||||
| exemptLabels: | ||||
|   - feature request | ||||
|   - Help wanted | ||||
|   - to do | ||||
|  | ||||
| # Set to true to ignore issues in a project (defaults to false) | ||||
| exemptProjects: true | ||||
|  | ||||
| # Set to true to ignore issues in a milestone (defaults to false) | ||||
| exemptMilestones: true | ||||
|  | ||||
| # Set to true to ignore issues with an assignee (defaults to false) | ||||
| exemptAssignees: false | ||||
|  | ||||
| # Label to use when marking as stale | ||||
| staleLabel: stale | ||||
|  | ||||
| # Comment to post when marking as stale. Set to `false` to disable | ||||
| markComment: > | ||||
|   There hasn't been any activity on this issue recently. Due to the high number | ||||
|   of incoming GitHub notifications, we have to clean some of the old issues, | ||||
|   as many of them have already been resolved with the latest updates. | ||||
|  | ||||
|   Please make sure to update to the latest Home Assistant version and check | ||||
|   if that solves the issue. Let us know if that works for you by adding a | ||||
|   comment 👍 | ||||
|  | ||||
|   This issue now has been marked as stale and will be closed if no further | ||||
|   activity occurs. Thank you for your contributions. | ||||
|  | ||||
| # Comment to post when removing the stale label. | ||||
| # unmarkComment: > | ||||
| #   Your comment here. | ||||
|  | ||||
| # Comment to post when closing a stale Issue or Pull Request. | ||||
| # closeComment: > | ||||
| #   Your comment here. | ||||
|  | ||||
| # Limit the number of actions per hour, from 1-30. Default is 30 | ||||
| limitPerRun: 30 | ||||
|  | ||||
| # Limit to only `issues` or `pulls` | ||||
| only: issues | ||||
							
								
								
									
										20
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										20
									
								
								.github/workflows/lock.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,20 +0,0 @@ | ||||
| name: Lock | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: "0 * * * *" | ||||
|  | ||||
| jobs: | ||||
|   lock: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - uses: dessant/lock-threads@v2.0.1 | ||||
|         with: | ||||
|           github-token: ${{ github.token }} | ||||
|           issue-lock-inactive-days: "30" | ||||
|           issue-exclude-created-before: "2020-10-01T00:00:00Z" | ||||
|           issue-lock-reason: "" | ||||
|           pr-lock-inactive-days: "1" | ||||
|           pr-exclude-created-before: "2020-11-01T00:00:00Z" | ||||
|           pr-lock-reason: "" | ||||
							
								
								
									
										42
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										42
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -1,42 +0,0 @@ | ||||
| name: Stale | ||||
|  | ||||
| # yamllint disable-line rule:truthy | ||||
| on: | ||||
|   schedule: | ||||
|     - cron: "0 * * * *" | ||||
|  | ||||
| jobs: | ||||
|   stale: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: 90 days stale policy | ||||
|         uses: actions/stale@v3.0.13 | ||||
|         with: | ||||
|           repo-token: ${{ secrets.GITHUB_TOKEN }} | ||||
|           days-before-stale: 90 | ||||
|           days-before-close: 7 | ||||
|           operations-per-run: 25 | ||||
|           remove-stale-when-updated: true | ||||
|           stale-issue-label: "stale" | ||||
|           exempt-issue-labels: "no-stale,Help%20wanted,help-wanted,feature-request,feature%20request" | ||||
|           stale-issue-message: > | ||||
|             There hasn't been any activity on this issue recently. Due to the | ||||
|             high number of incoming GitHub notifications, we have to clean some | ||||
|             of the old issues, as many of them have already been resolved with | ||||
|             the latest updates. | ||||
|  | ||||
|             Please make sure to update to the latest Home Assistant version and | ||||
|             check if that solves the issue. Let us know if that works for you by | ||||
|             adding a comment 👍 | ||||
|  | ||||
|             This issue has now been marked as stale and will be closed if no | ||||
|             further activity occurs. Thank you for your contributions. | ||||
|  | ||||
|           stale-pr-label: "stale" | ||||
|           exempt-pr-labels: "no-stale" | ||||
|           stale-pr-message: > | ||||
|             There hasn't been any activity on this pull request recently. This | ||||
|             pull request has been automatically marked as stale because of that | ||||
|             and will be closed if no further activity occurs within 7 days. | ||||
|  | ||||
|             Thank you for your contributions. | ||||
							
								
								
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										5
									
								
								.gitignore
									
									
									
									
										vendored
									
									
								
							| @@ -23,8 +23,6 @@ dist | ||||
| # vscode | ||||
| .vscode/* | ||||
| !.vscode/extensions.json | ||||
| !.vscode/launch.json | ||||
| !.vscode/tasks.json | ||||
|  | ||||
| # Cast dev settings | ||||
| src/cast/dev_const.ts | ||||
| @@ -35,6 +33,3 @@ yarn-error.log | ||||
|  | ||||
| #asdf | ||||
| .tool-versions | ||||
|  | ||||
| # Home Assistant config | ||||
| /config | ||||
|   | ||||
							
								
								
									
										6
									
								
								.hound.yml
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								.hound.yml
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,6 @@ | ||||
| jshint: | ||||
|   enabled: false | ||||
|  | ||||
| eslint: | ||||
|   enabled: true | ||||
|   config_file: .eslintrc-hound.json | ||||
							
								
								
									
										44
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										44
									
								
								.vscode/launch.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,44 +0,0 @@ | ||||
| { | ||||
|   // https://github.com/microsoft/vscode-js-debug/blob/master/OPTIONS.md | ||||
|   "configurations": [ | ||||
|     { | ||||
|       "name": "Debug Frontend", | ||||
|       "request": "launch", | ||||
|       "type": "pwa-chrome", | ||||
|       "url": "http://localhost:8123/", | ||||
|       "webRoot": "${workspaceFolder}/hass_frontend", | ||||
|       "disableNetworkCache": true, | ||||
|       "preLaunchTask": "Develop Frontend", | ||||
|       "outFiles": [ | ||||
|         "${workspaceFolder}/hass_frontend/frontend_latest/*.js" | ||||
|       ] | ||||
|     }, | ||||
|     { | ||||
|       "name": "Debug Gallery", | ||||
|       "request": "launch", | ||||
|       "type": "pwa-chrome", | ||||
|       "url": "http://localhost:8100/", | ||||
|       "webRoot": "${workspaceFolder}/gallery/dist", | ||||
|       "disableNetworkCache": true, | ||||
|       "preLaunchTask": "Develop Gallery" | ||||
|     }, | ||||
|     { | ||||
|       "name": "Debug Demo", | ||||
|       "request": "launch", | ||||
|       "type": "pwa-chrome", | ||||
|       "url": "http://localhost:8090/", | ||||
|       "webRoot": "${workspaceFolder}/demo/dist", | ||||
|       "disableNetworkCache": true, | ||||
|       "preLaunchTask": "Develop Demo" | ||||
|     }, | ||||
|     { | ||||
|       "name": "Debug Cast", | ||||
|       "request": "launch", | ||||
|       "type": "pwa-chrome", | ||||
|       "url": "http://localhost:8080/", | ||||
|       "webRoot": "${workspaceFolder}/cast/dist", | ||||
|       "disableNetworkCache": true, | ||||
|       "preLaunchTask": "Develop Cast" | ||||
|     }, | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										208
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										208
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -1,208 +0,0 @@ | ||||
| { | ||||
|   "version": "2.0.0", | ||||
|   "tasks": [ | ||||
|     { | ||||
|       "label": "Develop Frontend", | ||||
|       "type": "gulp", | ||||
|       "task": "develop-app", | ||||
|       // Sync changes here to other tasks until issue resolved | ||||
|       // https://github.com/Microsoft/vscode/issues/61497 | ||||
|       "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": { | ||||
|         "kind": "build", | ||||
|         "isDefault": true | ||||
|       }, | ||||
|       "runOptions": { | ||||
|         "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", | ||||
|       "task": "develop-gallery", | ||||
|       "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 Demo", | ||||
|       "type": "gulp", | ||||
|       "task": "develop-demo", | ||||
|       "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 Cast", | ||||
|       "type": "gulp", | ||||
|       "task": "develop-cast", | ||||
|       "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": "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" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
| @@ -1,39 +0,0 @@ | ||||
| # 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: !latestBuild, | ||||
|   safari10: true, | ||||
|   ecma: latestBuild ? undefined : 5, | ||||
|   output: { comments: false }, | ||||
| }); | ||||
| @@ -55,6 +55,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({ | ||||
|     !latestBuild && [ | ||||
|       require("@babel/preset-env").default, | ||||
|       { | ||||
|         modules: false, | ||||
|         useBuiltIns: "entry", | ||||
|         corejs: "3.6", | ||||
|       }, | ||||
| @@ -70,6 +71,7 @@ module.exports.babelOptions = ({ latestBuild }) => ({ | ||||
|     // Only support the syntax, Webpack will handle it. | ||||
|     "@babel/plugin-syntax-import-meta", | ||||
|     "@babel/plugin-syntax-dynamic-import", | ||||
|     "@babel/plugin-syntax-top-level-await", | ||||
|     "@babel/plugin-proposal-optional-chaining", | ||||
|     "@babel/plugin-proposal-nullish-coalescing-operator", | ||||
|     [ | ||||
| @@ -117,7 +119,7 @@ BundleConfig { | ||||
| */ | ||||
|  | ||||
| module.exports.config = { | ||||
|   app({ isProdBuild, latestBuild, isStatsBuild, isWDS }) { | ||||
|   app({ isProdBuild, latestBuild, isStatsBuild }) { | ||||
|     return { | ||||
|       entry: { | ||||
|         service_worker: "./src/entrypoints/service_worker.ts", | ||||
| @@ -132,7 +134,6 @@ module.exports.config = { | ||||
|       isProdBuild, | ||||
|       latestBuild, | ||||
|       isStatsBuild, | ||||
|       isWDS, | ||||
|     }; | ||||
|   }, | ||||
|  | ||||
|   | ||||
| @@ -6,9 +6,6 @@ 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,7 +12,6 @@ require("./webpack.js"); | ||||
| require("./service-worker.js"); | ||||
| require("./entry-html.js"); | ||||
| require("./rollup.js"); | ||||
| require("./wds.js"); | ||||
|  | ||||
| gulp.task( | ||||
|   "develop-app", | ||||
| @@ -29,11 +28,7 @@ gulp.task( | ||||
|       "build-translations" | ||||
|     ), | ||||
|     "copy-static-app", | ||||
|     env.useWDS() | ||||
|       ? "wds-watch-app" | ||||
|       : env.useRollup() | ||||
|       ? "rollup-watch-app" | ||||
|       : "webpack-watch-app" | ||||
|     env.useRollup() ? "rollup-watch-app" : "webpack-watch-app" | ||||
|   ) | ||||
| ); | ||||
|  | ||||
|   | ||||
| @@ -19,7 +19,6 @@ const renderTemplate = (pth, data = {}, pathFunc = templatePath) => { | ||||
|   return compiled({ | ||||
|     ...data, | ||||
|     useRollup: env.useRollup(), | ||||
|     useWDS: env.useWDS(), | ||||
|     renderTemplate, | ||||
|   }); | ||||
| }; | ||||
| @@ -91,23 +90,10 @@ 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, | ||||
|     latestCoreJS, | ||||
|     latestCustomPanelJS, | ||||
|     latestAppJS: "/frontend_latest/app.js", | ||||
|     latestCoreJS: "/frontend_latest/core.js", | ||||
|     latestCustomPanelJS: "/frontend_latest/custom-panel.js", | ||||
|  | ||||
|     es5AppJS: "/frontend_es5/app.js", | ||||
|     es5CoreJS: "/frontend_es5/core.js", | ||||
|   | ||||
| @@ -7,6 +7,7 @@ const gulp = require("gulp"); | ||||
| const fs = require("fs"); | ||||
| const foreach = require("gulp-foreach"); | ||||
| const merge = require("gulp-merge-json"); | ||||
| const minify = require("gulp-jsonminify"); | ||||
| const rename = require("gulp-rename"); | ||||
| const transform = require("gulp-json-transform"); | ||||
| const { mapFiles } = require("../util"); | ||||
| @@ -33,10 +34,21 @@ String.prototype.rsplit = function (sep, maxsplit) { | ||||
|     : split; | ||||
| }; | ||||
|  | ||||
| // Panel translations which should be split from the core translations. | ||||
| const TRANSLATION_FRAGMENTS = Object.keys( | ||||
|   require("../../src/translations/en.json").ui.panel | ||||
| ); | ||||
| // 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", | ||||
| ]; | ||||
|  | ||||
| function recursiveFlatten(prefix, data) { | ||||
|   let output = {}; | ||||
| @@ -289,6 +301,7 @@ gulp.task("build-flattened-translations", function () { | ||||
|         return flatten(data); | ||||
|       }) | ||||
|     ) | ||||
|     .pipe(minify()) | ||||
|     .pipe( | ||||
|       rename((filePath) => { | ||||
|         if (filePath.dirname === "core") { | ||||
|   | ||||
| @@ -1,11 +0,0 @@ | ||||
| // Tasks to run Rollup | ||||
| const gulp = require("gulp"); | ||||
| const { startDevServer } = require("@web/dev-server"); | ||||
|  | ||||
| gulp.task("wds-watch-app", () => { | ||||
|   startDevServer({ | ||||
|     config: { | ||||
|       watch: true, | ||||
|     }, | ||||
|   }); | ||||
| }); | ||||
| @@ -18,14 +18,6 @@ const bothBuilds = (createConfigFunc, params) => [ | ||||
|   createConfigFunc({ ...params, latestBuild: false }), | ||||
| ]; | ||||
|  | ||||
| /** | ||||
|  * @param {{ | ||||
|  *   compiler: import("webpack").Compiler, | ||||
|  *   contentBase: string, | ||||
|  *   port: number, | ||||
|  *   listenHost?: string | ||||
|  * }} | ||||
|  */ | ||||
| const runDevServer = ({ | ||||
|   compiler, | ||||
|   contentBase, | ||||
| @@ -41,13 +33,10 @@ const runDevServer = ({ | ||||
|       throw err; | ||||
|     } | ||||
|     // Server listening | ||||
|     log( | ||||
|       "[webpack-dev-server]", | ||||
|       `Project is running at http://localhost:${port}` | ||||
|     ); | ||||
|     log("[webpack-dev-server]", `http://localhost:${port}`); | ||||
|   }); | ||||
|  | ||||
| const doneHandler = (done) => (err, stats) => { | ||||
| const handler = (done) => (err, stats) => { | ||||
|   if (err) { | ||||
|     log.error(err.stack || err); | ||||
|     if (err.details) { | ||||
| @@ -56,31 +45,22 @@ const doneHandler = (done) => (err, stats) => { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   if (stats.hasErrors() || stats.hasWarnings()) { | ||||
|     console.log(stats.toString("minimal")); | ||||
|   } | ||||
|  | ||||
|   log(`Build done @ ${new Date().toLocaleTimeString()}`); | ||||
|  | ||||
|   if (stats.hasErrors() || stats.hasWarnings()) { | ||||
|     log.warn(stats.toString("minimal")); | ||||
|   } | ||||
|  | ||||
|   if (done) { | ||||
|     done(); | ||||
|   } | ||||
| }; | ||||
|  | ||||
| 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", () => { | ||||
|   // This command will run forever because we don't close compiler | ||||
|   // we are not calling done, so this command will run forever | ||||
|   webpack(createAppConfig({ isProdBuild: false, latestBuild: true })).watch( | ||||
|     { ignored: /build-translations/ }, | ||||
|     doneHandler() | ||||
|     handler() | ||||
|   ); | ||||
|   gulp.watch( | ||||
|     path.join(paths.translations_src, "en.json"), | ||||
| @@ -88,12 +68,15 @@ gulp.task("webpack-watch-app", () => { | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| gulp.task("webpack-prod-app", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createAppConfig, { | ||||
|       isProdBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| gulp.task( | ||||
|   "webpack-prod-app", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         bothBuilds(createAppConfig, { isProdBuild: true }), | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| ); | ||||
|  | ||||
| gulp.task("webpack-dev-server-demo", () => { | ||||
| @@ -104,12 +87,17 @@ gulp.task("webpack-dev-server-demo", () => { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task("webpack-prod-demo", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createDemoConfig, { | ||||
|       isProdBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| gulp.task( | ||||
|   "webpack-prod-demo", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         bothBuilds(createDemoConfig, { | ||||
|           isProdBuild: true, | ||||
|         }), | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| ); | ||||
|  | ||||
| gulp.task("webpack-dev-server-cast", () => { | ||||
| @@ -122,30 +110,41 @@ gulp.task("webpack-dev-server-cast", () => { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task("webpack-prod-cast", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createCastConfig, { | ||||
|       isProdBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| gulp.task( | ||||
|   "webpack-prod-cast", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         bothBuilds(createCastConfig, { | ||||
|           isProdBuild: true, | ||||
|         }), | ||||
|  | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| ); | ||||
|  | ||||
| gulp.task("webpack-watch-hassio", () => { | ||||
|   // This command will run forever because we don't close compiler | ||||
|   // we are not calling done, so this command will run forever | ||||
|   webpack( | ||||
|     createHassioConfig({ | ||||
|       isProdBuild: false, | ||||
|       latestBuild: true, | ||||
|     }) | ||||
|   ).watch({}, doneHandler()); | ||||
|   ).watch({}, handler()); | ||||
| }); | ||||
|  | ||||
| gulp.task("webpack-prod-hassio", () => | ||||
|   prodBuild( | ||||
|     bothBuilds(createHassioConfig, { | ||||
|       isProdBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| gulp.task( | ||||
|   "webpack-prod-hassio", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         bothBuilds(createHassioConfig, { | ||||
|           isProdBuild: true, | ||||
|         }), | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| ); | ||||
|  | ||||
| gulp.task("webpack-dev-server-gallery", () => { | ||||
| @@ -157,11 +156,17 @@ gulp.task("webpack-dev-server-gallery", () => { | ||||
|   }); | ||||
| }); | ||||
|  | ||||
| gulp.task("webpack-prod-gallery", () => | ||||
|   prodBuild( | ||||
|     createGalleryConfig({ | ||||
|       isProdBuild: true, | ||||
|       latestBuild: true, | ||||
|     }) | ||||
|   ) | ||||
| gulp.task( | ||||
|   "webpack-prod-gallery", | ||||
|   () => | ||||
|     new Promise((resolve) => | ||||
|       webpack( | ||||
|         createGalleryConfig({ | ||||
|           isProdBuild: true, | ||||
|           latestBuild: true, | ||||
|         }), | ||||
|  | ||||
|         handler(resolve) | ||||
|       ) | ||||
|     ) | ||||
| ); | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| const path = require("path"); | ||||
| var path = require("path"); | ||||
|  | ||||
| module.exports = { | ||||
|   polymer_dir: path.resolve(__dirname, ".."), | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| 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").babel; | ||||
| const babel = require("rollup-plugin-babel"); | ||||
| const replace = require("@rollup/plugin-replace"); | ||||
| const visualizer = require("rollup-plugin-visualizer"); | ||||
| const { string } = require("rollup-plugin-string"); | ||||
| @@ -31,7 +31,6 @@ const createRollupConfig = ({ | ||||
|   isStatsBuild, | ||||
|   publicPath, | ||||
|   dontHash, | ||||
|   isWDS, | ||||
| }) => { | ||||
|   return { | ||||
|     /** | ||||
| @@ -62,7 +61,6 @@ const createRollupConfig = ({ | ||||
|           ...bundle.babelOptions({ latestBuild }), | ||||
|           extensions, | ||||
|           exclude: bundle.babelExclude(), | ||||
|           babelHelpers: isWDS ? "inline" : "bundled", | ||||
|         }), | ||||
|         string({ | ||||
|           // Import certain extensions as strings | ||||
| @@ -71,21 +69,19 @@ const createRollupConfig = ({ | ||||
|         replace( | ||||
|           bundle.definedVars({ isProdBuild, latestBuild, defineOverlay }) | ||||
|         ), | ||||
|         !isWDS && | ||||
|           manifest({ | ||||
|             publicPath, | ||||
|           }), | ||||
|         !isWDS && worker(), | ||||
|         !isWDS && dontHashPlugin({ dontHash }), | ||||
|         !isWDS && isProdBuild && terser(bundle.terserOptions(latestBuild)), | ||||
|         !isWDS && | ||||
|           isStatsBuild && | ||||
|         manifest({ | ||||
|           publicPath, | ||||
|         }), | ||||
|         worker(), | ||||
|         dontHashPlugin({ dontHash }), | ||||
|         isProdBuild && terser(bundle.terserOptions(latestBuild)), | ||||
|         isStatsBuild && | ||||
|           visualizer({ | ||||
|             // https://github.com/btd/rollup-plugin-visualizer#options | ||||
|             open: true, | ||||
|             sourcemap: true, | ||||
|           }), | ||||
|       ].filter(Boolean), | ||||
|       ], | ||||
|     }, | ||||
|     /** | ||||
|      * @type { import("rollup").OutputOptions } | ||||
| @@ -112,13 +108,12 @@ const createRollupConfig = ({ | ||||
|   }; | ||||
| }; | ||||
|  | ||||
| const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild, isWDS }) => { | ||||
| const createAppConfig = ({ isProdBuild, latestBuild, isStatsBuild }) => { | ||||
|   return createRollupConfig( | ||||
|     bundle.config.app({ | ||||
|       isProdBuild, | ||||
|       latestBuild, | ||||
|       isStatsBuild, | ||||
|       isWDS, | ||||
|     }) | ||||
|   ); | ||||
| }; | ||||
|   | ||||
| @@ -4,21 +4,6 @@ const TerserPlugin = require("terser-webpack-plugin"); | ||||
| const ManifestPlugin = require("webpack-manifest-plugin"); | ||||
| const paths = require("./paths.js"); | ||||
| const bundle = require("./bundle"); | ||||
| const log = require("fancy-log"); | ||||
|  | ||||
| class LogStartCompilePlugin { | ||||
|   ignoredFirst = false; | ||||
|  | ||||
|   apply(compiler) { | ||||
|     compiler.hooks.beforeCompile.tap("LogStartCompilePlugin", () => { | ||||
|       if (!this.ignoredFirst) { | ||||
|         this.ignoredFirst = true; | ||||
|         return; | ||||
|       } | ||||
|       log("Changes detected. Starting compilation"); | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| const createWebpackConfig = ({ | ||||
|   entry, | ||||
| @@ -60,12 +45,17 @@ const createWebpackConfig = ({ | ||||
|     optimization: { | ||||
|       minimizer: [ | ||||
|         new TerserPlugin({ | ||||
|           cache: true, | ||||
|           parallel: true, | ||||
|           extractComments: true, | ||||
|           sourceMap: true, | ||||
|           terserOptions: bundle.terserOptions(latestBuild), | ||||
|         }), | ||||
|       ], | ||||
|     }, | ||||
|     experiments: { | ||||
|       topLevelAwait: true, | ||||
|     }, | ||||
|     plugins: [ | ||||
|       new ManifestPlugin({ | ||||
|         // Only include the JS of entrypoints | ||||
| @@ -110,17 +100,7 @@ const createWebpackConfig = ({ | ||||
|         new RegExp(bundle.emptyPackages({ latestBuild }).join("|")), | ||||
|         path.resolve(paths.polymer_dir, "src/util/empty.js") | ||||
|       ), | ||||
|       // We need to change the import of the polyfill for EventTarget, so we replace the polyfill file with our customized one | ||||
|       new webpack.NormalModuleReplacementPlugin( | ||||
|         new RegExp( | ||||
|           require.resolve( | ||||
|             "lit-virtualizer/lib/uni-virtualizer/lib/polyfillLoaders/EventTarget.js" | ||||
|           ) | ||||
|         ), | ||||
|         path.resolve(paths.polymer_dir, "src/resources/EventTarget-ponyfill.js") | ||||
|       ), | ||||
|       !isProdBuild && new LogStartCompilePlugin(), | ||||
|     ].filter(Boolean), | ||||
|     ], | ||||
|     resolve: { | ||||
|       extensions: [".ts", ".js", ".json"], | ||||
|     }, | ||||
|   | ||||
| @@ -3,10 +3,22 @@ import { Lovelace } from "../../../src/panels/lovelace/types"; | ||||
| import { DemoConfig } from "./types"; | ||||
|  | ||||
| export const demoConfigs: Array<() => Promise<DemoConfig>> = [ | ||||
|   () => import("./arsaboo").then((mod) => mod.demoArsaboo), | ||||
|   () => import("./teachingbirds").then((mod) => mod.demoTeachingbirds), | ||||
|   () => import("./kernehed").then((mod) => mod.demoKernehed), | ||||
|   () => import("./jimpower").then((mod) => mod.demoJimpower), | ||||
|   () => | ||||
|     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 | ||||
|     ), | ||||
| ]; | ||||
|  | ||||
| // eslint-disable-next-line import/no-mutable-exports | ||||
|   | ||||
| @@ -7,183 +7,205 @@ export const demoLovelaceTeachingbirds: DemoConfig["lovelace"] = () => ({ | ||||
|       cards: [ | ||||
|         { type: "custom:ha-demo-card" }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           columns: 4, | ||||
|           cards: [ | ||||
|             { | ||||
|               image: "/assets/teachingbirds/isa_square.jpg", | ||||
|               type: "picture-entity", | ||||
|               show_name: false, | ||||
|               tap_action: { | ||||
|                 action: "more-info", | ||||
|               }, | ||||
|               entity: "sensor.presence_isa", | ||||
|             }, | ||||
|             { | ||||
|               image: "/assets/teachingbirds/Stefan_square.jpg", | ||||
|               type: "picture-entity", | ||||
|               show_name: false, | ||||
|               tap_action: { | ||||
|                 action: "more-info", | ||||
|               }, | ||||
|               entity: "sensor.presence_stefan", | ||||
|             }, | ||||
|             { | ||||
|               image: "/assets/teachingbirds/background_square.png", | ||||
|               elements: [ | ||||
|               cards: [ | ||||
|                 { | ||||
|                   state_image: { | ||||
|                     on: "/assets/teachingbirds/radiator_on.jpg", | ||||
|                     off: "/assets/teachingbirds/radiator_off.jpg", | ||||
|                   }, | ||||
|                   type: "image", | ||||
|                   style: { | ||||
|                     width: "100%", | ||||
|                     top: "50%", | ||||
|                     left: "50%", | ||||
|                   }, | ||||
|                   image: "/assets/teachingbirds/isa_square.jpg", | ||||
|                   type: "picture-entity", | ||||
|                   show_name: false, | ||||
|                   tap_action: { | ||||
|                     action: "more-info", | ||||
|                   }, | ||||
|                   entity: "switch.stefan_radiator_3", | ||||
|                   entity: "sensor.presence_isa", | ||||
|                 }, | ||||
|                 { | ||||
|                   style: { | ||||
|                     top: "90%", | ||||
|                     left: "50%", | ||||
|                   image: "/assets/teachingbirds/Stefan_square.jpg", | ||||
|                   type: "picture-entity", | ||||
|                   show_name: false, | ||||
|                   tap_action: { | ||||
|                     action: "more-info", | ||||
|                   }, | ||||
|                   type: "state-label", | ||||
|                   entity: "sensor.presence_stefan", | ||||
|                 }, | ||||
|                 { | ||||
|                   image: "/assets/teachingbirds/background_square.png", | ||||
|                   elements: [ | ||||
|                     { | ||||
|                       state_image: { | ||||
|                         on: "/assets/teachingbirds/radiator_on.jpg", | ||||
|                         off: "/assets/teachingbirds/radiator_off.jpg", | ||||
|                       }, | ||||
|                       type: "image", | ||||
|                       style: { | ||||
|                         width: "100%", | ||||
|                         top: "50%", | ||||
|                         left: "50%", | ||||
|                       }, | ||||
|                       tap_action: { | ||||
|                         action: "more-info", | ||||
|                       }, | ||||
|                       entity: "switch.stefan_radiator_3", | ||||
|                     }, | ||||
|                     { | ||||
|                       style: { | ||||
|                         top: "90%", | ||||
|                         left: "50%", | ||||
|                       }, | ||||
|                       type: "state-label", | ||||
|                       entity: "sensor.temperature_stefan", | ||||
|                     }, | ||||
|                   ], | ||||
|                   type: "picture-elements", | ||||
|                 }, | ||||
|                 { | ||||
|                   image: "/assets/teachingbirds/background_square.png", | ||||
|                   elements: [ | ||||
|                     { | ||||
|                       style: { | ||||
|                         "--mdc-icon-size": "100%", | ||||
|                         top: "50%", | ||||
|                         left: "50%", | ||||
|                       }, | ||||
|                       type: "icon", | ||||
|                       tap_action: { | ||||
|                         action: "navigate", | ||||
|                         navigation_path: "/lovelace/home_info", | ||||
|                       }, | ||||
|                       icon: "mdi:car", | ||||
|                     }, | ||||
|                   ], | ||||
|                   type: "picture-elements", | ||||
|                 }, | ||||
|               ], | ||||
|               type: "horizontal-stack", | ||||
|             }, | ||||
|             { | ||||
|               cards: [ | ||||
|                 { | ||||
|                   show_name: false, | ||||
|                   type: "picture-entity", | ||||
|                   name: "Alarm", | ||||
|                   image: "/assets/teachingbirds/House_square.jpg", | ||||
|                   entity: "alarm_control_panel.house", | ||||
|                 }, | ||||
|                 { | ||||
|                   name: "Roomba", | ||||
|                   image: "/assets/teachingbirds/roomba_square.jpg", | ||||
|                   show_name: false, | ||||
|                   type: "picture-entity", | ||||
|                   state_image: { | ||||
|                     "Not Today": "/assets/teachingbirds/roomba_bw_square.jpg", | ||||
|                   }, | ||||
|                   entity: "input_select.roomba_mode", | ||||
|                 }, | ||||
|                 { | ||||
|                   show_name: false, | ||||
|                   type: "picture-entity", | ||||
|                   state_image: { | ||||
|                     Mail: "/assets/teachingbirds/mailbox_square.jpg", | ||||
|                     "Package and mail": | ||||
|                       "/assets/teachingbirds/mailbox_square.jpg", | ||||
|                     Empty: "/assets/teachingbirds/mailbox_bw_square.jpg", | ||||
|                     Package: "/assets/teachingbirds/mailbox_square.jpg", | ||||
|                   }, | ||||
|                   entity: "sensor.mailbox", | ||||
|                 }, | ||||
|                 { | ||||
|                   show_name: false, | ||||
|                   state_image: { | ||||
|                     "Put out": "/assets/teachingbirds/trash_square.jpg", | ||||
|                     "Take in": "/assets/teachingbirds/trash_square.jpg", | ||||
|                   }, | ||||
|                   type: "picture-entity", | ||||
|                   image: "/assets/teachingbirds/trash_bear_bw_square.jpg", | ||||
|                   entity: "sensor.trash_status", | ||||
|                 }, | ||||
|               ], | ||||
|               type: "horizontal-stack", | ||||
|             }, | ||||
|             { | ||||
|               cards: [ | ||||
|                 { | ||||
|                   state_image: { | ||||
|                     Idle: "/assets/teachingbirds/washer_square.jpg", | ||||
|                     Running: "/assets/teachingbirds/laundry_running_square.jpg", | ||||
|                     Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg", | ||||
|                   }, | ||||
|                   entity: "input_select.washing_machine_status", | ||||
|                   type: "picture-entity", | ||||
|                   show_name: false, | ||||
|                   name: "Washer", | ||||
|                 }, | ||||
|                 { | ||||
|                   state_image: { | ||||
|                     Idle: "/assets/teachingbirds/dryer_square.jpg", | ||||
|                     Running: "/assets/teachingbirds/clothes_drying_square.jpg", | ||||
|                     Clean: "/assets/teachingbirds/folded_clothes_square.jpg", | ||||
|                   }, | ||||
|                   entity: "input_select.dryer_status", | ||||
|                   type: "picture-entity", | ||||
|                   show_name: false, | ||||
|                   name: "Dryer", | ||||
|                 }, | ||||
|                 { | ||||
|                   image: "/assets/teachingbirds/guests_square.jpg", | ||||
|                   type: "picture-entity", | ||||
|                   show_name: false, | ||||
|                   tap_action: { | ||||
|                     action: "toggle", | ||||
|                   }, | ||||
|                   entity: "input_boolean.guest_mode", | ||||
|                 }, | ||||
|                 { | ||||
|                   image: "/assets/teachingbirds/cleaning_square.jpg", | ||||
|                   type: "picture-entity", | ||||
|                   show_name: false, | ||||
|                   tap_action: { | ||||
|                     action: "toggle", | ||||
|                   }, | ||||
|                   entity: "input_boolean.cleaning_day", | ||||
|                 }, | ||||
|               ], | ||||
|               type: "horizontal-stack", | ||||
|             }, | ||||
|           ], | ||||
|           type: "vertical-stack", | ||||
|         }, | ||||
|         { | ||||
|           type: "vertical-stack", | ||||
|           cards: [ | ||||
|             { | ||||
|               cards: [ | ||||
|                 { | ||||
|                   graph: "line", | ||||
|                   type: "sensor", | ||||
|                   entity: "sensor.temperature_bedroom", | ||||
|                 }, | ||||
|                 { | ||||
|                   graph: "line", | ||||
|                   type: "sensor", | ||||
|                   name: "S's room", | ||||
|                   entity: "sensor.temperature_stefan", | ||||
|                 }, | ||||
|               ], | ||||
|               type: "picture-elements", | ||||
|               type: "horizontal-stack", | ||||
|             }, | ||||
|             { | ||||
|               image: "/assets/teachingbirds/background_square.png", | ||||
|               elements: [ | ||||
|               cards: [ | ||||
|                 { | ||||
|                   style: { | ||||
|                     "--mdc-icon-size": "100%", | ||||
|                     top: "50%", | ||||
|                     left: "50%", | ||||
|                   }, | ||||
|                   type: "icon", | ||||
|                   tap_action: { | ||||
|                     action: "navigate", | ||||
|                     navigation_path: "/lovelace/home_info", | ||||
|                   }, | ||||
|                   icon: "mdi:car", | ||||
|                   graph: "line", | ||||
|                   type: "sensor", | ||||
|                   entity: "sensor.temperature_passage", | ||||
|                 }, | ||||
|                 { | ||||
|                   graph: "line", | ||||
|                   type: "sensor", | ||||
|                   name: "Laundry", | ||||
|                   entity: "sensor.temperature_downstairs_bathroom", | ||||
|                 }, | ||||
|               ], | ||||
|               type: "picture-elements", | ||||
|             }, | ||||
|  | ||||
|             { | ||||
|               show_name: false, | ||||
|               type: "picture-entity", | ||||
|               name: "Alarm", | ||||
|               image: "/assets/teachingbirds/House_square.jpg", | ||||
|               entity: "alarm_control_panel.house", | ||||
|             }, | ||||
|             { | ||||
|               name: "Roomba", | ||||
|               image: "/assets/teachingbirds/roomba_square.jpg", | ||||
|               show_name: false, | ||||
|               type: "picture-entity", | ||||
|               state_image: { | ||||
|                 "Not Today": "/assets/teachingbirds/roomba_bw_square.jpg", | ||||
|               }, | ||||
|               entity: "input_select.roomba_mode", | ||||
|             }, | ||||
|             { | ||||
|               show_name: false, | ||||
|               type: "picture-entity", | ||||
|               state_image: { | ||||
|                 Mail: "/assets/teachingbirds/mailbox_square.jpg", | ||||
|                 "Package and mail": "/assets/teachingbirds/mailbox_square.jpg", | ||||
|                 Empty: "/assets/teachingbirds/mailbox_bw_square.jpg", | ||||
|                 Package: "/assets/teachingbirds/mailbox_square.jpg", | ||||
|               }, | ||||
|               entity: "sensor.mailbox", | ||||
|             }, | ||||
|             { | ||||
|               show_name: false, | ||||
|               state_image: { | ||||
|                 "Put out": "/assets/teachingbirds/trash_square.jpg", | ||||
|                 "Take in": "/assets/teachingbirds/trash_square.jpg", | ||||
|               }, | ||||
|               type: "picture-entity", | ||||
|               image: "/assets/teachingbirds/trash_bear_bw_square.jpg", | ||||
|               entity: "sensor.trash_status", | ||||
|             }, | ||||
|  | ||||
|             { | ||||
|               state_image: { | ||||
|                 Idle: "/assets/teachingbirds/washer_square.jpg", | ||||
|                 Running: "/assets/teachingbirds/laundry_running_square.jpg", | ||||
|                 Clean: "/assets/teachingbirds/laundry_clean_2_square.jpg", | ||||
|               }, | ||||
|               entity: "input_select.washing_machine_status", | ||||
|               type: "picture-entity", | ||||
|               show_name: false, | ||||
|               name: "Washer", | ||||
|             }, | ||||
|             { | ||||
|               state_image: { | ||||
|                 Idle: "/assets/teachingbirds/dryer_square.jpg", | ||||
|                 Running: "/assets/teachingbirds/clothes_drying_square.jpg", | ||||
|                 Clean: "/assets/teachingbirds/folded_clothes_square.jpg", | ||||
|               }, | ||||
|               entity: "input_select.dryer_status", | ||||
|               type: "picture-entity", | ||||
|               show_name: false, | ||||
|               name: "Dryer", | ||||
|             }, | ||||
|             { | ||||
|               image: "/assets/teachingbirds/guests_square.jpg", | ||||
|               type: "picture-entity", | ||||
|               show_name: false, | ||||
|               tap_action: { | ||||
|                 action: "toggle", | ||||
|               }, | ||||
|               entity: "input_boolean.guest_mode", | ||||
|             }, | ||||
|             { | ||||
|               image: "/assets/teachingbirds/cleaning_square.jpg", | ||||
|               type: "picture-entity", | ||||
|               show_name: false, | ||||
|               tap_action: { | ||||
|                 action: "toggle", | ||||
|               }, | ||||
|               entity: "input_boolean.cleaning_day", | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           columns: 2, | ||||
|           cards: [ | ||||
|             { | ||||
|               graph: "line", | ||||
|               type: "sensor", | ||||
|               entity: "sensor.temperature_bedroom", | ||||
|             }, | ||||
|             { | ||||
|               graph: "line", | ||||
|               type: "sensor", | ||||
|               name: "S's room", | ||||
|               entity: "sensor.temperature_stefan", | ||||
|             }, | ||||
|             { | ||||
|               graph: "line", | ||||
|               type: "sensor", | ||||
|               entity: "sensor.temperature_passage", | ||||
|             }, | ||||
|             { | ||||
|               graph: "line", | ||||
|               type: "sensor", | ||||
|               name: "Laundry", | ||||
|               entity: "sensor.temperature_downstairs_bathroom", | ||||
|               type: "horizontal-stack", | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|   | ||||
| @@ -9,5 +9,5 @@ export interface DemoConfig { | ||||
|   authorUrl: string; | ||||
|   lovelace: (localize: LocalizeFunc) => LovelaceConfig; | ||||
|   entities: (localize: LocalizeFunc) => Entity[]; | ||||
|   theme: () => Record<string, string> | null; | ||||
|   theme: () => { [key: string]: string } | null; | ||||
| } | ||||
|   | ||||
| @@ -7,5 +7,7 @@ import "./ha-demo"; | ||||
|  | ||||
| /* polyfill for paper-dropdown */ | ||||
| setTimeout(() => { | ||||
|   import("web-animations-js/web-animations-next-lite.min"); | ||||
|   import( | ||||
|     /* webpackChunkName: "polyfill-web-animations-next" */ "web-animations-js/web-animations-next-lite.min" | ||||
|   ); | ||||
| }, 1000); | ||||
|   | ||||
| @@ -6,11 +6,4 @@ export const mockTemplate = (hass: MockHomeAssistant) => { | ||||
|       body: { message: "Template dev tool does not work in the demo." }, | ||||
|     }) | ||||
|   ); | ||||
|   hass.mockWS("render_template", (msg, onChange) => { | ||||
|     onChange!({ | ||||
|       result: msg.template, | ||||
|       listeners: { all: false, domains: [], entities: [], time: false }, | ||||
|     }); | ||||
|     return () => {}; | ||||
|   }); | ||||
| }; | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 94 KiB | 
| @@ -21,16 +21,15 @@ class DemoCard extends PolymerElement { | ||||
|         } | ||||
|         pre { | ||||
|           width: 400px; | ||||
|           margin: 0 16px; | ||||
|           margin: 16px; | ||||
|           overflow: auto; | ||||
|           color: var(--primary-text-color); | ||||
|         } | ||||
|         @media only screen and (max-width: 800px) { | ||||
|           .root { | ||||
|             flex-direction: column; | ||||
|           } | ||||
|           pre { | ||||
|             margin: 16px 0; | ||||
|             margin-left: 0; | ||||
|           } | ||||
|         } | ||||
|       </style> | ||||
|   | ||||
| @@ -5,16 +5,11 @@ import { PolymerElement } from "@polymer/polymer/polymer-element"; | ||||
| import "../../../src/components/ha-switch"; | ||||
| import "../../../src/components/ha-formfield"; | ||||
| import "./demo-card"; | ||||
| import { applyThemesOnElement } from "../../../src/common/dom/apply_themes_on_element"; | ||||
|  | ||||
| class DemoCards extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` | ||||
|       <style> | ||||
|         #container { | ||||
|           min-height: calc(100vh - 128px); | ||||
|           background: var(--primary-background-color); | ||||
|         } | ||||
|         .cards { | ||||
|           display: flex; | ||||
|           flex-wrap: wrap; | ||||
| @@ -29,9 +24,6 @@ class DemoCards extends PolymerElement { | ||||
|         .filters { | ||||
|           margin-left: 60px; | ||||
|         } | ||||
|         ha-formfield { | ||||
|           margin-right: 16px; | ||||
|         } | ||||
|       </style> | ||||
|       <app-toolbar> | ||||
|         <div class="filters"> | ||||
| @@ -39,21 +31,16 @@ class DemoCards extends PolymerElement { | ||||
|             <ha-switch checked="[[_showConfig]]" on-change="_showConfigToggled"> | ||||
|             </ha-switch> | ||||
|           </ha-formfield> | ||||
|           <ha-formfield label="Dark theme"> | ||||
|             <ha-switch on-change="_darkThemeToggled"> </ha-switch> | ||||
|           </ha-formfield> | ||||
|         </div> | ||||
|       </app-toolbar> | ||||
|       <div id="container"> | ||||
|         <div class="cards"> | ||||
|           <template is="dom-repeat" items="[[configs]]"> | ||||
|             <demo-card | ||||
|               config="[[item]]" | ||||
|               show-config="[[_showConfig]]" | ||||
|               hass="[[hass]]" | ||||
|             ></demo-card> | ||||
|           </template> | ||||
|         </div> | ||||
|       <div class="cards"> | ||||
|         <template is="dom-repeat" items="[[configs]]"> | ||||
|           <demo-card | ||||
|             config="[[item]]" | ||||
|             show-config="[[_showConfig]]" | ||||
|             hass="[[hass]]" | ||||
|           ></demo-card> | ||||
|         </template> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
| @@ -72,12 +59,6 @@ class DemoCards extends PolymerElement { | ||||
|   _showConfigToggled(ev) { | ||||
|     this._showConfig = ev.target.checked; | ||||
|   } | ||||
|  | ||||
|   _darkThemeToggled(ev) { | ||||
|     applyThemesOnElement(this.$.container, { themes: {} }, "default", { | ||||
|       dark: ev.target.checked, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| customElements.define("demo-cards", DemoCards); | ||||
|   | ||||
| @@ -3,7 +3,7 @@ import { html } from "@polymer/polymer/lib/utils/html-tag"; | ||||
| import { PolymerElement } from "@polymer/polymer/polymer-element"; | ||||
| import "../../../src/components/ha-card"; | ||||
| import "../../../src/state-summary/state-card-content"; | ||||
| import "../../../src/dialogs/more-info/more-info-content"; | ||||
| import "./more-info-content"; | ||||
|  | ||||
| class DemoMoreInfo extends PolymerElement { | ||||
|   static get template() { | ||||
| @@ -16,19 +16,21 @@ class DemoMoreInfo extends PolymerElement { | ||||
|  | ||||
|         ha-card { | ||||
|           width: 333px; | ||||
|           padding: 20px 24px; | ||||
|         } | ||||
|  | ||||
|         state-card-content { | ||||
|           display: block; | ||||
|           margin-bottom: 16px; | ||||
|           padding: 16px; | ||||
|         } | ||||
|  | ||||
|         more-info-content { | ||||
|           padding: 0 16px; | ||||
|         } | ||||
|  | ||||
|         pre { | ||||
|           width: 400px; | ||||
|           margin: 0 16px; | ||||
|           margin: 16px; | ||||
|           overflow: auto; | ||||
|           color: var(--primary-text-color); | ||||
|         } | ||||
|  | ||||
|         @media only screen and (max-width: 800px) { | ||||
| @@ -36,7 +38,7 @@ class DemoMoreInfo extends PolymerElement { | ||||
|             flex-direction: column; | ||||
|           } | ||||
|           pre { | ||||
|             margin: 16px 0; | ||||
|             margin-left: 0; | ||||
|           } | ||||
|         } | ||||
|       </style> | ||||
|   | ||||
							
								
								
									
										73
									
								
								gallery/src/components/more-info-content.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								gallery/src/components/more-info-content.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,73 @@ | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { property, PropertyValues, UpdatingElement } from "lit-element"; | ||||
| import dynamicContentUpdater from "../../../src/common/dom/dynamic_content_updater"; | ||||
| import { stateMoreInfoType } from "../../../src/dialogs/more-info/state_more_info_control"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-alarm_control_panel"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-automation"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-camera"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-climate"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-configurator"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-counter"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-cover"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-default"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-fan"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-group"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-humidifier"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-input_datetime"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-light"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-lock"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-media_player"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-person"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-script"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-sun"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-timer"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-vacuum"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-water_heater"; | ||||
| import "../../../src/dialogs/more-info/controls/more-info-weather"; | ||||
| import { HomeAssistant } from "../../../src/types"; | ||||
|  | ||||
| class MoreInfoContent extends UpdatingElement { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property() public stateObj?: HassEntity; | ||||
|  | ||||
|   private _detachedChild?: ChildNode; | ||||
|  | ||||
|   protected firstUpdated(): void { | ||||
|     this.style.position = "relative"; | ||||
|     this.style.display = "block"; | ||||
|   } | ||||
|  | ||||
|   // This is not a lit element, but an updating element, so we implement update | ||||
|   protected update(changedProps: PropertyValues): void { | ||||
|     super.update(changedProps); | ||||
|     const stateObj = this.stateObj; | ||||
|     const hass = this.hass; | ||||
|  | ||||
|     if (!stateObj || !hass) { | ||||
|       if (this.lastChild) { | ||||
|         this._detachedChild = this.lastChild; | ||||
|         // Detach child to prevent it from doing work. | ||||
|         this.removeChild(this.lastChild); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (this._detachedChild) { | ||||
|       this.appendChild(this._detachedChild); | ||||
|       this._detachedChild = undefined; | ||||
|     } | ||||
|  | ||||
|     const moreInfoType = | ||||
|       stateObj.attributes && "custom_ui_more_info" in stateObj.attributes | ||||
|         ? stateObj.attributes.custom_ui_more_info | ||||
|         : "more-info-" + stateMoreInfoType(stateObj); | ||||
|  | ||||
|     dynamicContentUpdater(this, moreInfoType.toUpperCase(), { | ||||
|       hass, | ||||
|       stateObj, | ||||
|     }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| customElements.define("more-info-content", MoreInfoContent); | ||||
| @@ -6,8 +6,6 @@ export const createMediaPlayerEntities = () => [ | ||||
|     media_content_type: "music", | ||||
|     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, | ||||
|     entity_picture: "/images/album_cover_2.jpg", | ||||
|     media_duration: 300, | ||||
| @@ -16,16 +14,13 @@ export const createMediaPlayerEntities = () => [ | ||||
|       // 23 seconds in | ||||
|       new Date().getTime() - 23000 | ||||
|     ).toISOString(), | ||||
|     volume_level: 0.5, | ||||
|   }), | ||||
|   getEntity("media_player", "music_playing", "playing", { | ||||
|     friendly_name: "Playing The Music", | ||||
|     media_content_type: "music", | ||||
|     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, | ||||
|     supported_features: 64063, | ||||
|     entity_picture: "/images/album_cover.jpg", | ||||
|     media_duration: 300, | ||||
|     media_position: 0, | ||||
| @@ -33,7 +28,6 @@ export const createMediaPlayerEntities = () => [ | ||||
|       // 23 seconds in | ||||
|       new Date().getTime() - 23000 | ||||
|     ).toISOString(), | ||||
|     volume_level: 0.5, | ||||
|   }), | ||||
|   getEntity("media_player", "stream_playing", "playing", { | ||||
|     friendly_name: "Playing the Stream", | ||||
| @@ -41,125 +35,50 @@ export const createMediaPlayerEntities = () => [ | ||||
|     media_title: "Epic sax guy 10 hours", | ||||
|     app_name: "YouTube", | ||||
|     entity_picture: "/images/frenck.jpg", | ||||
|     // Pause + Next Track + Play + Browse Media | ||||
|     supported_features: 147489, | ||||
|     supported_features: 33, | ||||
|   }), | ||||
|   getEntity("media_player", "stream_paused", "paused", { | ||||
|     friendly_name: "Paused the Stream", | ||||
|     media_content_type: "movie", | ||||
|     media_title: "Epic sax guy 10 hours", | ||||
|     app_name: "YouTube", | ||||
|     entity_picture: "/images/frenck.jpg", | ||||
|     // Pause + Next Track + Play | ||||
|     supported_features: 16417, | ||||
|   }), | ||||
|   getEntity("media_player", "stream_playing_previous", "playing", { | ||||
|     friendly_name: 'Playing the Stream (with "previous" support)', | ||||
|     media_content_type: "movie", | ||||
|     media_title: "Epic sax guy 10 hours", | ||||
|     app_name: "YouTube", | ||||
|     entity_picture: "/images/frenck.jpg", | ||||
|     // Pause + Previous Track + Play | ||||
|     supported_features: 16401, | ||||
|   }), | ||||
|   getEntity("media_player", "tv_playing", "playing", { | ||||
|     friendly_name: "Playing non-skip TV Show", | ||||
|   getEntity("media_player", "living_room", "playing", { | ||||
|     friendly_name: "Pause, No skip, tvshow", | ||||
|     media_content_type: "tvshow", | ||||
|     media_title: "Chapter 1", | ||||
|     media_series_title: "House of Cards", | ||||
|     app_name: "Netflix", | ||||
|     entity_picture: "/images/netflix.jpg", | ||||
|     // Pause | ||||
|     supported_features: 1, | ||||
|   }), | ||||
|   getEntity("media_player", "sonos_idle", "idle", { | ||||
|     friendly_name: "Sonos Idle", | ||||
|     // Pause + Seek + Volume Set + Volume Mute + Previous Track + Next Track + Play Media + | ||||
|     // Select Source + Stop + Clear + Play + Shuffle Set | ||||
|     supported_features: 64063, | ||||
|     volume_level: 0.33, | ||||
|     is_volume_muted: true, | ||||
|   }), | ||||
|   getEntity("media_player", "idle_browse_media", "idle", { | ||||
|     friendly_name: "Idle waiting for Browse Media (e.g. Spotify)", | ||||
|     // Pause + Seek + Volume Set + Previous Track + Next Track + Play Media + | ||||
|     // Select Source + Play + Shuffle Set + Browse Media | ||||
|     supported_features: 182839, | ||||
|     volume_level: 0.79, | ||||
|   }), | ||||
|   getEntity("media_player", "theater_off", "off", { | ||||
|   getEntity("media_player", "theater", "off", { | ||||
|     friendly_name: "TV Off", | ||||
|     // On + Off + Play + Next + Pause | ||||
|     supported_features: 16801, | ||||
|   }), | ||||
|   getEntity("media_player", "theater_on", "on", { | ||||
|     friendly_name: "TV On", | ||||
|     // On + Off + Play + Next + Pause | ||||
|     supported_features: 16801, | ||||
|   }), | ||||
|   getEntity("media_player", "theater_off_static", "off", { | ||||
|     friendly_name: "TV Off (cannot be switched on)", | ||||
|     // Off + Next + Pause | ||||
|     supported_features: 289, | ||||
|   }), | ||||
|   getEntity("media_player", "theater_on_static", "on", { | ||||
|     friendly_name: "TV On (cannot be switched off)", | ||||
|     // On + Next + Pause | ||||
|     supported_features: 161, | ||||
|   }), | ||||
|   getEntity("media_player", "android_cast", "playing", { | ||||
|     friendly_name: "Casting App (no supported features)", | ||||
|     friendly_name: "Casting App", | ||||
|     media_title: "Android Screen Casting", | ||||
|     app_name: "Screen Mirroring", | ||||
|   }), | ||||
|   getEntity("media_player", "image_display", "playing", { | ||||
|     friendly_name: "Digital Picture Frame", | ||||
|     media_content_type: "image", | ||||
|     media_title: "Famous Painting", | ||||
|     media_artist: "Famous Artist", | ||||
|     entity_picture: "/images/sunflowers.jpg", | ||||
|     // On + Off + Browse Media | ||||
|     supported_features: 131456, | ||||
|     // supported_features: 21437, | ||||
|   }), | ||||
|   getEntity("media_player", "unavailable", "unavailable", { | ||||
|     friendly_name: "Player Unavailable", | ||||
|     // Pause + Volume Set + Volume Mute + Previous Track + Next Track + | ||||
|     // Play Media + Stop + Play | ||||
|     supported_features: 21437, | ||||
|   }), | ||||
|   getEntity("media_player", "unknown", "unknown", { | ||||
|     friendly_name: "Player Unknown", | ||||
|     // Pause + Volume Set + Volume Mute + Previous Track + Next Track + | ||||
|     // Play Media + Stop + Play | ||||
|     supported_features: 21437, | ||||
|   }), | ||||
|   getEntity("media_player", "playing", "playing", { | ||||
|     friendly_name: "Player Playing (no Pause support)", | ||||
|     // Volume Set + Volume Mute + Previous Track + Next Track + | ||||
|     // Play Media + Stop + Play | ||||
|     supported_features: 21436, | ||||
|     volume_level: 1, | ||||
|   }), | ||||
|   getEntity("media_player", "idle", "idle", { | ||||
|     friendly_name: "Player Idle", | ||||
|     // Pause + Volume Set + Volume Mute + Previous Track + Next Track + | ||||
|     // Play Media + Stop + Play | ||||
|     supported_features: 21437, | ||||
|     volume_level: 0, | ||||
|   }), | ||||
|   getEntity("media_player", "receiver_on", "on", { | ||||
|     source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], | ||||
|     volume_level: 0.63, | ||||
|     is_volume_muted: false, | ||||
|     source: "TV", | ||||
|     friendly_name: "Receiver (selectable sources)", | ||||
|     // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode | ||||
|     friendly_name: "Receiver", | ||||
|     supported_features: 84364, | ||||
|   }), | ||||
|   getEntity("media_player", "receiver_off", "off", { | ||||
|     source_list: ["AirPlay", "Blu-Ray", "TV", "USB", "iPod (USB)"], | ||||
|     friendly_name: "Receiver (selectable sources)", | ||||
|     // Volume Set + Volume Mute + On + Off + Select Source + Play + Sound Mode | ||||
|     friendly_name: "Receiver", | ||||
|     supported_features: 84364, | ||||
|   }), | ||||
| ]; | ||||
|   | ||||
| @@ -15,10 +15,6 @@ const ENTITIES = [ | ||||
|   getEntity("alarm_control_panel", "unavailable", "unavailable", { | ||||
|     friendly_name: "Alarm", | ||||
|   }), | ||||
|   getEntity("alarm_control_panel", "alarm_code", "disarmed", { | ||||
|     friendly_name: "Alarm", | ||||
|     code_format: "number", | ||||
|   }), | ||||
| ]; | ||||
|  | ||||
| const CONFIGS = [ | ||||
| @@ -34,14 +30,7 @@ const CONFIGS = [ | ||||
|     config: ` | ||||
| - type: alarm-panel | ||||
|   entity: alarm_control_panel.alarm_armed | ||||
|   name: My Alarm | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Code Example", | ||||
|     config: ` | ||||
| - type: alarm-panel | ||||
|   entity: alarm_control_panel.alarm_code | ||||
|   title: My Alarm | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -73,7 +62,13 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoAlarmPanelEntity extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -82,17 +77,14 @@ class DemoAlarmPanelEntity extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   public ready() { | ||||
|     super.ready(); | ||||
|     this._setupDemo(); | ||||
|   } | ||||
|  | ||||
|   private async _setupDemo() { | ||||
|     const hass = provideHass(this.$.demos); | ||||
|     await hass.updateTranslations(null, "en"); | ||||
|     hass.updateTranslations(null, "en"); | ||||
|     hass.addEntities(ENTITIES); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -55,7 +55,13 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoConditional extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -64,6 +70,7 @@ class DemoConditional extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -20,10 +20,10 @@ const CONFIGS = [ | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "With Name (defined in card)", | ||||
|     heading: "With Name", | ||||
|     config: ` | ||||
| - type: button | ||||
|   name: Custom Name | ||||
|   name: Bedroom | ||||
|   entity: light.bed_light | ||||
|     `, | ||||
|   }, | ||||
| @@ -32,7 +32,7 @@ const CONFIGS = [ | ||||
|     config: ` | ||||
| - type: button | ||||
|   entity: light.bed_light | ||||
|   icon: mdi:tools | ||||
|   icon: mdi:hotel | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -71,7 +71,13 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoButtonEntity extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -80,6 +86,7 @@ class DemoButtonEntity extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
| @@ -91,4 +98,4 @@ class DemoButtonEntity extends PolymerElement { | ||||
|   } | ||||
| } | ||||
|  | ||||
| customElements.define("demo-hui-entity-button-card", DemoButtonEntity); | ||||
| customElements.define("demo-hui-button-card", DemoButtonEntity); | ||||
|   | ||||
| @@ -7,10 +7,7 @@ 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", { | ||||
|     unit_of_measurement: "%", | ||||
|   }), | ||||
| @@ -23,10 +20,16 @@ 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 | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -35,7 +38,6 @@ const CONFIGS = [ | ||||
| - type: gauge | ||||
|   entity: sensor.outside_temperature | ||||
|   unit_of_measurement: C | ||||
|   name: Outside Temperature | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -43,45 +45,19 @@ const CONFIGS = [ | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   entity: sensor.brightness | ||||
|   name: Brightness Low | ||||
|   severity: | ||||
|     red: 75 | ||||
|     red: 32 | ||||
|     green: 0 | ||||
|     yellow: 50 | ||||
|     yellow: 23 | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     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", | ||||
|     heading: "Setting Min and Max Values", | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   entity: sensor.brightness | ||||
|   name: Brightness | ||||
|   min: 0 | ||||
|   max: 15 | ||||
|   max: 38 | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -98,13 +74,6 @@ const CONFIGS = [ | ||||
|   entity: plant.bonsai | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Unavailable entity", | ||||
|     config: ` | ||||
| - type: gauge | ||||
|   entity: sensor.not_working | ||||
|     `, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| class DemoGaugeEntity extends PolymerElement { | ||||
|   | ||||
| @@ -8,43 +8,29 @@ import "../components/demo-cards"; | ||||
| const ENTITIES = [ | ||||
|   getEntity("light", "bed_light", "on", { | ||||
|     friendly_name: "Bed Light", | ||||
|     brightness: 255, | ||||
|     brightness: 130, | ||||
|   }), | ||||
|   getEntity("light", "dim_on", "on", { | ||||
|     friendly_name: "Dining Room", | ||||
|     supported_features: 1, | ||||
|     brightness: 100, | ||||
|   }), | ||||
|   getEntity("light", "dim_off", "off", { | ||||
|     friendly_name: "Dining Room", | ||||
|   getEntity("light", "dim", "off", { | ||||
|     supported_features: 1, | ||||
|   }), | ||||
|   getEntity("light", "unavailable", "unavailable", { | ||||
|     friendly_name: "Lost Light", | ||||
|     supported_features: 1, | ||||
|   }), | ||||
| ]; | ||||
|  | ||||
| const CONFIGS = [ | ||||
|   { | ||||
|     heading: "Switchable Light", | ||||
|     heading: "Basic example", | ||||
|     config: ` | ||||
| - type: light | ||||
|   entity: light.bed_light | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Dimmable Light On", | ||||
|     heading: "Dim", | ||||
|     config: ` | ||||
| - type: light | ||||
|   entity: light.dim_on | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Dimmable Light Off", | ||||
|     config: ` | ||||
| - type: light | ||||
|   entity: light.dim_off | ||||
|   entity: light.dim | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|   | ||||
| @@ -163,7 +163,13 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoMap extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -172,6 +178,7 @@ class DemoMap extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,8 +1,6 @@ | ||||
| import { html } from "@polymer/polymer/lib/utils/html-tag"; | ||||
| /* eslint-plugin-disable lit */ | ||||
| import { PolymerElement } from "@polymer/polymer/polymer-element"; | ||||
| import { mockTemplate } from "../../../demo/src/stubs/template"; | ||||
| import { provideHass } from "../../../src/fake_data/provide_hass"; | ||||
| import "../components/demo-cards"; | ||||
|  | ||||
| const CONFIGS = [ | ||||
| @@ -256,7 +254,7 @@ const CONFIGS = [ | ||||
|  | ||||
| class DemoMarkdown extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|     return html` <demo-cards configs="[[_configs]]"></demo-cards> `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -267,12 +265,6 @@ class DemoMarkdown extends PolymerElement { | ||||
|       }, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   public ready() { | ||||
|     super.ready(); | ||||
|     const hass = provideHass(this.$.demos); | ||||
|     mockTemplate(hass); | ||||
|   } | ||||
| } | ||||
|  | ||||
| customElements.define("demo-hui-markdown-card", DemoMarkdown); | ||||
|   | ||||
| @@ -7,61 +7,40 @@ import { createMediaPlayerEntities } from "../data/media_players"; | ||||
|  | ||||
| const CONFIGS = [ | ||||
|   { | ||||
|     heading: "Paused Music", | ||||
|     heading: "Paused music", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.music_paused | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Playing Music", | ||||
|     heading: "Playing music", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.music_playing | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Playing Stream", | ||||
|     heading: "Playing stream", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.stream_playing | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Paused Stream", | ||||
|     heading: "Pause, No skip, tvshow", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.stream_paused | ||||
|     entity: media_player.living_room | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: 'Playing Stream (with "previous" support)', | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.stream_playing_previous | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Playing non-skip TV Show", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.tv_playing | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Screen Casting", | ||||
|     heading: "Screen casting", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.android_cast | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Digital Picture Frame", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.image_display | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Sonos Idle", | ||||
|     config: ` | ||||
| @@ -69,53 +48,11 @@ const CONFIGS = [ | ||||
|     entity: media_player.sonos_idle | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Idle waiting for Browse Media", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.idle_browse_media | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Player Off", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.theater_off | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Player On", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.theater_on | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Player Off (cannot be switched on)", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.theater_off_static | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Player On (cannot be switched off)", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.theater_on_static | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Player Idle", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.idle | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Player Playing", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.playing | ||||
|     entity: media_player.theater | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
| @@ -133,34 +70,30 @@ const CONFIGS = [ | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Receiver On (selectable sources)", | ||||
|     heading: "Receiver On", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     entity: media_player.receiver_on | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Receiver Off (selectable sources)", | ||||
|     heading: "Receiver Off", | ||||
|     config: ` | ||||
|   - type: media-control | ||||
|     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" configs="[[_configs]]"></demo-cards> `; | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -169,6 +102,7 @@ class DemoHuiMediControlCard extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -12,52 +12,36 @@ const CONFIGS = [ | ||||
| - type: entities | ||||
|   entities: | ||||
|     - entity: media_player.music_paused | ||||
|       name: Paused Music | ||||
|       name: Paused music | ||||
|     - entity: media_player.music_playing | ||||
|       name: Playing Music | ||||
|       name: Playing music | ||||
|     - entity: media_player.stream_playing | ||||
|       name: Playing Stream | ||||
|     - entity: media_player.stream_paused | ||||
|       name: Paused Stream | ||||
|     - entity: media_player.stream_playing_previous | ||||
|       name: Playing Stream (with "previous" support) | ||||
|     - entity: media_player.tv_playing | ||||
|       name: Playing non-skip TV Show | ||||
|       name: Paused, no play | ||||
|     - entity: media_player.living_room | ||||
|       name: Pause, No skip, tvshow | ||||
|     - entity: media_player.android_cast | ||||
|       name: Screen casting | ||||
|     - entity: media_player.image_display | ||||
|       name: Digital Picture Frame   | ||||
|     - entity: media_player.sonos_idle | ||||
|       name: Sonos Idle   | ||||
|     - entity: media_player.idle_browse_media | ||||
|       name: Idle waiting for Browse Media | ||||
|     - entity: media_player.theater_off | ||||
|       name: Chromcast Idle | ||||
|     - entity: media_player.theater | ||||
|       name: Player Off | ||||
|     - entity: media_player.theater_on | ||||
|       name: Player On | ||||
|     - entity: media_player.theater_off_static | ||||
|       name: Player Off (cannot be switched on) | ||||
|     - entity: media_player.theater_on_static | ||||
|       name: Player On (cannot be switched off)   | ||||
|     - entity: media_player.idle | ||||
|       name: Player Idle | ||||
|     - entity: media_player.playing | ||||
|       name: Player Playing | ||||
|     - entity: media_player.unavailable | ||||
|       name: Player Unavailable | ||||
|     - entity: media_player.unknown | ||||
|       name: Player Unknown | ||||
|     - entity: media_player.receiver_on | ||||
|       name: Receiver On (selectable sources) | ||||
|     - entity: media_player.receiver_off | ||||
|       name: Receiver Off (selectable sources) | ||||
|     `, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| class DemoHuiMediaPlayerRows extends PolymerElement { | ||||
|   static get template() { | ||||
|     return html` <demo-cards id="demos" configs="[[_configs]]"></demo-cards> `; | ||||
|     return html` | ||||
|       <demo-cards | ||||
|         id="demos" | ||||
|         hass="[[hass]]" | ||||
|         configs="[[_configs]]" | ||||
|       ></demo-cards> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get properties() { | ||||
| @@ -66,6 +50,7 @@ class DemoHuiMediaPlayerRows extends PolymerElement { | ||||
|         type: Object, | ||||
|         value: CONFIGS, | ||||
|       }, | ||||
|       hass: Object, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { html } from "@polymer/polymer/lib/utils/html-tag"; | ||||
| /* eslint-plugin-disable lit */ | ||||
| import { PolymerElement } from "@polymer/polymer/polymer-element"; | ||||
| import { mockHistory } from "../../../demo/src/stubs/history"; | ||||
| import { getEntity } from "../../../src/fake_data/entity"; | ||||
| import { provideHass } from "../../../src/fake_data/provide_hass"; | ||||
| import "../components/demo-cards"; | ||||
| @@ -37,10 +36,6 @@ const ENTITIES = [ | ||||
|     battery: 71, | ||||
|     friendly_name: "Home Boy", | ||||
|   }), | ||||
|   getEntity("sensor", "illumination", "23", { | ||||
|     friendly_name: "Illumination", | ||||
|     unit_of_measurement: "lx", | ||||
|   }), | ||||
| ]; | ||||
|  | ||||
| const CONFIGS = [ | ||||
| @@ -94,42 +89,6 @@ const CONFIGS = [ | ||||
|       entity: light.bed_light | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Default Grid", | ||||
|     config: ` | ||||
| - type: grid | ||||
|   cards: | ||||
|     - type: entity | ||||
|       entity: light.kitchen_lights | ||||
|     - type: entity | ||||
|       entity: light.bed_light | ||||
|     - type: entity | ||||
|       entity: device_tracker.demo_paulus | ||||
|     - type: sensor | ||||
|       entity: sensor.illumination | ||||
|       graph: line | ||||
|     - type: entity | ||||
|       entity: device_tracker.demo_anne_therese | ||||
|     `, | ||||
|   }, | ||||
|   { | ||||
|     heading: "Non-square Grid with 2 columns", | ||||
|     config: ` | ||||
| - type: grid | ||||
|   columns: 2 | ||||
|   square: false | ||||
|   cards: | ||||
|     - type: entity | ||||
|       entity: light.kitchen_lights | ||||
|     - type: entity | ||||
|       entity: light.bed_light | ||||
|     - type: entity | ||||
|       entity: device_tracker.demo_paulus | ||||
|     - type: sensor | ||||
|       entity: sensor.illumination | ||||
|       graph: line | ||||
|     `, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
| class DemoStack extends PolymerElement { | ||||
| @@ -151,7 +110,6 @@ class DemoStack extends PolymerElement { | ||||
|     const hass = provideHass(this.$.demos); | ||||
|     hass.updateTranslations(null, "en"); | ||||
|     hass.addEntities(ENTITIES); | ||||
|     mockHistory(hass); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import { SUPPORT_BRIGHTNESS } from "../../../src/data/light"; | ||||
| import { getEntity } from "../../../src/fake_data/entity"; | ||||
| import { provideHass } from "../../../src/fake_data/provide_hass"; | ||||
| import "../components/demo-more-infos"; | ||||
| import "../../../src/dialogs/more-info/more-info-content"; | ||||
| import "../components/more-info-content"; | ||||
|  | ||||
| const ENTITIES = [ | ||||
|   getEntity("light", "bed_light", "on", { | ||||
| @@ -40,12 +40,8 @@ class DemoMoreInfoLight extends PolymerElement { | ||||
|  | ||||
|   public ready() { | ||||
|     super.ready(); | ||||
|     this._setupDemo(); | ||||
|   } | ||||
|  | ||||
|   private async _setupDemo() { | ||||
|     const hass = provideHass(this); | ||||
|     await hass.updateTranslations(null, "en"); | ||||
|     hass.updateTranslations(null, "en"); | ||||
|     hass.addEntities(ENTITIES); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -20,47 +20,48 @@ 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); | ||||
|         } | ||||
|       p a { | ||||
|         color: var(--primary-color); | ||||
|       } | ||||
|  | ||||
|       a { | ||||
|         color: var(--primary-text-color); | ||||
|         text-decoration: none; | ||||
|       } | ||||
|  | ||||
|         a { | ||||
|           color: var(--primary-text-color); | ||||
|           text-decoration: none; | ||||
|         } | ||||
|       </style> | ||||
|  | ||||
|       <app-header-layout> | ||||
| @@ -69,42 +70,32 @@ 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. | ||||
|                     Check <a href='https://www.home-assistant.io/lovelace'>the official website</a> for instructions on how to get started with Lovelace.</a>. | ||||
|                   </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> | ||||
| @@ -113,14 +104,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> | ||||
| @@ -129,14 +120,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> | ||||
| @@ -148,10 +139,7 @@ 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,8 +27,6 @@ declare global { | ||||
|   } | ||||
| } | ||||
|  | ||||
| const MAX_FILE_SIZE = 1 * 1024 * 1024 * 1024; // 1GB | ||||
|  | ||||
| @customElement("hassio-upload-snapshot") | ||||
| export class HassioUploadSnapshot extends LitElement { | ||||
|   public hass!: HomeAssistant; | ||||
| @@ -53,20 +51,6 @@ 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 { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { HassioAddonInfo } from "../../../src/data/hassio/addon"; | ||||
| 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 supervisor!: Supervisor; | ||||
|   @property({ attribute: false }) public addons?: HassioAddonInfo[]; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <div class="content"> | ||||
|         <h1>Add-ons</h1> | ||||
|         <div class="card-group"> | ||||
|           ${!this.supervisor.supervisor.addons?.length | ||||
|           ${!this.addons?.length | ||||
|             ? html` | ||||
|                 <ha-card> | ||||
|                   <div class="card-content"> | ||||
| @@ -41,7 +41,7 @@ class HassioAddons extends LitElement { | ||||
|                   </div> | ||||
|                 </ha-card> | ||||
|               ` | ||||
|             : this.supervisor.supervisor.addons | ||||
|             : this.addons | ||||
|                 .sort((a, b) => compare(a.name, b.name)) | ||||
|                 .map( | ||||
|                   (addon) => html` | ||||
|   | ||||
| @@ -7,7 +7,11 @@ import { | ||||
|   property, | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { HassioHassOSInfo } from "../../../src/data/hassio/host"; | ||||
| import { | ||||
|   HassioHomeAssistantInfo, | ||||
|   HassioSupervisorInfo, | ||||
| } from "../../../src/data/hassio/supervisor"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| @@ -19,12 +23,16 @@ 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 | ||||
| @@ -39,11 +47,13 @@ class HassioDashboard extends LitElement { | ||||
|         <div class="content"> | ||||
|           <hassio-update | ||||
|             .hass=${this.hass} | ||||
|             .supervisor=${this.supervisor} | ||||
|             .hassInfo=${this.hassInfo} | ||||
|             .supervisorInfo=${this.supervisorInfo} | ||||
|             .hassOsInfo=${this.hassOsInfo} | ||||
|           ></hassio-update> | ||||
|           <hassio-addons | ||||
|             .hass=${this.hass} | ||||
|             .supervisor=${this.supervisor} | ||||
|             .addons=${this.supervisorInfo.addons} | ||||
|           ></hassio-addons> | ||||
|         </div> | ||||
|       </hass-tabs-subpage> | ||||
|   | ||||
| @@ -23,7 +23,6 @@ import { | ||||
|   HassioHomeAssistantInfo, | ||||
|   HassioSupervisorInfo, | ||||
| } from "../../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| @@ -36,20 +35,31 @@ import { hassioStyle } from "../resources/hassio-style"; | ||||
| export class HassioUpdate extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|   @property({ attribute: false }) public hassInfo?: HassioHomeAssistantInfo; | ||||
|  | ||||
|   private _pendingUpdates = memoizeOne((supervisor: Supervisor): number => { | ||||
|     return Object.keys(supervisor).filter( | ||||
|       (value) => supervisor[value].update_available | ||||
|     ).length; | ||||
|   }); | ||||
|   @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; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this.supervisor) { | ||||
|       return html``; | ||||
|     } | ||||
|     const updatesAvailable = this._pendingUpdates( | ||||
|       this.hassInfo, | ||||
|       this.supervisorInfo, | ||||
|       this.hassOsInfo | ||||
|     ); | ||||
|  | ||||
|     const updatesAvailable = this._pendingUpdates(this.supervisor); | ||||
|     if (!updatesAvailable) { | ||||
|       return html``; | ||||
|     } | ||||
| @@ -64,24 +74,26 @@ export class HassioUpdate extends LitElement { | ||||
|         <div class="card-group"> | ||||
|           ${this._renderUpdateCard( | ||||
|             "Home Assistant Core", | ||||
|             this.supervisor.core, | ||||
|             this.hassInfo!, | ||||
|             "hassio/homeassistant/update", | ||||
|             `https://${ | ||||
|               this.supervisor.core.version_latest.includes("b") ? "rc" : "www" | ||||
|               this.hassInfo?.version_latest.includes("b") ? "rc" : "www" | ||||
|             }.home-assistant.io/latest-release-notes/` | ||||
|           )} | ||||
|           ${this._renderUpdateCard( | ||||
|             "Supervisor", | ||||
|             this.supervisor.supervisor, | ||||
|             this.supervisorInfo!, | ||||
|             "hassio/supervisor/update", | ||||
|             `https://github.com//home-assistant/hassio/releases/tag/${this.supervisor.supervisor.version_latest}` | ||||
|             `https://github.com//home-assistant/hassio/releases/tag/${ | ||||
|               this.supervisorInfo!.version_latest | ||||
|             }` | ||||
|           )} | ||||
|           ${this.supervisor.host.features.includes("hassos") | ||||
|           ${this.hassOsInfo | ||||
|             ? this._renderUpdateCard( | ||||
|                 "Operating System", | ||||
|                 this.supervisor.os, | ||||
|                 this.hassOsInfo, | ||||
|                 "hassio/os/update", | ||||
|                 `https://github.com//home-assistant/hassos/releases/tag/${this.supervisor.os.version_latest}` | ||||
|                 `https://github.com//home-assistant/hassos/releases/tag/${this.hassOsInfo.version_latest}` | ||||
|               ) | ||||
|             : ""} | ||||
|         </div> | ||||
|   | ||||
| @@ -11,7 +11,10 @@ export const showHassioMarkdownDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-markdown", | ||||
|     dialogImport: () => import("./dialog-hassio-markdown"), | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-markdown" */ "./dialog-hassio-markdown" | ||||
|       ), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| 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"; | ||||
| @@ -18,22 +16,18 @@ 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, | ||||
| @@ -44,51 +38,54 @@ 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 _accessPoints?: AccessPoints; | ||||
|  | ||||
|   @internalProperty() private _curTabIndex = 0; | ||||
|  | ||||
|   @internalProperty() private _dirty = false; | ||||
|  | ||||
|   @internalProperty() private _interface?: NetworkInterface; | ||||
|  | ||||
|   @internalProperty() private _interfaces!: NetworkInterface[]; | ||||
|   @internalProperty() private _prosessing = false; | ||||
|  | ||||
|   @internalProperty() private _params?: HassioNetworkDialogParams; | ||||
|  | ||||
|   @internalProperty() private _processing = false; | ||||
|   @internalProperty() private _network!: { | ||||
|     interface: string; | ||||
|     data: NetworkInterface; | ||||
|   }[]; | ||||
|  | ||||
|   @internalProperty() private _scanning = false; | ||||
|   @internalProperty() private _curTabIndex = 0; | ||||
|  | ||||
|   @internalProperty() private _wifiConfiguration?: WifiConfiguration; | ||||
|   @internalProperty() private _device?: { | ||||
|     interface: string; | ||||
|     data: NetworkInterface; | ||||
|   }; | ||||
|  | ||||
|   @internalProperty() private _dirty = false; | ||||
|  | ||||
|   public async showDialog(params: HassioNetworkDialogParams): Promise<void> { | ||||
|     this._params = params; | ||||
|     this._dirty = false; | ||||
|     this._curTabIndex = 0; | ||||
|     this._interfaces = params.network.interfaces.sort((a, b) => { | ||||
|       return a.primary > b.primary ? -1 : 1; | ||||
|     }); | ||||
|     this._interface = { ...this._interfaces[this._curTabIndex] }; | ||||
|  | ||||
|     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); | ||||
|     await this.updateComplete; | ||||
|   } | ||||
|  | ||||
|   public closeDialog(): void { | ||||
|     this._params = undefined; | ||||
|     this._processing = false; | ||||
|     this._prosessing = false; | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this._params || !this._interface) { | ||||
|     if (!this._params || !this._network) { | ||||
|       return html``; | ||||
|     } | ||||
|  | ||||
| @@ -110,11 +107,11 @@ export class DialogHassioNetwork extends LitElement | ||||
|               <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||
|             </mwc-icon-button> | ||||
|           </ha-header-bar> | ||||
|           ${this._interfaces.length > 1 | ||||
|           ${this._network.length > 1 | ||||
|             ? html` <mwc-tab-bar | ||||
|                 .activeIndex=${this._curTabIndex} | ||||
|                 @MDCTabBar:activated=${this._handleTabActivated} | ||||
|                 >${this._interfaces.map( | ||||
|                 >${this._network.map( | ||||
|                   (device) => | ||||
|                     html`<mwc-tab | ||||
|                       .id=${device.interface} | ||||
| @@ -132,302 +129,81 @@ export class DialogHassioNetwork extends LitElement | ||||
|  | ||||
|   private _renderTab() { | ||||
|     return html` <div class="form container"> | ||||
|         ${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._processing | ||||
|             ? html`<ha-circular-progress active size="small"> | ||||
|               </ha-circular-progress>` | ||||
|             : "Save"} | ||||
|         </mwc-button> | ||||
|       </div>`; | ||||
|   } | ||||
|  | ||||
|   private _selectAP(event) { | ||||
|     this._wifiConfiguration = event.currentTarget.ap; | ||||
|     this._dirty = true; | ||||
|   } | ||||
|  | ||||
|   private async _scanForAP() { | ||||
|     if (!this._interface) { | ||||
|       return; | ||||
|     } | ||||
|     this._scanning = true; | ||||
|     try { | ||||
|       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 | ||||
|         <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="address" | ||||
|                 id="ip_address" | ||||
|                 label="IP address/Netmask" | ||||
|                 .version=${version} | ||||
|                 .value=${this._toString(this._interface![version].address)} | ||||
|                 .value="${this._device!.data.ip_address}" | ||||
|                 @value-changed=${this._handleInputValueChanged} | ||||
|               > | ||||
|               </paper-input> | ||||
|               ></paper-input> | ||||
|               <paper-input | ||||
|                 class="flex-auto" | ||||
|                 id="gateway" | ||||
|                 label="Gateway address" | ||||
|                 .version=${version} | ||||
|                 .value=${this._interface![version].gateway} | ||||
|                 .value="${this._device!.data.gateway}" | ||||
|                 @value-changed=${this._handleInputValueChanged} | ||||
|               > | ||||
|               </paper-input> | ||||
|               ></paper-input> | ||||
|               <paper-input | ||||
|                 class="flex-auto" | ||||
|                 id="nameservers" | ||||
|                 label="DNS servers" | ||||
|                 .version=${version} | ||||
|                 .value=${this._toString(this._interface![version].nameservers)} | ||||
|                 .value="${this._device!.data.nameservers as string}" | ||||
|                 @value-changed=${this._handleInputValueChanged} | ||||
|               > | ||||
|               </paper-input> | ||||
|             ` | ||||
|               ></paper-input> | ||||
|               NB!: If you are changing IP or gateway addresses, you might lose | ||||
|               the connection.` | ||||
|           : ""} | ||||
|       </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; | ||||
|       </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> | ||||
|       </div>`; | ||||
|   } | ||||
|  | ||||
|   private async _updateNetwork() { | ||||
|     this._processing = true; | ||||
|     let interfaceOptions: Partial<NetworkInterface> = {}; | ||||
|  | ||||
|     IP_VERSIONS.forEach((version) => { | ||||
|       interfaceOptions[version] = { | ||||
|         method: this._interface![version]?.method || "auto", | ||||
|     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(","), | ||||
|       }; | ||||
|       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 | ||||
|       ); | ||||
|       await updateNetworkInterface(this.hass, this._device!.interface, options); | ||||
|     } catch (err) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "Failed to change network settings", | ||||
|         text: extractApiErrorMessage(err), | ||||
|       }); | ||||
|       this._processing = false; | ||||
|       this._prosessing = false; | ||||
|       return; | ||||
|     } | ||||
|     this._params?.loadData(); | ||||
| @@ -443,73 +219,40 @@ export class DialogHassioNetwork extends LitElement | ||||
|         dismissText: "no", | ||||
|       }); | ||||
|       if (!confirm) { | ||||
|         this.requestUpdate("_interface"); | ||||
|         this.requestUpdate("_device"); | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|     this._curTabIndex = ev.detail.index; | ||||
|     this._interface = { ...this._interfaces[ev.detail.index] }; | ||||
|     this._device = this._network[ev.detail.index]; | ||||
|     this._device.data.nameservers = String(this._device.data.nameservers); | ||||
|   } | ||||
|  | ||||
|   private _handleRadioValueChanged(ev: CustomEvent): void { | ||||
|     const value = (ev.target as any).value as "disabled" | "auto" | "static"; | ||||
|     const version = (ev.target as any).version as "ipv4" | "ipv6"; | ||||
|     const value = (ev.target as HaRadio).value as "dhcp" | "static"; | ||||
|  | ||||
|     if ( | ||||
|       !value || | ||||
|       !this._interface || | ||||
|       this._interface[version]!.method === value | ||||
|     ) { | ||||
|     if (!value || !this._device || this._device!.data.method === value) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._dirty = true; | ||||
|  | ||||
|     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"); | ||||
|     this._device!.data.method = value; | ||||
|     this.requestUpdate("_device"); | ||||
|   } | ||||
|  | ||||
|   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._interface || | ||||
|       this._toString(this._interface[version]![id]) === this._toString(value) | ||||
|     ) { | ||||
|     if (!value || !this._device || this._device.data[id] === value) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._dirty = true; | ||||
|     this._interface[version]![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; | ||||
|     this._device.data[id] = value; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResult[] { | ||||
| @@ -556,16 +299,12 @@ 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: 0 8px 4px; | ||||
|           padding: 20px 24px; | ||||
|         } | ||||
|         .form { | ||||
|           margin-bottom: 53px; | ||||
| @@ -583,24 +322,6 @@ 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,7 +13,10 @@ export const showNetworkDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-network", | ||||
|     dialogImport: () => import("./dialog-hassio-network"), | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-network" */ "./dialog-hassio-network" | ||||
|       ), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -4,7 +4,10 @@ import "./dialog-hassio-registries"; | ||||
| export const showRegistriesDialog = (element: HTMLElement): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-registries", | ||||
|     dialogImport: () => import("./dialog-hassio-registries"), | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-registries" */ "./dialog-hassio-registries" | ||||
|       ), | ||||
|     dialogParams: {}, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -13,7 +13,10 @@ export const showRepositoriesDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-repositories", | ||||
|     dialogImport: () => import("./dialog-hassio-repositories"), | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-repositories" */ "./dialog-hassio-repositories" | ||||
|       ), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -109,7 +109,7 @@ class HassioSnapshotDialog extends LitElement { | ||||
|       return html``; | ||||
|     } | ||||
|     return html` | ||||
|       <ha-dialog open @closing=${this._closeDialog} .heading=${true}> | ||||
|       <ha-dialog open stacked @closing=${this._closeDialog} .heading=${true}> | ||||
|         <div slot="heading"> | ||||
|           <ha-header-bar> | ||||
|             <span slot="title"> | ||||
| @@ -191,37 +191,47 @@ class HassioSnapshotDialog extends LitElement { | ||||
|           : ""} | ||||
|         ${this._error ? html` <p class="error">Error: ${this._error}</p> ` : ""} | ||||
|  | ||||
|         <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> | ||||
|         <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>` | ||||
|           : ""} | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
| @@ -235,14 +245,6 @@ 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); | ||||
|         } | ||||
| @@ -250,6 +252,10 @@ class HassioSnapshotDialog extends LitElement { | ||||
|         .error { | ||||
|           color: var(--error-color); | ||||
|         } | ||||
|         .buttons { | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|         } | ||||
|         .buttons li { | ||||
|           list-style-type: none; | ||||
|         } | ||||
|   | ||||
| @@ -12,7 +12,10 @@ export const showHassioSnapshotDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-snapshot", | ||||
|     dialogImport: () => import("./dialog-hassio-snapshot"), | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-snapshot" */ "./dialog-hassio-snapshot" | ||||
|       ), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -13,7 +13,10 @@ export const showSnapshotUploadDialog = ( | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "dialog-hassio-snapshot-upload", | ||||
|     dialogImport: () => import("./dialog-hassio-snapshot-upload"), | ||||
|     dialogImport: () => | ||||
|       import( | ||||
|         /* webpackChunkName: "dialog-hassio-snapshot-upload" */ "./dialog-hassio-snapshot-upload" | ||||
|       ), | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
|   | ||||
| @@ -1,22 +1,29 @@ | ||||
| import { html, PropertyValues, customElement, property } from "lit-element"; | ||||
| import { | ||||
|   html, | ||||
|   PropertyValues, | ||||
|   customElement, | ||||
|   LitElement, | ||||
|   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 SupervisorBaseElement { | ||||
| export class HassioMain extends urlSyncMixin(ProvideHassLitMixin(LitElement)) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public panel!: HassioPanelInfo; | ||||
|   @property() public panel!: HassioPanelInfo; | ||||
|  | ||||
|   @property({ type: Boolean }) public narrow!: boolean; | ||||
|   @property() public narrow!: boolean; | ||||
|  | ||||
|   @property({ attribute: false }) public route?: Route; | ||||
|   @property() public route?: Route; | ||||
|  | ||||
|   protected firstUpdated(changedProps: PropertyValues) { | ||||
|     super.firstUpdated(changedProps); | ||||
| @@ -70,13 +77,9 @@ export class HassioMain extends SupervisorBaseElement { | ||||
|   } | ||||
|  | ||||
|   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,5 +1,10 @@ | ||||
| import { customElement, property } from "lit-element"; | ||||
| import { Supervisor } from "../../src/data/supervisor/supervisor"; | ||||
| import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host"; | ||||
| import { | ||||
|   HassioHomeAssistantInfo, | ||||
|   HassioSupervisorInfo, | ||||
|   HassioInfo, | ||||
| } from "../../src/data/hassio/supervisor"; | ||||
| import { | ||||
|   HassRouterPage, | ||||
|   RouterOptions, | ||||
| @@ -16,12 +21,20 @@ 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: { | ||||
| @@ -41,9 +54,13 @@ 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,13 +1,18 @@ | ||||
| import { | ||||
|   css, | ||||
|   CSSResult, | ||||
|   customElement, | ||||
|   html, | ||||
|   LitElement, | ||||
|   property, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   CSSResult, | ||||
| } from "lit-element"; | ||||
| import { Supervisor } from "../../src/data/supervisor/supervisor"; | ||||
| import { HassioHassOSInfo, HassioHostInfo } from "../../src/data/hassio/host"; | ||||
| import { | ||||
|   HassioHomeAssistantInfo, | ||||
|   HassioSupervisorInfo, | ||||
|   HassioInfo, | ||||
| } from "../../src/data/hassio/supervisor"; | ||||
| import { HomeAssistant, Route } from "../../src/types"; | ||||
| import "./hassio-panel-router"; | ||||
|  | ||||
| @@ -15,19 +20,34 @@ 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 | ||||
|         .hass=${this.hass} | ||||
|         .supervisor=${this.supervisor} | ||||
|         .route=${this.route} | ||||
|         .hass=${this.hass} | ||||
|         .narrow=${this.narrow} | ||||
|         .supervisorInfo=${this.supervisorInfo} | ||||
|         .hassioInfo=${this.hassioInfo} | ||||
|         .hostInfo=${this.hostInfo} | ||||
|         .hassInfo=${this.hassInfo} | ||||
|         .hassOsInfo=${this.hassOsInfo} | ||||
|       ></hassio-panel-router> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -1,6 +1,24 @@ | ||||
| import { customElement, property } from "lit-element"; | ||||
| import { HassioPanelInfo } from "../../src/data/hassio/supervisor"; | ||||
| import { Supervisor } from "../../src/data/supervisor/supervisor"; | ||||
| 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 { | ||||
|   HassRouterPage, | ||||
|   RouterOptions, | ||||
| @@ -14,11 +32,9 @@ import "./hassio-panel"; | ||||
| class HassioRouter extends HassRouterPage { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|   @property() public panel!: HassioPanelInfo; | ||||
|  | ||||
|   @property({ attribute: false }) public panel!: HassioPanelInfo; | ||||
|  | ||||
|   @property({ type: Boolean }) public narrow!: boolean; | ||||
|   @property() public narrow!: boolean; | ||||
|  | ||||
|   protected routerOptions: RouterOptions = { | ||||
|     // Hass.io has a page with tabs, so we route all non-matching routes to it. | ||||
| @@ -35,22 +51,47 @@ class HassioRouter extends HassRouterPage { | ||||
|       system: "dashboard", | ||||
|       addon: { | ||||
|         tag: "hassio-addon-dashboard", | ||||
|         load: () => import("./addon-view/hassio-addon-dashboard"), | ||||
|         load: () => | ||||
|           import( | ||||
|             /* webpackChunkName: "hassio-addon-dashboard" */ "./addon-view/hassio-addon-dashboard" | ||||
|           ), | ||||
|       }, | ||||
|       ingress: { | ||||
|         tag: "hassio-ingress-view", | ||||
|         load: () => import("./ingress-view/hassio-ingress-view"), | ||||
|         load: () => | ||||
|           import( | ||||
|             /* webpackChunkName: "hassio-ingress-view" */ "./ingress-view/hassio-ingress-view" | ||||
|           ), | ||||
|       }, | ||||
|     }, | ||||
|   }; | ||||
|  | ||||
|   @internalProperty() private _supervisorInfo?: HassioSupervisorInfo; | ||||
|  | ||||
|   @internalProperty() private _hostInfo?: HassioHostInfo; | ||||
|  | ||||
|   @internalProperty() private _hassioInfo?: HassioInfo; | ||||
|  | ||||
|   @internalProperty() private _hassOsInfo?: HassioHassOSInfo; | ||||
|  | ||||
|   @internalProperty() private _hassInfo?: HassioHomeAssistantInfo; | ||||
|  | ||||
|   protected firstUpdated(changedProps: PropertyValues) { | ||||
|     super.firstUpdated(changedProps); | ||||
|     this.addEventListener("hass-api-called", (ev) => this._apiCalled(ev)); | ||||
|   } | ||||
|  | ||||
|   protected updatePageEl(el) { | ||||
|     // 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") { | ||||
| @@ -61,12 +102,45 @@ 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 { | ||||
|   | ||||
| @@ -13,10 +13,7 @@ import { | ||||
|   fetchHassioAddonInfo, | ||||
|   HassioAddonDetails, | ||||
| } from "../../../src/data/hassio/addon"; | ||||
| import { | ||||
|   createHassioSession, | ||||
|   validateHassioSession, | ||||
| } from "../../../src/data/hassio/ingress"; | ||||
| import { createHassioSession } from "../../../src/data/hassio/supervisor"; | ||||
| import "../../../src/layouts/hass-loading-screen"; | ||||
| import "../../../src/layouts/hass-subpage"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| @@ -38,17 +35,6 @@ class HassioIngressView extends LitElement { | ||||
|   @property({ type: Boolean }) | ||||
|   public narrow = false; | ||||
|  | ||||
|   private _sessionKeepAlive?: number; | ||||
|  | ||||
|   public disconnectedCallback() { | ||||
|     super.disconnectedCallback(); | ||||
|  | ||||
|     if (this._sessionKeepAlive) { | ||||
|       clearInterval(this._sessionKeepAlive); | ||||
|       this._sessionKeepAlive = undefined; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     if (!this._addon) { | ||||
|       return html` <hass-loading-screen></hass-loading-screen> `; | ||||
| @@ -58,7 +44,6 @@ class HassioIngressView extends LitElement { | ||||
|  | ||||
|     if (!this.ingressPanel) { | ||||
|       return html`<hass-subpage | ||||
|         .hass=${this.hass} | ||||
|         .header=${this._addon.name} | ||||
|         .narrow=${this.narrow} | ||||
|       > | ||||
| @@ -98,7 +83,10 @@ class HassioIngressView extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _fetchData(addonSlug: string) { | ||||
|     const createSessionPromise = createHassioSession(this.hass); | ||||
|     const createSessionPromise = createHassioSession(this.hass).then( | ||||
|       () => true, | ||||
|       () => false | ||||
|     ); | ||||
|  | ||||
|     let addon; | ||||
|  | ||||
| @@ -131,11 +119,7 @@ class HassioIngressView extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     let session; | ||||
|  | ||||
|     try { | ||||
|       session = await createSessionPromise; | ||||
|     } catch (err) { | ||||
|     if (!(await createSessionPromise)) { | ||||
|       await showAlertDialog(this, { | ||||
|         text: "Unable to create an Ingress session", | ||||
|         title: addon.name, | ||||
| @@ -144,17 +128,6 @@ class HassioIngressView extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     if (this._sessionKeepAlive) { | ||||
|       clearInterval(this._sessionKeepAlive); | ||||
|     } | ||||
|     this._sessionKeepAlive = window.setInterval(async () => { | ||||
|       try { | ||||
|         await validateHassioSession(this.hass, session); | ||||
|       } catch (err) { | ||||
|         session = await createHassioSession(this.hass); | ||||
|       } | ||||
|     }, 60000); | ||||
|  | ||||
|     this._addon = addon; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -26,6 +26,7 @@ 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"; | ||||
| @@ -40,7 +41,7 @@ import { | ||||
|   HassioSnapshot, | ||||
|   reloadHassioSnapshots, | ||||
| } from "../../../src/data/hassio/snapshot"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { HassioSupervisorInfo } from "../../../src/data/hassio/supervisor"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { PolymerChangedEvent } from "../../../src/polymer-types"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| @@ -66,7 +67,7 @@ class HassioSnapshots extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) public route!: Route; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|   @property({ attribute: false }) public supervisorInfo!: HassioSupervisorInfo; | ||||
|  | ||||
|   @internalProperty() private _snapshotName = ""; | ||||
|  | ||||
| @@ -265,7 +266,7 @@ class HassioSnapshots extends LitElement { | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if (changedProps.has("supervisorInfo")) { | ||||
|       this._addonList = this.supervisor.supervisor.addons | ||||
|       this._addonList = this.supervisorInfo.addons | ||||
|         .map((addon) => ({ | ||||
|           slug: addon.slug, | ||||
|           name: addon.name, | ||||
| @@ -371,6 +372,7 @@ 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); | ||||
|     } | ||||
|   | ||||
| @@ -1,69 +0,0 @@ | ||||
| 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,6 +27,8 @@ import { | ||||
|   changeHostOptions, | ||||
|   configSyncOS, | ||||
|   fetchHassioHostInfo, | ||||
|   HassioHassOSInfo, | ||||
|   HassioHostInfo as HassioHostInfoType, | ||||
|   rebootHost, | ||||
|   shutdownHost, | ||||
|   updateOS, | ||||
| @@ -35,7 +37,7 @@ import { | ||||
|   fetchNetworkInfo, | ||||
|   NetworkInfo, | ||||
| } from "../../../src/data/hassio/network"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { HassioInfo } from "../../../src/data/hassio/supervisor"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
|   showConfirmationDialog, | ||||
| @@ -51,22 +53,28 @@ import { hassioStyle } from "../resources/hassio-style"; | ||||
| class HassioHostInfo extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|   @property() public hostInfo!: HassioHostInfoType; | ||||
|  | ||||
|   @property({ attribute: false }) public hassioInfo!: HassioInfo; | ||||
|  | ||||
|   @property({ attribute: false }) public hassOsInfo!: HassioHassOSInfo; | ||||
|  | ||||
|   @internalProperty() public _networkInfo?: NetworkInfo; | ||||
|  | ||||
|   protected render(): TemplateResult | void { | ||||
|     const primaryIpAddress = this.supervisor.host.features.includes("network") | ||||
|       ? this._primaryIpAddress(this.supervisor.network!) | ||||
|     const primaryIpAddress = this.hostInfo.features.includes("network") | ||||
|       ? this._primaryIpAddress(this._networkInfo!) | ||||
|       : ""; | ||||
|     return html` | ||||
|       <ha-card header="Host System"> | ||||
|         <div class="card-content"> | ||||
|           ${this.supervisor.host.features.includes("hostname") | ||||
|           ${this.hostInfo.features.includes("hostname") | ||||
|             ? html`<ha-settings-row> | ||||
|                 <span slot="heading"> | ||||
|                   Hostname | ||||
|                 </span> | ||||
|                 <span slot="description"> | ||||
|                   ${this.supervisor.host.hostname} | ||||
|                   ${this.hostInfo.hostname} | ||||
|                 </span> | ||||
|                 <mwc-button | ||||
|                   title="Change the hostname" | ||||
| @@ -76,7 +84,7 @@ class HassioHostInfo extends LitElement { | ||||
|                 </mwc-button> | ||||
|               </ha-settings-row>` | ||||
|             : ""} | ||||
|           ${this.supervisor.host.features.includes("network") | ||||
|           ${this.hostInfo.features.includes("network") | ||||
|             ? html` <ha-settings-row> | ||||
|                 <span slot="heading"> | ||||
|                   IP Address | ||||
| @@ -98,9 +106,10 @@ class HassioHostInfo extends LitElement { | ||||
|               Operating System | ||||
|             </span> | ||||
|             <span slot="description"> | ||||
|               ${this.supervisor.host.operating_system} | ||||
|               ${this.hostInfo.operating_system} | ||||
|             </span> | ||||
|             ${this.supervisor.os.update_available | ||||
|             ${this.hostInfo.features.includes("hassos") && | ||||
|             this.hassOsInfo.update_available | ||||
|               ? html` | ||||
|                   <ha-progress-button | ||||
|                     title="Update the host OS" | ||||
| @@ -111,29 +120,29 @@ class HassioHostInfo extends LitElement { | ||||
|                 ` | ||||
|               : ""} | ||||
|           </ha-settings-row> | ||||
|           ${!this.supervisor.host.features.includes("hassos") | ||||
|           ${!this.hostInfo.features.includes("hassos") | ||||
|             ? html`<ha-settings-row> | ||||
|                 <span slot="heading"> | ||||
|                   Docker version | ||||
|                 </span> | ||||
|                 <span slot="description"> | ||||
|                   ${this.supervisor.info.docker} | ||||
|                   ${this.hassioInfo.docker} | ||||
|                 </span> | ||||
|               </ha-settings-row>` | ||||
|             : ""} | ||||
|           ${this.supervisor.host.deployment | ||||
|           ${this.hostInfo.deployment | ||||
|             ? html`<ha-settings-row> | ||||
|                 <span slot="heading"> | ||||
|                   Deployment | ||||
|                 </span> | ||||
|                 <span slot="description"> | ||||
|                   ${this.supervisor.host.deployment} | ||||
|                   ${this.hostInfo.deployment} | ||||
|                 </span> | ||||
|               </ha-settings-row>` | ||||
|             : ""} | ||||
|         </div> | ||||
|         <div class="card-actions"> | ||||
|           ${this.supervisor.host.features.includes("reboot") | ||||
|           ${this.hostInfo.features.includes("reboot") | ||||
|             ? html` | ||||
|                 <ha-progress-button | ||||
|                   title="Reboot the host OS" | ||||
| @@ -144,7 +153,7 @@ class HassioHostInfo extends LitElement { | ||||
|                 </ha-progress-button> | ||||
|               ` | ||||
|             : ""} | ||||
|           ${this.supervisor.host.features.includes("shutdown") | ||||
|           ${this.hostInfo.features.includes("shutdown") | ||||
|             ? html` | ||||
|                 <ha-progress-button | ||||
|                   title="Shutdown the host OS" | ||||
| @@ -166,7 +175,7 @@ class HassioHostInfo extends LitElement { | ||||
|             <mwc-list-item title="Show a list of hardware"> | ||||
|               Hardware | ||||
|             </mwc-list-item> | ||||
|             ${this.supervisor.host.features.includes("hassos") | ||||
|             ${this.hostInfo.features.includes("hassos") | ||||
|               ? html`<mwc-list-item | ||||
|                   title="Load HassOS configs or updates from USB" | ||||
|                 > | ||||
| @@ -184,10 +193,12 @@ class HassioHostInfo extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _primaryIpAddress = memoizeOne((network_info: NetworkInfo) => { | ||||
|     if (!network_info || !network_info.interfaces) { | ||||
|     if (!network_info) { | ||||
|       return ""; | ||||
|     } | ||||
|     return network_info.interfaces.find((a) => a.primary)?.ipv4?.address![0]; | ||||
|     return Object.keys(network_info?.interfaces) | ||||
|       .map((device) => network_info.interfaces[device]) | ||||
|       .find((device) => device.primary)?.ip_address; | ||||
|   }); | ||||
|  | ||||
|   private async _handleMenuAction(ev: CustomEvent<ActionDetail>) { | ||||
| @@ -305,13 +316,13 @@ class HassioHostInfo extends LitElement { | ||||
|  | ||||
|   private async _changeNetworkClicked(): Promise<void> { | ||||
|     showNetworkDialog(this, { | ||||
|       network: this.supervisor.network!, | ||||
|       network: this._networkInfo!, | ||||
|       loadData: () => this._loadData(), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private async _changeHostnameClicked(): Promise<void> { | ||||
|     const curHostname: string = this.supervisor.host.hostname; | ||||
|     const curHostname: string = this.hostInfo.hostname; | ||||
|     const hostname = await showPromptDialog(this, { | ||||
|       title: "Change Hostname", | ||||
|       inputLabel: "Please enter a new hostname:", | ||||
| @@ -322,8 +333,7 @@ class HassioHostInfo extends LitElement { | ||||
|     if (hostname && hostname !== curHostname) { | ||||
|       try { | ||||
|         await changeHostOptions(this.hass, { hostname }); | ||||
|         const host = await fetchHassioHostInfo(this.hass); | ||||
|         fireEvent(this, "supervisor-update", { host }); | ||||
|         this.hostInfo = await fetchHassioHostInfo(this.hass); | ||||
|       } catch (err) { | ||||
|         showAlertDialog(this, { | ||||
|           title: "Setting hostname failed", | ||||
| @@ -336,8 +346,7 @@ class HassioHostInfo extends LitElement { | ||||
|   private async _importFromUSB(): Promise<void> { | ||||
|     try { | ||||
|       await configSyncOS(this.hass); | ||||
|       const host = await fetchHassioHostInfo(this.hass); | ||||
|       fireEvent(this, "supervisor-update", { host }); | ||||
|       this.hostInfo = await fetchHassioHostInfo(this.hass); | ||||
|     } catch (err) { | ||||
|       showAlertDialog(this, { | ||||
|         title: "Failed to import from USB", | ||||
| @@ -347,8 +356,7 @@ class HassioHostInfo extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _loadData(): Promise<void> { | ||||
|     const network = await fetchNetworkInfo(this.hass); | ||||
|     fireEvent(this, "supervisor-update", { network }); | ||||
|     this._networkInfo = await fetchNetworkInfo(this.hass); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResult[] { | ||||
|   | ||||
| @@ -13,15 +13,16 @@ 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, | ||||
| @@ -31,7 +32,7 @@ import { HomeAssistant } from "../../../src/types"; | ||||
| import { documentationUrl } from "../../../src/util/documentation-url"; | ||||
| import { hassioStyle } from "../resources/hassio-style"; | ||||
|  | ||||
| const UNSUPPORTED_REASON = { | ||||
| const ISSUES = { | ||||
|   container: { | ||||
|     title: "Containers known to cause issues", | ||||
|     url: "/more-info/unsupported/container", | ||||
| @@ -45,10 +46,6 @@ const UNSUPPORTED_REASON = { | ||||
|     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", | ||||
| @@ -62,30 +59,14 @@ const UNSUPPORTED_REASON = { | ||||
|   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 supervisor!: Supervisor; | ||||
|   @property({ attribute: false }) | ||||
|   public supervisorInfo!: HassioSupervisorInfoType; | ||||
|  | ||||
|   @property() public hostInfo!: HassioHostInfoType; | ||||
|  | ||||
|   protected render(): TemplateResult | void { | ||||
|     return html` | ||||
| @@ -96,7 +77,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|               Version | ||||
|             </span> | ||||
|             <span slot="description"> | ||||
|               ${this.supervisor.supervisor.version} | ||||
|               ${this.supervisorInfo.version} | ||||
|             </span> | ||||
|           </ha-settings-row> | ||||
|           <ha-settings-row> | ||||
| @@ -104,9 +85,9 @@ class HassioSupervisorInfo extends LitElement { | ||||
|               Newest Version | ||||
|             </span> | ||||
|             <span slot="description"> | ||||
|               ${this.supervisor.supervisor.version_latest} | ||||
|               ${this.supervisorInfo.version_latest} | ||||
|             </span> | ||||
|             ${this.supervisor.supervisor.update_available | ||||
|             ${this.supervisorInfo.update_available | ||||
|               ? html` | ||||
|                   <ha-progress-button | ||||
|                     title="Update the supervisor" | ||||
| @@ -122,9 +103,9 @@ class HassioSupervisorInfo extends LitElement { | ||||
|               Channel | ||||
|             </span> | ||||
|             <span slot="description"> | ||||
|               ${this.supervisor.supervisor.channel} | ||||
|               ${this.supervisorInfo.channel} | ||||
|             </span> | ||||
|             ${this.supervisor.supervisor.channel === "beta" | ||||
|             ${this.supervisorInfo.channel === "beta" | ||||
|               ? html` | ||||
|                   <ha-progress-button | ||||
|                     @click=${this._toggleBeta} | ||||
| @@ -133,7 +114,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|                     Leave beta channel | ||||
|                   </ha-progress-button> | ||||
|                 ` | ||||
|               : this.supervisor.supervisor.channel === "stable" | ||||
|               : this.supervisorInfo.channel === "stable" | ||||
|               ? html` | ||||
|                   <ha-progress-button | ||||
|                     @click=${this._toggleBeta} | ||||
| @@ -145,7 +126,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|               : ""} | ||||
|           </ha-settings-row> | ||||
|  | ||||
|           ${this.supervisor.supervisor.supported | ||||
|           ${this.supervisorInfo?.supported | ||||
|             ? html` <ha-settings-row three-line> | ||||
|                 <span slot="heading"> | ||||
|                   Share Diagnostics | ||||
| @@ -162,7 +143,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|                 </div> | ||||
|                 <ha-switch | ||||
|                   haptic | ||||
|                   .checked=${this.supervisor.supervisor.diagnostics} | ||||
|                   .checked=${this.supervisorInfo.diagnostics} | ||||
|                   @change=${this._toggleDiagnostics} | ||||
|                 ></ha-switch> | ||||
|               </ha-settings-row>` | ||||
| @@ -176,33 +157,14 @@ 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> | ||||
|     `; | ||||
| @@ -212,7 +174,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|     const button = ev.currentTarget as any; | ||||
|     button.progress = true; | ||||
|  | ||||
|     if (this.supervisor.supervisor.channel === "stable") { | ||||
|     if (this.supervisorInfo.channel === "stable") { | ||||
|       const confirmed = await showConfirmationDialog(this, { | ||||
|         title: "WARNING", | ||||
|         text: html` Beta releases are for testers and early adopters and can | ||||
| @@ -241,19 +203,18 @@ class HassioSupervisorInfo extends LitElement { | ||||
|  | ||||
|     try { | ||||
|       const data: Partial<SupervisorOptions> = { | ||||
|         channel: | ||||
|           this.supervisor.supervisor.channel === "stable" ? "beta" : "stable", | ||||
|         channel: this.supervisorInfo.channel === "stable" ? "beta" : "stable", | ||||
|       }; | ||||
|       await setSupervisorOption(this.hass, data); | ||||
|       await this._reloadSupervisor(); | ||||
|       await reloadSupervisor(this.hass); | ||||
|       fireEvent(this, "hass-api-called", { success: true, response: null }); | ||||
|     } 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> { | ||||
| @@ -261,37 +222,15 @@ class HassioSupervisorInfo extends LitElement { | ||||
|     button.progress = true; | ||||
|  | ||||
|     try { | ||||
|       await this._reloadSupervisor(); | ||||
|       await reloadSupervisor(this.hass); | ||||
|       this.supervisorInfo = await fetchHassioSupervisorInfo(this.hass); | ||||
|     } 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> { | ||||
| @@ -300,7 +239,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.supervisor.supervisor.version_latest}?`, | ||||
|       text: `Are you sure you want to update supervisor to version ${this.supervisorInfo.version_latest}?`, | ||||
|       confirmText: "update", | ||||
|       dismissText: "cancel", | ||||
|     }); | ||||
| @@ -317,9 +256,8 @@ 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> { | ||||
| @@ -338,53 +276,22 @@ 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> | ||||
|           ${this.supervisor.resolution.unsupported.map( | ||||
|           ${resolution.unsupported.map( | ||||
|             (issue) => html` | ||||
|               <li> | ||||
|                 ${UNSUPPORTED_REASON[issue] | ||||
|                 ${ISSUES[issue] | ||||
|                   ? html`<a | ||||
|                       href="${documentationUrl( | ||||
|                         this.hass, | ||||
|                         UNSUPPORTED_REASON[issue].url | ||||
|                       )}" | ||||
|                       href="${documentationUrl(this.hass, ISSUES[issue].url)}" | ||||
|                       target="_blank" | ||||
|                       rel="noreferrer" | ||||
|                     > | ||||
|                       ${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} | ||||
|                       ${ISSUES[issue].title} | ||||
|                     </a>` | ||||
|                   : issue} | ||||
|               </li> | ||||
| @@ -397,7 +304,7 @@ class HassioSupervisorInfo extends LitElement { | ||||
|   private async _toggleDiagnostics(): Promise<void> { | ||||
|     try { | ||||
|       const data: SupervisorOptions = { | ||||
|         diagnostics: !this.supervisor.supervisor?.diagnostics, | ||||
|         diagnostics: !this.supervisorInfo?.diagnostics, | ||||
|       }; | ||||
|       await setSupervisorOption(this.hass, data); | ||||
|     } catch (err) { | ||||
|   | ||||
| @@ -19,7 +19,6 @@ 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"; | ||||
| @@ -33,7 +32,7 @@ import { hassioStyle } from "../resources/hassio-style"; | ||||
| class HassioSystemMetrics extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public supervisor!: Supervisor; | ||||
|   @property() public hostInfo!: HassioHostInfo; | ||||
|  | ||||
|   @internalProperty() private _supervisorMetrics?: HassioStats; | ||||
|  | ||||
| @@ -65,8 +64,8 @@ class HassioSystemMetrics extends LitElement { | ||||
|       }, | ||||
|       { | ||||
|         description: "Used Space", | ||||
|         value: this._getUsedSpace(this.supervisor.host), | ||||
|         tooltip: `${this.supervisor.host.disk_used} GB/${this.supervisor.host.disk_total} GB`, | ||||
|         value: this._getUsedSpace(this.hostInfo), | ||||
|         tooltip: `${this.hostInfo.disk_used} GB/${this.hostInfo.disk_total} GB`, | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|   | ||||
| @@ -7,7 +7,14 @@ import { | ||||
|   property, | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import { Supervisor } from "../../../src/data/supervisor/supervisor"; | ||||
| import { | ||||
|   HassioHassOSInfo, | ||||
|   HassioHostInfo, | ||||
| } from "../../../src/data/hassio/host"; | ||||
| import { | ||||
|   HassioInfo, | ||||
|   HassioSupervisorInfo, | ||||
| } from "../../../src/data/hassio/supervisor"; | ||||
| import "../../../src/layouts/hass-tabs-subpage"; | ||||
| import { haStyle } from "../../../src/resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../src/types"; | ||||
| @@ -22,12 +29,18 @@ 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 | ||||
| @@ -43,15 +56,18 @@ class HassioSystem extends LitElement { | ||||
|           <div class="card-group"> | ||||
|             <hassio-supervisor-info | ||||
|               .hass=${this.hass} | ||||
|               .supervisor=${this.supervisor} | ||||
|               .hostInfo=${this.hostInfo} | ||||
|               .supervisorInfo=${this.supervisorInfo} | ||||
|             ></hassio-supervisor-info> | ||||
|             <hassio-host-info | ||||
|               .hass=${this.hass} | ||||
|               .supervisor=${this.supervisor} | ||||
|               .hassioInfo=${this.hassioInfo} | ||||
|               .hostInfo=${this.hostInfo} | ||||
|               .hassOsInfo=${this.hassOsInfo} | ||||
|             ></hassio-host-info> | ||||
|             <hassio-system-metrics | ||||
|               .hass=${this.hass} | ||||
|               .supervisor=${this.supervisor} | ||||
|               .hostInfo=${this.hostInfo} | ||||
|             ></hassio-system-metrics> | ||||
|           </div> | ||||
|           <hassio-supervisor-log .hass=${this.hass}></hassio-supervisor-log> | ||||
|   | ||||
							
								
								
									
										26
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										26
									
								
								package.json
									
									
									
									
									
								
							| @@ -83,9 +83,6 @@ | ||||
|     "@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", | ||||
| @@ -93,6 +90,7 @@ | ||||
|     "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", | ||||
| @@ -112,7 +110,7 @@ | ||||
|     "marked": "^1.1.1", | ||||
|     "mdn-polyfills": "^5.16.0", | ||||
|     "memoize-one": "^5.0.2", | ||||
|     "node-vibrant": "3.2.1-alpha.1", | ||||
|     "node-vibrant": "^3.1.6", | ||||
|     "proxy-polyfill": "^0.3.1", | ||||
|     "punycode": "^2.1.1", | ||||
|     "qrcode": "^1.4.4", | ||||
| @@ -121,10 +119,7 @@ | ||||
|     "roboto-fontface": "^0.10.0", | ||||
|     "sortablejs": "^1.10.2", | ||||
|     "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", | ||||
| @@ -144,11 +139,9 @@ | ||||
|     "@babel/plugin-proposal-optional-chaining": "^7.11.0", | ||||
|     "@babel/plugin-syntax-dynamic-import": "^7.8.3", | ||||
|     "@babel/plugin-syntax-import-meta": "^7.10.4", | ||||
|     "@babel/plugin-syntax-top-level-await": "^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", | ||||
| @@ -167,11 +160,8 @@ | ||||
|     "@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", | ||||
| @@ -187,6 +177,7 @@ | ||||
|     "gulp": "^4.0.0", | ||||
|     "gulp-foreach": "^0.1.0", | ||||
|     "gulp-json-transform": "^0.4.6", | ||||
|     "gulp-jsonminify": "^1.1.0", | ||||
|     "gulp-merge-json": "^1.3.1", | ||||
|     "gulp-rename": "^2.0.0", | ||||
|     "gulp-zopfli-green": "^3.0.1", | ||||
| @@ -205,6 +196,7 @@ | ||||
|     "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", | ||||
| @@ -212,15 +204,15 @@ | ||||
|     "sinon": "^7.3.1", | ||||
|     "source-map-url": "^0.4.0", | ||||
|     "systemjs": "^6.3.2", | ||||
|     "terser-webpack-plugin": "^5.0.0", | ||||
|     "terser-webpack-plugin": "^3.0.6", | ||||
|     "ts-lit-plugin": "^1.2.1", | ||||
|     "ts-mocha": "^7.0.0", | ||||
|     "typescript": "^4.0.3", | ||||
|     "vinyl-buffer": "^1.0.1", | ||||
|     "vinyl-source-stream": "^2.0.0", | ||||
|     "webpack": "5.1.3", | ||||
|     "webpack-cli": "4.1.0", | ||||
|     "webpack-dev-server": "^3.11.0", | ||||
|     "webpack": "5.0.0-rc.3", | ||||
|     "webpack-cli": "4.0.0-rc.0", | ||||
|     "webpack-dev-server": "^3.10.3", | ||||
|     "webpack-manifest-plugin": "3.0.0-rc.0", | ||||
|     "workbox-build": "^5.1.3" | ||||
|   }, | ||||
|   | ||||
							
								
								
									
										41
									
								
								polymer.json
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										41
									
								
								polymer.json
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,41 @@ | ||||
| { | ||||
|   "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/shopping-list/ha-panel-shopping-list.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" | ||||
|     } | ||||
|   ] | ||||
| } | ||||
							
								
								
									
										55
									
								
								script/core
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								script/core
									
									
									
									
									
								
							| @@ -1,55 +0,0 @@ | ||||
| #!/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="20201212.0", | ||||
|     version="20201021.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("./ha-pick-auth-provider"); | ||||
| import(/* webpackChunkName: "pick-auth-provider" */ "./ha-pick-auth-provider"); | ||||
|  | ||||
| class HaAuthorize extends litLocalizeLiteMixin(LitElement) { | ||||
|   @property() public clientId?: string; | ||||
|   | ||||
| @@ -1,4 +1,10 @@ | ||||
| import { expandHex } from "./hex"; | ||||
| const expand_hex = (hex: string): string => { | ||||
|   let result = ""; | ||||
|   for (const val of hex) { | ||||
|     result += val + val; | ||||
|   } | ||||
|   return result; | ||||
| }; | ||||
|  | ||||
| const rgb_hex = (component: number): string => { | ||||
|   const hex = Math.round(Math.min(Math.max(component, 0), 255)).toString(16); | ||||
| @@ -8,7 +14,10 @@ const rgb_hex = (component: number): string => { | ||||
| // Conversion between HEX and RGB | ||||
|  | ||||
| export const hex2rgb = (hex: string): [number, number, number] => { | ||||
|   hex = expandHex(hex); | ||||
|   hex = hex.replace("#", ""); | ||||
|   if (hex.length === 3 || hex.length === 4) { | ||||
|     hex = expand_hex(hex); | ||||
|   } | ||||
|  | ||||
|   return [ | ||||
|     parseInt(hex.substring(0, 2), 16), | ||||
|   | ||||
| @@ -1,24 +0,0 @@ | ||||
| export const expandHex = (hex: string): string => { | ||||
|   hex = hex.replace("#", ""); | ||||
|   if (hex.length === 6) return hex; | ||||
|   let result = ""; | ||||
|   for (const val of hex) { | ||||
|     result += val + val; | ||||
|   } | ||||
|   return result; | ||||
| }; | ||||
|  | ||||
| // Blend 2 hex colors: c1 is placed over c2, blend is c1's opacity. | ||||
| export const hexBlend = (c1: string, c2: string, blend = 50): string => { | ||||
|   let color = ""; | ||||
|   c1 = expandHex(c1); | ||||
|   c2 = expandHex(c2); | ||||
|   for (let i = 0; i <= 5; i += 2) { | ||||
|     const h1 = parseInt(c1.substr(i, 2), 16); | ||||
|     const h2 = parseInt(c2.substr(i, 2), 16); | ||||
|     let hex = Math.floor(h2 + (h1 - h2) * (blend / 100)).toString(16); | ||||
|     while (hex.length < 2) hex = "0" + hex; | ||||
|     color += hex; | ||||
|   } | ||||
|   return `#${color}`; | ||||
| }; | ||||
| @@ -22,8 +22,3 @@ 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,18 +0,0 @@ | ||||
| import { isComponentLoaded } from "./is_component_loaded"; | ||||
| import { PageNavigation } from "../../layouts/hass-tabs-subpage"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
|  | ||||
| export const canShowPage = (hass: HomeAssistant, page: PageNavigation) => { | ||||
|   return ( | ||||
|     (isCore(page) || isLoadedIntegration(hass, page)) && | ||||
|     !hideAdvancedPage(hass, page) | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| const isLoadedIntegration = (hass: HomeAssistant, page: PageNavigation) => | ||||
|   !page.component || isComponentLoaded(hass, page.component); | ||||
| const isCore = (page: PageNavigation) => page.core; | ||||
| const isAdvancedPage = (page: PageNavigation) => page.advancedOnly; | ||||
| const userWantsAdvanced = (hass: HomeAssistant) => hass.userData?.showAdvanced; | ||||
| const hideAdvancedPage = (hass: HomeAssistant, page: PageNavigation) => | ||||
|   isAdvancedPage(page) && !userWantsAdvanced(hass); | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { Theme } from "../../data/ws-themes"; | ||||
| import { darkStyles, derivedStyles } from "../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { HomeAssistant, Theme } from "../../types"; | ||||
| import { | ||||
|   hex2rgb, | ||||
|   lab2hex, | ||||
| @@ -8,16 +7,15 @@ import { | ||||
|   rgb2hex, | ||||
|   rgb2lab, | ||||
| } from "../color/convert-color"; | ||||
| import { hexBlend } from "../color/hex"; | ||||
| import { labBrighten, labDarken } from "../color/lab"; | ||||
| import { rgbContrast } from "../color/rgb"; | ||||
|  | ||||
| interface ProcessedTheme { | ||||
|   keys: { [key: string]: "" }; | ||||
|   styles: Record<string, string>; | ||||
|   styles: { [key: string]: string }; | ||||
| } | ||||
|  | ||||
| let PROCESSED_THEMES: Record<string, ProcessedTheme> = {}; | ||||
| let PROCESSED_THEMES: { [key: string]: ProcessedTheme } = {}; | ||||
|  | ||||
| /** | ||||
|  * Apply a theme to an element by setting the CSS variables on it. | ||||
| @@ -39,13 +37,6 @@ export const applyThemesOnElement = ( | ||||
|     if (themeOptions.dark) { | ||||
|       cacheKey = `${cacheKey}__dark`; | ||||
|       themeRules = darkStyles; | ||||
|       if (themeOptions.primaryColor) { | ||||
|         themeRules["app-header-background-color"] = hexBlend( | ||||
|           themeOptions.primaryColor, | ||||
|           "#121212", | ||||
|           8 | ||||
|         ); | ||||
|       } | ||||
|     } | ||||
|     if (themeOptions.primaryColor) { | ||||
|       cacheKey = `${cacheKey}__primary_${themeOptions.primaryColor}`; | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import { directive, NodePart, Part } from "lit-html"; | ||||
|  | ||||
| export const dynamicElement = directive( | ||||
|   (tag: string, properties?: Record<string, any>) => (part: Part): void => { | ||||
|   (tag: string, properties?: { [key: string]: any }) => (part: Part): void => { | ||||
|     if (!(part instanceof NodePart)) { | ||||
|       throw new Error( | ||||
|         "dynamicElementDirective can only be used in content bindings" | ||||
|   | ||||
| @@ -13,12 +13,13 @@ export const setupLeafletMap = async ( | ||||
|     throw new Error("Cannot setup Leaflet map on disconnected element"); | ||||
|   } | ||||
|   // eslint-disable-next-line | ||||
|   const Leaflet = ((await import("leaflet")) as any) | ||||
|     .default as LeafletModuleType; | ||||
|   const Leaflet = ((await import( | ||||
|     /* webpackChunkName: "leaflet" */ "leaflet" | ||||
|   )) as any).default as LeafletModuleType; | ||||
|   Leaflet.Icon.Default.imagePath = "/static/images/leaflet/images/"; | ||||
|  | ||||
|   if (draw) { | ||||
|     await import("leaflet-draw"); | ||||
|     await import(/* webpackChunkName: "leaflet-draw" */ "leaflet-draw"); | ||||
|   } | ||||
|  | ||||
|   const map = Leaflet.map(mapElement); | ||||
|   | ||||
| @@ -1,6 +0,0 @@ | ||||
| export const ensureArray = (value?: any) => { | ||||
|   if (!value || Array.isArray(value)) { | ||||
|     return value; | ||||
|   } | ||||
|   return [value]; | ||||
| }; | ||||
| @@ -5,7 +5,6 @@ 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 { formatNumber } from "../string/format_number"; | ||||
|  | ||||
| export const computeStateDisplay = ( | ||||
|   localize: LocalizeFunc, | ||||
| @@ -20,9 +19,7 @@ export const computeStateDisplay = ( | ||||
|   } | ||||
|  | ||||
|   if (stateObj.attributes.unit_of_measurement) { | ||||
|     return `${formatNumber(compareState, language)} ${ | ||||
|       stateObj.attributes.unit_of_measurement | ||||
|     }`; | ||||
|     return `${compareState} ${stateObj.attributes.unit_of_measurement}`; | ||||
|   } | ||||
|  | ||||
|   const domain = computeStateDomain(stateObj); | ||||
| @@ -67,10 +64,6 @@ export const computeStateDisplay = ( | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (domain === "counter") { | ||||
|     return formatNumber(compareState, language); | ||||
|   } | ||||
|  | ||||
|   return ( | ||||
|     // Return device class translation | ||||
|     (stateObj.attributes.device_class && | ||||
|   | ||||
| @@ -43,7 +43,6 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => { | ||||
|       } | ||||
|     case "blind": | ||||
|     case "curtain": | ||||
|     case "shade": | ||||
|       switch (state) { | ||||
|         case "opening": | ||||
|           return "hass:arrow-up-box"; | ||||
| @@ -78,25 +77,3 @@ export const coverIcon = (state?: string, stateObj?: HassEntity): string => { | ||||
|       return "hass:window-open"; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const computeOpenIcon = (stateObj: HassEntity): string => { | ||||
|   switch (stateObj.attributes.device_class) { | ||||
|     case "awning": | ||||
|     case "door": | ||||
|     case "gate": | ||||
|       return "hass:arrow-expand-horizontal"; | ||||
|     default: | ||||
|       return "hass:arrow-up"; | ||||
|   } | ||||
| }; | ||||
|  | ||||
| export const computeCloseIcon = (stateObj: HassEntity): string => { | ||||
|   switch (stateObj.attributes.device_class) { | ||||
|     case "awning": | ||||
|     case "door": | ||||
|     case "gate": | ||||
|       return "hass:arrow-collapse-horizontal"; | ||||
|     default: | ||||
|       return "hass:arrow-down"; | ||||
|   } | ||||
| }; | ||||
|   | ||||
| @@ -77,11 +77,6 @@ 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 type { GroupEntity } from "../../data/group"; | ||||
| import { GroupEntity } from "../../types"; | ||||
| 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 "../../data/group"; | ||||
| import { GroupEntity } from "../../types"; | ||||
|  | ||||
| export const getGroupEntities = ( | ||||
|   entities: HassEntities, | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import { HassEntities } from "home-assistant-js-websocket"; | ||||
| import { GroupEntity } from "../../data/group"; | ||||
| import { GroupEntity } from "../../types"; | ||||
| 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 "../../data/group"; | ||||
| import { GroupEntity } from "../../types"; | ||||
| import { computeDomain } from "./compute_domain"; | ||||
|  | ||||
| // Split a collection into a list of groups and a 'rest' list of ungrouped | ||||
|   | ||||
| @@ -1,132 +0,0 @@ | ||||
| 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! }; | ||||
|     }); | ||||
| @@ -59,7 +59,7 @@ export const fuzzyFilterSort: FuzzyFilterSort = (filter, items) => { | ||||
|         : fuzzySequentialMatch(filter, item.text); | ||||
|       return item; | ||||
|     }) | ||||
|     .filter((item) => item.score !== undefined && item.score > 0) | ||||
|     .filter((item) => item.score === undefined || item.score > 0) | ||||
|     .sort(({ score: scoreA = 0 }, { score: scoreB = 0 }) => | ||||
|       scoreA > scoreB ? -1 : scoreA < scoreB ? 1 : 0 | ||||
|     ); | ||||
|   | ||||
| @@ -1,54 +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 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; | ||||
| }; | ||||
| @@ -13,12 +13,9 @@ export interface FormatsType { | ||||
|   time: FormatType; | ||||
| } | ||||
|  | ||||
| let polyfillLoaded = !shouldPolyfill(); | ||||
| const polyfillProm = polyfillLoaded | ||||
|   ? undefined | ||||
|   : import("@formatjs/intl-pluralrules/polyfill-locales").then(() => { | ||||
|       polyfillLoaded = true; | ||||
|     }); | ||||
| if (shouldPolyfill()) { | ||||
|   await import("@formatjs/intl-pluralrules/polyfill-locales"); | ||||
| } | ||||
|  | ||||
| /** | ||||
|  * Adapted from Polymer app-localize-behavior. | ||||
| @@ -41,16 +38,12 @@ const polyfillProm = polyfillLoaded | ||||
|  * } | ||||
|  */ | ||||
|  | ||||
| export const computeLocalize = async ( | ||||
| export const computeLocalize = ( | ||||
|   cache: any, | ||||
|   language: string, | ||||
|   resources: Resources, | ||||
|   formats?: FormatsType | ||||
| ): Promise<LocalizeFunc> => { | ||||
|   if (!polyfillLoaded) { | ||||
|     await polyfillProm; | ||||
|   } | ||||
|  | ||||
| ): LocalizeFunc => { | ||||
|   // Everytime any of the parameters change, invalidate the strings cache. | ||||
|   cache._localizationCache = {}; | ||||
|  | ||||
| @@ -102,7 +95,7 @@ export const computeLocalize = async ( | ||||
| export const localizeKey = ( | ||||
|   localize: LocalizeFunc, | ||||
|   key: string, | ||||
|   placeholders?: Record<string, string> | ||||
|   placeholders?: { [key: string]: string } | ||||
| ) => { | ||||
|   const args: [string, ...string[]] = [key]; | ||||
|   if (placeholders) { | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| export const extractSearchParamsObject = (): Record<string, string> => { | ||||
| export const extractSearchParamsObject = (): { [key: string]: string } => { | ||||
|   const query = {}; | ||||
|   const searchParams = new URLSearchParams(location.search); | ||||
|   for (const [key, value] of searchParams.entries()) { | ||||
|   | ||||
| @@ -1,12 +0,0 @@ | ||||
| export const copyToClipboard = (str) => { | ||||
|   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,12 +98,6 @@ 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; | ||||
|  | ||||
| @@ -132,8 +126,6 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   @query("slot[name='header']") private _header!: HTMLSlotElement; | ||||
|  | ||||
|   private _items: DataTableRowData[] = []; | ||||
|  | ||||
|   private _checkableRowsCount?: number; | ||||
|  | ||||
|   private _checkedRows: string[] = []; | ||||
| @@ -326,13 +318,10 @@ export class HaDataTable extends LitElement { | ||||
|                   @scroll=${this._saveScrollPos} | ||||
|                 > | ||||
|                   ${scroll({ | ||||
|                     items: this._items, | ||||
|                     items: !this.hasFab | ||||
|                       ? this._filteredData | ||||
|                       : [...this._filteredData, ...[{ empty: true }]], | ||||
|                     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> `; | ||||
|                       } | ||||
| @@ -458,20 +447,6 @@ 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; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import "@material/mwc-icon-button/mwc-icon-button"; | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import "../ha-icon-button"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import "@polymer/paper-item/paper-item"; | ||||
| import "@polymer/paper-item/paper-item-body"; | ||||
| @@ -38,8 +38,6 @@ import { SubscribeMixin } from "../../mixins/subscribe-mixin"; | ||||
| import { PolymerChangedEvent } from "../../polymer-types"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import "./ha-devices-picker"; | ||||
| import "../ha-svg-icon"; | ||||
| import { mdiClose, mdiMenuDown, mdiMenuUp } from "@mdi/js"; | ||||
|  | ||||
| interface DevicesByArea { | ||||
|   [areaId: string]: AreaDevices; | ||||
| @@ -64,7 +62,7 @@ const rowRenderer = ( | ||||
|         margin: -10px 0; | ||||
|         padding: 0; | ||||
|       } | ||||
|       mwc-icon-button { | ||||
|       ha-icon-button { | ||||
|         float: right; | ||||
|       } | ||||
|       .devices { | ||||
| @@ -139,7 +137,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   private _filteredDevices: DeviceRegistryEntry[] = []; | ||||
|  | ||||
|   private _getAreasWithDevices = memoizeOne( | ||||
|   private _getDevices = memoizeOne( | ||||
|     ( | ||||
|       devices: DeviceRegistryEntry[], | ||||
|       areas: AreaRegistryEntry[], | ||||
| @@ -277,7 +275,7 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | ||||
|     if (!this._devices || !this._areas || !this._entities) { | ||||
|       return html``; | ||||
|     } | ||||
|     const areas = this._getAreasWithDevices( | ||||
|     const areas = this._getDevices( | ||||
|       this._devices, | ||||
|       this._areas, | ||||
|       this._entities, | ||||
| @@ -326,34 +324,36 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | ||||
|           autocorrect="off" | ||||
|           spellcheck="false" | ||||
|         > | ||||
|           <div class="suffix" slot="suffix"> | ||||
|             ${this.value | ||||
|               ? html`<mwc-icon-button | ||||
|                   class="clear-button" | ||||
|                   .label=${this.hass.localize( | ||||
|           ${this.value | ||||
|             ? html` | ||||
|                 <ha-icon-button | ||||
|                   aria-label=${this.hass.localize( | ||||
|                     "ui.components.device-picker.clear" | ||||
|                   )} | ||||
|                   slot="suffix" | ||||
|                   class="clear-button" | ||||
|                   icon="hass:close" | ||||
|                   @click=${this._clearValue} | ||||
|                   no-ripple | ||||
|                 > | ||||
|                   <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||
|                 </mwc-icon-button> ` | ||||
|               : ""} | ||||
|             ${areas.length > 0 | ||||
|               ? html` | ||||
|                   <mwc-icon-button | ||||
|                     .label=${this.hass.localize( | ||||
|                       "ui.components.device-picker.show_devices" | ||||
|                     )} | ||||
|                     class="toggle-button" | ||||
|                   > | ||||
|                     <ha-svg-icon | ||||
|                       .path=${this._opened ? mdiMenuUp : mdiMenuDown} | ||||
|                     ></ha-svg-icon> | ||||
|                   </mwc-icon-button> | ||||
|                 ` | ||||
|               : ""} | ||||
|           </div> | ||||
|                   Clear | ||||
|                 </ha-icon-button> | ||||
|               ` | ||||
|             : ""} | ||||
|           ${areas.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> | ||||
|               ` | ||||
|             : ""} | ||||
|         </paper-input> | ||||
|       </vaadin-combo-box-light> | ||||
|       <mwc-button @click=${this._switchPicker} | ||||
| @@ -409,12 +409,10 @@ export class HaAreaDevicesPicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   static get styles(): CSSResult { | ||||
|     return css` | ||||
|       .suffix { | ||||
|         display: flex; | ||||
|       } | ||||
|       mwc-icon-button { | ||||
|         --mdc-icon-button-size: 24px; | ||||
|         padding: 0px 2px; | ||||
|       paper-input > ha-icon-button { | ||||
|         width: 24px; | ||||
|         height: 24px; | ||||
|         padding: 2px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|       [hidden] { | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import "../ha-svg-icon"; | ||||
| import "@material/mwc-icon-button/mwc-icon-button"; | ||||
| import "../ha-icon-button"; | ||||
| import "@polymer/paper-input/paper-input"; | ||||
| import "@polymer/paper-item/paper-item"; | ||||
| import "@polymer/paper-item/paper-item-body"; | ||||
| @@ -13,8 +12,6 @@ import { | ||||
|   html, | ||||
|   LitElement, | ||||
|   property, | ||||
|   PropertyValues, | ||||
|   query, | ||||
|   TemplateResult, | ||||
| } from "lit-element"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| @@ -38,7 +35,6 @@ 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; | ||||
| @@ -46,10 +42,6 @@ interface Device { | ||||
|   id: string; | ||||
| } | ||||
|  | ||||
| export type HaDevicePickerDeviceFilterFunc = ( | ||||
|   device: DeviceRegistryEntry | ||||
| ) => boolean; | ||||
|  | ||||
| const rowRenderer = (root: HTMLElement, _owner, model: { item: Device }) => { | ||||
|   if (!root.firstElementChild) { | ||||
|     root.innerHTML = ` | ||||
| @@ -110,15 +102,9 @@ 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[], | ||||
| @@ -126,31 +112,21 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|       entities: EntityRegistryEntry[], | ||||
|       includeDomains: this["includeDomains"], | ||||
|       excludeDomains: this["excludeDomains"], | ||||
|       includeDeviceClasses: this["includeDeviceClasses"], | ||||
|       deviceFilter: this["deviceFilter"] | ||||
|       includeDeviceClasses: this["includeDeviceClasses"] | ||||
|     ): Device[] => { | ||||
|       if (!devices.length) { | ||||
|         return [ | ||||
|           { | ||||
|             id: "", | ||||
|             area: "", | ||||
|             name: this.hass.localize("ui.components.device-picker.no_devices"), | ||||
|           }, | ||||
|         ]; | ||||
|         return []; | ||||
|       } | ||||
|  | ||||
|       const deviceEntityLookup: DeviceEntityLookup = {}; | ||||
|  | ||||
|       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); | ||||
|       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); | ||||
|       } | ||||
|  | ||||
|       const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; | ||||
| @@ -158,9 +134,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|         areaLookup[area.area_id] = area; | ||||
|       } | ||||
|  | ||||
|       let inputDevices = devices.filter( | ||||
|         (device) => device.id === this.value || !device.disabled_by | ||||
|       ); | ||||
|       let inputDevices = [...devices]; | ||||
|  | ||||
|       if (includeDomains) { | ||||
|         inputDevices = inputDevices.filter((device) => { | ||||
| @@ -206,14 +180,6 @@ 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, | ||||
| @@ -227,15 +193,6 @@ 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; | ||||
|       } | ||||
| @@ -243,18 +200,6 @@ 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) => { | ||||
| @@ -269,33 +214,24 @@ 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} | ||||
| @@ -313,30 +249,34 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|         > | ||||
|           ${this.value | ||||
|             ? html` | ||||
|                 <mwc-icon-button | ||||
|                   .label=${this.hass.localize( | ||||
|                 <ha-icon-button | ||||
|                   aria-label=${this.hass.localize( | ||||
|                     "ui.components.device-picker.clear" | ||||
|                   )} | ||||
|                   slot="suffix" | ||||
|                   class="clear-button" | ||||
|                   icon="hass:close" | ||||
|                   @click=${this._clearValue} | ||||
|                   no-ripple | ||||
|                 > | ||||
|                   <ha-svg-icon .path=${mdiClose}></ha-svg-icon> | ||||
|                 </mwc-icon-button> | ||||
|                   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> | ||||
|               ` | ||||
|             : ""} | ||||
|  | ||||
|           <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> | ||||
|     `; | ||||
| @@ -373,7 +313,7 @@ export class HaDevicePicker extends SubscribeMixin(LitElement) { | ||||
|  | ||||
|   static get styles(): CSSResult { | ||||
|     return css` | ||||
|       paper-input > mwc-icon-button { | ||||
|       paper-input > ha-icon-button { | ||||
|         --mdc-icon-button-size: 24px; | ||||
|         padding: 2px; | ||||
|         color: var(--secondary-text-color); | ||||
|   | ||||
| @@ -88,7 +88,6 @@ class HaChartBase extends mixinBehaviors( | ||||
|         .chartTooltip .beforeBody { | ||||
|           text-align: center; | ||||
|           font-weight: 300; | ||||
|           word-break: break-all; | ||||
|         } | ||||
|         .chartLegend li { | ||||
|           display: inline-block; | ||||
| @@ -230,7 +229,9 @@ class HaChartBase extends mixinBehaviors( | ||||
|     } | ||||
|  | ||||
|     if (scriptsLoaded === null) { | ||||
|       scriptsLoaded = import("../../resources/ha-chart-scripts.js"); | ||||
|       scriptsLoaded = import( | ||||
|         /* webpackChunkName: "load_chart" */ "../../resources/ha-chart-scripts.js" | ||||
|       ); | ||||
|     } | ||||
|     scriptsLoaded.then((ChartModule) => { | ||||
|       this.ChartClass = ChartModule.default; | ||||
|   | ||||
| @@ -19,7 +19,6 @@ import { PolymerChangedEvent } from "../../polymer-types"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| 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; | ||||
| @@ -36,9 +35,7 @@ const rowRenderer = (root: HTMLElement, _owner, model: { item: string }) => { | ||||
|       <paper-item></paper-item> | ||||
|     `; | ||||
|   } | ||||
|   root.querySelector("paper-item")!.textContent = formatAttributeName( | ||||
|     model.item | ||||
|   ); | ||||
|   root.querySelector("paper-item")!.textContent = model.item; | ||||
| }; | ||||
|  | ||||
| @customElement("ha-entity-attribute-picker") | ||||
| @@ -95,7 +92,7 @@ class HaEntityAttributePicker extends LitElement { | ||||
|           this.hass.localize( | ||||
|             "ui.components.entity.entity-attribute-picker.attribute" | ||||
|           )} | ||||
|           .value=${this._value ? formatAttributeName(this._value) : ""} | ||||
|           .value=${this._value} | ||||
|           .disabled=${this.disabled || !this.entityId} | ||||
|           class="input" | ||||
|           autocapitalize="none" | ||||
| @@ -143,7 +140,7 @@ class HaEntityAttributePicker extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private get _value() { | ||||
|     return this.value; | ||||
|     return this.value || ""; | ||||
|   } | ||||
|  | ||||
|   private _openedChanged(ev: PolymerChangedEvent<boolean>) { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user