mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-29 21:49:45 +00:00 
			
		
		
		
	Compare commits
	
		
			97 Commits
		
	
	
		
			20230831.0
			...
			delay-init
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 7f2fcc73b5 | ||
|   | 4b5c7021ff | ||
|   | 3349031cbd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 5e107d43d7 | ||
|   | e46f2cd9bf | ||
|   | 713ebfcc22 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 46e4eafe95 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e6fd18e23b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 71cd71dfd5 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1019ccfd26 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 577c1d8522 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 63f0b469cc | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e688417863 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | a19633e2d4 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 8797142cca | ||
|   | 2a7403b6fd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 22efe14149 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7cce24bcd1 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b8f0bb66cd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b950f990b4 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | b511e7a37d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 50f4b78f2e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7b0b4cdfe4 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c60e5c4c61 | ||
|   | 709a63e6da | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f689eed073 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | cd55eee2fc | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | cf27e68748 | ||
|   | 472ed2fe82 | ||
|   | d0a60984ed | ||
|   | 24d401061c | ||
|   | 2352d05573 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 87d53e38c4 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | db3c535884 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 158b24f902 | ||
|   | 19c4ed4690 | ||
|   | eae4ca1271 | ||
|   | 0276430ab5 | ||
|   | db7caf1c32 | ||
|   | 7176a51fec | ||
|   | 4a6539d75b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 850699ea70 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c17cc22f88 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 9e3f2d5cb7 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 0677c9c7b0 | ||
|   | af7e385884 | ||
|   | ba88fef09b | ||
|   | ad0e59c8f4 | ||
|   | 14e6f5e8ca | ||
|   | 3c48157793 | ||
|   | 3a07af6ad2 | ||
|   | c1c05f8d22 | ||
|   | 29aed5371c | ||
|   | 76c878df57 | ||
|   | d6e7ebe71d | ||
|   | 085b26d5ea | ||
|   | 32472ca627 | ||
|   | c3c4bb4421 | ||
|   | f7f1a0c32d | ||
|   | d4872b177f | ||
|   | 5bb8c51d25 | ||
|   | 77c08fd00f | ||
|   | d8894a0078 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4fd9c63633 | ||
|   | 5e1583f925 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 5d5894cae6 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 5417513f49 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 546ba8f12f | ||
|   | a398b37380 | ||
|   | 321f35f30e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 82dfb06a04 | ||
|   | e666aac1bd | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 9e9a0e377e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | ba3f9a318b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f3b4eefb72 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 6ac1db6953 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1b42189dd6 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 0d893b3d2b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7b167a4d7e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 8e2f1026e7 | ||
|   | fe3a63af80 | ||
|   | 5da4e1860a | ||
|   | 6dcb7f2273 | ||
|   | 53ae7e5a0c | ||
|   | 56381f9914 | ||
|   | be31aecf00 | ||
|   | cc5fffc174 | ||
|   | dd8a50af31 | ||
|   | c8feded4f2 | ||
|   | 0d0fe75f4e | ||
|   | fb69deb617 | ||
|   | c291af5d97 | ||
|   | 6d63028406 | ||
|   | 3917739ad2 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | e98e59a265 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 16ed60902d | ||
|   | 6c7efc17c2 | 
							
								
								
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
| @@ -57,7 +57,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|   | ||||
							
								
								
									
										10
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										10
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,7 +24,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
| @@ -37,7 +37,7 @@ jobs: | ||||
|       - name: Build resources | ||||
|         run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages | ||||
|       - name: Setup lint cache | ||||
|         uses: actions/cache@v3.3.1 | ||||
|         uses: actions/cache@v3.3.2 | ||||
|         with: | ||||
|           path: | | ||||
|             node_modules/.cache/prettier | ||||
| @@ -55,7 +55,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
| @@ -73,7 +73,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
| @@ -91,7 +91,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|         with: | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|  | ||||
|     steps: | ||||
|       - name: Checkout repository | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|         with: | ||||
|           # We must fetch at least the immediate parents so that if this is | ||||
|           # a pull request then we can checkout the head. | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -22,7 +22,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|         with: | ||||
|           ref: dev | ||||
|  | ||||
| @@ -58,7 +58,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|         with: | ||||
|           ref: master | ||||
|  | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -16,7 +16,7 @@ jobs: | ||||
|       url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }} | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -21,7 +21,7 @@ jobs: | ||||
|     if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview') | ||||
|     steps: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v3.8.1 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -20,7 +20,7 @@ jobs: | ||||
|       contents: write | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|  | ||||
|       - name: Set up Python ${{ env.PYTHON_VERSION }} | ||||
|         uses: actions/setup-python@v4 | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -23,7 +23,7 @@ jobs: | ||||
|       contents: write # Required to upload release assets | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|  | ||||
|       - name: Verify version | ||||
|         uses: home-assistant/actions/helpers/verify-version@master | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -13,7 +13,7 @@ jobs: | ||||
|     runs-on: ubuntu-latest | ||||
|     steps: | ||||
|       - name: Checkout the repository | ||||
|         uses: actions/checkout@v3.6.0 | ||||
|         uses: actions/checkout@v4.0.0 | ||||
|  | ||||
|       - name: Upload Translations | ||||
|         run: | | ||||
|   | ||||
| @@ -100,6 +100,7 @@ module.exports.babelOptions = ({ latestBuild, isProdBuild, isTestBuild }) => ({ | ||||
|         useBuiltIns: latestBuild ? false : "entry", | ||||
|         corejs: latestBuild ? false : { version: "3.32", proposals: true }, | ||||
|         bugfixes: true, | ||||
|         shippedProposals: true, | ||||
|       }, | ||||
|     ], | ||||
|     "@babel/preset-typescript", | ||||
|   | ||||
| @@ -1,10 +1,14 @@ | ||||
| import fs from "fs/promises"; | ||||
| import gulp from "gulp"; | ||||
| import path from "path"; | ||||
| import mapStream from "map-stream"; | ||||
| import transform from "gulp-json-transform"; | ||||
| import { LokaliseApi } from "@lokalise/node-api"; | ||||
| import JSZip from "jszip"; | ||||
|  | ||||
| const inDirFrontend = "translations/frontend"; | ||||
| const inDirBackend = "translations/backend"; | ||||
| const inDir = "translations"; | ||||
| const inDirFrontend = `${inDir}/frontend`; | ||||
| const inDirBackend = `${inDir}/backend`; | ||||
| const srcMeta = "src/translations/translationMetadata.json"; | ||||
| const encoding = "utf8"; | ||||
|  | ||||
| @@ -68,8 +72,9 @@ gulp.task("convert-backend-translations", function () { | ||||
| }); | ||||
|  | ||||
| gulp.task("check-translations-html", function () { | ||||
|   // We exclude backend translations because they are not compliant with the HTML rule for now | ||||
|   return gulp.src([`${inDirFrontend}/*.json`]).pipe(checkHtml()); | ||||
|   return gulp | ||||
|     .src([`${inDirFrontend}/*.json`, `${inDirBackend}/*.json`]) | ||||
|     .pipe(checkHtml()); | ||||
| }); | ||||
|  | ||||
| gulp.task("check-all-files-exist", async function () { | ||||
| @@ -89,7 +94,83 @@ gulp.task("check-all-files-exist", async function () { | ||||
|   await Promise.allSettled(writings); | ||||
| }); | ||||
|  | ||||
| const lokaliseProjects = { | ||||
|   backend: "130246255a974bd3b5e8a1.51616605", | ||||
|   frontend: "3420425759f6d6d241f598.13594006", | ||||
| }; | ||||
|  | ||||
| gulp.task("fetch-lokalise", async function () { | ||||
|   let apiKey; | ||||
|   try { | ||||
|     apiKey = | ||||
|       process.env.LOKALISE_TOKEN || | ||||
|       (await fs.readFile(".lokalise_token", { encoding })); | ||||
|   } catch { | ||||
|     throw new Error( | ||||
|       "An Administrator Lokalise API token is required to download the latest set of translations. Place your token in a new file `.lokalise_token` in the repo root directory." | ||||
|     ); | ||||
|   } | ||||
|   const lokaliseApi = new LokaliseApi({ apiKey }); | ||||
|  | ||||
|   const mkdirPromise = Promise.all([ | ||||
|     fs.mkdir(inDirFrontend, { recursive: true }), | ||||
|     fs.mkdir(inDirBackend, { recursive: true }), | ||||
|   ]); | ||||
|  | ||||
|   await Promise.all( | ||||
|     Object.entries(lokaliseProjects).map(([project, projectId]) => | ||||
|       lokaliseApi | ||||
|         .files() | ||||
|         .download(projectId, { | ||||
|           format: "json", | ||||
|           original_filenames: false, | ||||
|           replace_breaks: false, | ||||
|           json_unescaped_slashes: true, | ||||
|           export_empty_as: "skip", | ||||
|         }) | ||||
|         .then((download) => fetch(download.bundle_url)) | ||||
|         .then((response) => { | ||||
|           if (response.status === 200 || response.status === 0) { | ||||
|             return response.arrayBuffer(); | ||||
|           } | ||||
|           throw new Error(response.statusText); | ||||
|         }) | ||||
|         .then(JSZip.loadAsync) | ||||
|         .then(async (contents) => { | ||||
|           await mkdirPromise; | ||||
|           return Promise.all( | ||||
|             Object.keys(contents.files).map(async (filename) => { | ||||
|               const file = contents.file(filename); | ||||
|               if (!file) { | ||||
|                 // no file, probably a directory | ||||
|                 return Promise.resolve(); | ||||
|               } | ||||
|               return file | ||||
|                 .async("nodebuffer") | ||||
|                 .then((content) => | ||||
|                   fs.writeFile( | ||||
|                     path.join( | ||||
|                       inDir, | ||||
|                       project, | ||||
|                       filename.split("/").splice(-1)[0] | ||||
|                     ), | ||||
|                     content, | ||||
|                     { flag: "w", encoding } | ||||
|                   ) | ||||
|                 ); | ||||
|             }) | ||||
|           ); | ||||
|         }) | ||||
|     ) | ||||
|   ); | ||||
| }); | ||||
|  | ||||
| gulp.task( | ||||
|   "check-downloaded-translations", | ||||
|   gulp.series("check-translations-html", "check-all-files-exist") | ||||
|   "download-translations", | ||||
|   gulp.series( | ||||
|     "fetch-lokalise", | ||||
|     "convert-backend-translations", | ||||
|     "check-translations-html", | ||||
|     "check-all-files-exist" | ||||
|   ) | ||||
| ); | ||||
|   | ||||
| @@ -6,6 +6,8 @@ import presetEnv from "@babel/preset-env"; | ||||
| import compilationTargets from "@babel/helper-compilation-targets"; | ||||
| import coreJSCompat from "core-js-compat"; | ||||
| import { logPlugin } from "@babel/preset-env/lib/debug.js"; | ||||
| // eslint-disable-next-line import/no-relative-packages | ||||
| import shippedPolyfills from "../node_modules/babel-plugin-polyfill-corejs3/lib/shipped-proposals.js"; | ||||
| import { babelOptions } from "./bundle.cjs"; | ||||
|  | ||||
| const detailsOpen = (heading) => | ||||
| @@ -26,6 +28,22 @@ const dummyAPI = { | ||||
|   targets: () => ({}), | ||||
| }; | ||||
|  | ||||
| // Generate filter function based on proposal/method inputs | ||||
| // Copied and adapted from babel-plugin-polyfill-corejs3/esm/index.mjs | ||||
| const polyfillFilter = (method, proposals, shippedProposals) => (name) => { | ||||
|   if (proposals || method === "entry-global") return true; | ||||
|   if (shippedProposals && shippedPolyfills.default.has(name)) { | ||||
|     return true; | ||||
|   } | ||||
|   if (name.startsWith("esnext.")) { | ||||
|     const esName = `es.${name.slice(7)}`; | ||||
|     // If its imaginative esName is not in latest compat data, it means the proposal is not stage 4 | ||||
|     return esName in coreJSCompat.data; | ||||
|   } | ||||
|   return true; | ||||
| }; | ||||
|  | ||||
| // Log the plugins and polyfills for each build environment | ||||
| for (const buildType of ["Modern", "Legacy"]) { | ||||
|   const browserslistEnv = buildType.toLowerCase(); | ||||
|   const babelOpts = babelOptions({ latestBuild: browserslistEnv === "modern" }); | ||||
| @@ -46,7 +64,13 @@ for (const buildType of ["Modern", "Legacy"]) { | ||||
|     const targets = compilationTargets.default(babelOpts?.targets, { | ||||
|       browserslistEnv, | ||||
|     }); | ||||
|     const polyfillList = coreJSCompat({ targets }).list; | ||||
|     const polyfillList = coreJSCompat({ targets }).list.filter( | ||||
|       polyfillFilter( | ||||
|         `${presetEnvOpts.useBuiltIns}-global`, | ||||
|         presetEnvOpts?.corejs?.proposals, | ||||
|         presetEnvOpts?.shippedProposals | ||||
|       ) | ||||
|     ); | ||||
|     console.log( | ||||
|       "The following %i polyfills may be injected by Babel:\n", | ||||
|       polyfillList.length | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							
							
								
								
									
										4
									
								
								gallery/public/images/brand/README.md
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										4
									
								
								gallery/public/images/brand/README.md
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,4 @@ | ||||
| # Note! | ||||
|  | ||||
| Note, the assets in this folder, are not part of the CC license this repository is shipped in. | ||||
| All rights reserved. | ||||
							
								
								
									
										
											BIN
										
									
								
								gallery/public/images/brand/logo-exclusion-zone.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gallery/public/images/brand/logo-exclusion-zone.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 40 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gallery/public/images/brand/logo-layout-variants.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gallery/public/images/brand/logo-layout-variants.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 56 KiB | 
							
								
								
									
										
											BIN
										
									
								
								gallery/public/images/brand/logo.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								gallery/public/images/brand/logo.png
									
									
									
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 25 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 22 KiB | 
| @@ -2,30 +2,86 @@ | ||||
| title: "Logo" | ||||
| --- | ||||
|  | ||||
| # Using our logo | ||||
| # Our logo | ||||
|  | ||||
| As a community, we are proud of our logo. Follow these guidelines to ensure it always looks its best. Our logo follows Google's material design spec and uses the blue interface color. | ||||
| As a community, we are proud of our logo. Follow these guidelines to ensure it always represents the identity of the Home Assistant project and community the best way possible. | ||||
|  | ||||
| [Download Logo](https://github.com/home-assistant/assets/tree/master/logo) | ||||
|  | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Using the icon | ||||
| Please note that this logo is not released under the CC license. All rights reserved. | ||||
|  | ||||
| Our icon is a shorter and most used version of our logo. The icon can exist without the wordmark, the wordmark should never exist without the icon. | ||||
| # Design | ||||
|  | ||||
|  | ||||
| At the core of the Home Assistant logomark is the Blue House with Antenna, the three most recognizable and distinct features of the previous logo throughout the past decade. | ||||
|  | ||||
| ## Using the right variant | ||||
| ### Blue | ||||
|  | ||||
| The pretty blue logo with a background shadow, pictured top left, is our primary logo. It should only be used with black, white, and non-duotone photography. | ||||
| Blue feels stable and essential. A bright sky blue is joyful, clear, and free of clouds. | ||||
|  | ||||
| When needed you can use our logo without a shadow, as seen as the second variant. | ||||
| ### House | ||||
|  | ||||
| The outlined logo should only be used on packaging. | ||||
| Of all possible combinations of shapes, a home is best abstracted in the shape of a structure with a pitched roof. With the vast amount of logos based on this shape, the best we can do is to make it more iconic. The house is further simplified - there is no gable and there is no chimney - to an orthogonal shape with an elegant and deliberate proportion. | ||||
|  | ||||
| ## Exclusion zone | ||||
| ### Antenna | ||||
|  | ||||
| The logo needs some personal space. It's exclusion zone is equal to a quarter the height of the icon. | ||||
| Call it a tree, a set of nodes, a PCB, or an antenna. The antenna is the most recognizable and memorable part of the previous Home Assistant logo, and is an easily understandable symbol that conveys technologies that are smart, connected, and growing evergreen. | ||||
|  | ||||
|  | ||||
| # Usage | ||||
|  | ||||
| The default variation is the static colored wordmark in horizontal layout and dark text on a light background. | ||||
|  | ||||
| ## Layout variations | ||||
|  | ||||
|  | ||||
|  | ||||
| The default layout is the wordmark in horizontal layout. It provides the clearest context to the brand identity of Home Assistant. | ||||
|  | ||||
| Use the logomark variant when the context is clear that the logo is about Home Assistant. For example, inside the Home Assistant app where users are already aware of where they are at, the logomark variant without the wordmark can be used. The logomark can exist without the wordmark, however, the wordmark should never exist without the icon. | ||||
|  | ||||
| Use the wordmark in vertical layout when the space available has an aspect ratio less than 4:3. For example, in a square space on a t-shirt where a logo is needed, since there is no established context of Home Assistant, the wordmark in vertical layout should be used. | ||||
|  | ||||
| Lastly, use the wordmark in vertical layout with small logomark when Home Assistant is displayed in context of other Home Assistant-related projects. For example, in a flowchart showing the voice pipeline, use this layout for Home Assistant and its other related projects. | ||||
|  | ||||
| ## Color variations, backgrounds, and placement | ||||
|  | ||||
| The default color is the colored version on light background with dark text. | ||||
|  | ||||
| For backgrounds that are dark, for example, when it is used on a page in a dark theme, use the colored version on dark background with light text. | ||||
|  | ||||
| In printed materials where color is unavailable, use the monochrome color variations. | ||||
|  | ||||
| On background that are dark or photographic, use the light monochrome color on dark background variation. | ||||
|  | ||||
| On backgrounds that are light or photographic, use the colored version. Do not use the monochrome variations. | ||||
|  | ||||
| Do not enclose the logmark in a square or color or any confined backgrounds, except in specific situations enforced by another company's marketplace guidelines, for example, an iOS app icon. | ||||
|  | ||||
| Do not add drop shadow to the logomark or the wordmark. If legibility is compromised due to the background, change the background to provide more contrast, or in last resort, add a heavily blurred drop shadaow. | ||||
|  | ||||
| It should only be used with black, white, and non-duotone photography. | ||||
|  | ||||
| Unlike the previous version of our logo, no outlined variants are available. Use the monochrome variants in those spaces. | ||||
|  | ||||
| ### Exclusion zone | ||||
|  | ||||
| The logo needs some personal space. Its exclusion zone is equal to a quarter the height of the icon. | ||||
|  | ||||
|  | ||||
|  | ||||
| ## Animation | ||||
|  | ||||
| The default is the static variant. | ||||
|  | ||||
| Use the animated variant only for introductory purposes, for example, in the beginning of a video or on a loading screen. | ||||
|  | ||||
| Use the animated with sound variant only when sound is warranted in the user's context. For example, use it in the beginning of a video since sounds are expected in a video, but do not use it on a loading screen since sounds are not expected in a user interface. | ||||
|  | ||||
| Do not repeat the logo animation. | ||||
|  | ||||
| ## Sizes and app icon variants | ||||
|  | ||||
| Special variants are created for specific contexts. | ||||
|  | ||||
| Use the tiny variants when the logomark is used in a very small space (16x16 dp), for example, the favicon of the Home Assistant website, a notification on Android, or the menubar of macOS. | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| --- | ||||
| title: Temp Color Picker | ||||
| --- | ||||
| @@ -1,117 +0,0 @@ | ||||
| import "../../../../src/components/ha-temp-color-picker"; | ||||
|  | ||||
| import { css, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, state } from "lit/decorators"; | ||||
|  | ||||
| import "../../../../src/components/ha-card"; | ||||
| import "../../../../src/components/ha-slider"; | ||||
|  | ||||
| @customElement("demo-components-ha-temp-color-picker") | ||||
| export class DemoHaTempColorPicker extends LitElement { | ||||
|   @state() | ||||
|   min = 3000; | ||||
|  | ||||
|   @state() | ||||
|   max = 7000; | ||||
|  | ||||
|   @state() | ||||
|   value = 4000; | ||||
|  | ||||
|   @state() | ||||
|   liveValue?: number; | ||||
|  | ||||
|   private _minChanged(ev) { | ||||
|     this.min = Number(ev.target.value); | ||||
|   } | ||||
|  | ||||
|   private _maxChanged(ev) { | ||||
|     this.max = Number(ev.target.value); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev) { | ||||
|     this.value = Number(ev.target.value); | ||||
|   } | ||||
|  | ||||
|   private _tempColorCursor(ev) { | ||||
|     this.liveValue = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   private _tempColorChanged(ev) { | ||||
|     this.value = ev.detail.value; | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <ha-card> | ||||
|         <div class="card-content"> | ||||
|           <p class="value">${this.liveValue ?? this.value} K</p> | ||||
|           <ha-temp-color-picker | ||||
|             .min=${this.min} | ||||
|             .max=${this.max} | ||||
|             .value=${this.value} | ||||
|             @value-changed=${this._tempColorChanged} | ||||
|             @cursor-moved=${this._tempColorCursor} | ||||
|           ></ha-temp-color-picker> | ||||
|           <p>Min temp : ${this.min} K</p> | ||||
|           <ha-slider | ||||
|             step="1" | ||||
|             pin | ||||
|             min="2000" | ||||
|             max="10000" | ||||
|             .value=${this.min} | ||||
|             @change=${this._minChanged} | ||||
|           > | ||||
|           </ha-slider> | ||||
|           <p>Max temp : ${this.max} K</p> | ||||
|           <ha-slider | ||||
|             step="1" | ||||
|             pin | ||||
|             min="2000" | ||||
|             max="10000" | ||||
|             .value=${this.max} | ||||
|             @change=${this._maxChanged} | ||||
|           > | ||||
|           </ha-slider> | ||||
|           <p>Value : ${this.value} K</p> | ||||
|           <ha-slider | ||||
|             step="1" | ||||
|             pin | ||||
|             min=${this.min} | ||||
|             max=${this.max} | ||||
|             .value=${this.value} | ||||
|             @change=${this._valueChanged} | ||||
|           > | ||||
|           </ha-slider> | ||||
|         </div> | ||||
|       </ha-card> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       ha-card { | ||||
|         max-width: 600px; | ||||
|         margin: 24px auto; | ||||
|       } | ||||
|       .card-content { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|         flex-direction: column; | ||||
|       } | ||||
|       ha-temp-color-picker { | ||||
|         width: 400px; | ||||
|       } | ||||
|       .value { | ||||
|         font-size: 22px; | ||||
|         font-weight: bold; | ||||
|         margin: 0 0 12px 0; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "demo-components-ha-temp-color-picker": DemoHaTempColorPicker; | ||||
|   } | ||||
| } | ||||
| @@ -343,7 +343,7 @@ export class DemoEntityState extends LitElement { | ||||
|       const columns: DataTableColumnContainer<EntityRowData> = { | ||||
|         icon: { | ||||
|           title: "Icon", | ||||
|           template: (_, entry) => html` | ||||
|           template: (entry) => html` | ||||
|             <state-badge | ||||
|               .stateObj=${entry.stateObj} | ||||
|               .stateColor=${true} | ||||
| @@ -360,7 +360,7 @@ export class DemoEntityState extends LitElement { | ||||
|           title: "State", | ||||
|           width: "20%", | ||||
|           sortable: true, | ||||
|           template: (_, entry) => | ||||
|           template: (entry) => | ||||
|             html`${computeStateDisplay( | ||||
|               hass.localize, | ||||
|               entry.stateObj, | ||||
| @@ -371,14 +371,14 @@ export class DemoEntityState extends LitElement { | ||||
|         }, | ||||
|         device_class: { | ||||
|           title: "Device class", | ||||
|           template: (dc) => html`${dc ?? "-"}`, | ||||
|           template: (entry) => html`${entry.device_class ?? "-"}`, | ||||
|           width: "20%", | ||||
|           filterable: true, | ||||
|           sortable: true, | ||||
|         }, | ||||
|         domain: { | ||||
|           title: "Domain", | ||||
|           template: (_, entry) => html`${computeDomain(entry.entity_id)}`, | ||||
|           template: (entry) => html`${computeDomain(entry.entity_id)}`, | ||||
|           width: "20%", | ||||
|           filterable: true, | ||||
|           sortable: true, | ||||
|   | ||||
| @@ -49,6 +49,10 @@ import { showHassioCreateBackupDialog } from "../dialogs/backup/show-dialog-hass | ||||
| import { supervisorTabs } from "../hassio-tabs"; | ||||
| import { hassioStyle } from "../resources/hassio-style"; | ||||
|  | ||||
| type BackupItem = HassioBackup & { | ||||
|   secondary: string; | ||||
| }; | ||||
|  | ||||
| @customElement("hassio-backups") | ||||
| export class HassioBackups extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -117,15 +121,15 @@ export class HassioBackups extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _columns = memoizeOne( | ||||
|     (narrow: boolean): DataTableColumnContainer => ({ | ||||
|     (narrow: boolean): DataTableColumnContainer<BackupItem> => ({ | ||||
|       name: { | ||||
|         title: this.supervisor.localize("backup.name"), | ||||
|         main: true, | ||||
|         sortable: true, | ||||
|         filterable: true, | ||||
|         grows: true, | ||||
|         template: (entry: string, backup: any) => | ||||
|           html`${entry || backup.slug} | ||||
|         template: (backup) => | ||||
|           html`${backup.name || backup.slug} | ||||
|             <div class="secondary">${backup.secondary}</div>`, | ||||
|       }, | ||||
|       size: { | ||||
| @@ -134,7 +138,7 @@ export class HassioBackups extends LitElement { | ||||
|         hidden: narrow, | ||||
|         filterable: true, | ||||
|         sortable: true, | ||||
|         template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB", | ||||
|         template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB", | ||||
|       }, | ||||
|       location: { | ||||
|         title: this.supervisor.localize("backup.location"), | ||||
| @@ -142,8 +146,8 @@ export class HassioBackups extends LitElement { | ||||
|         hidden: narrow, | ||||
|         filterable: true, | ||||
|         sortable: true, | ||||
|         template: (entry: string | null) => | ||||
|           entry || this.supervisor.localize("backup.data_disk"), | ||||
|         template: (backup) => | ||||
|           backup.location || this.supervisor.localize("backup.data_disk"), | ||||
|       }, | ||||
|       date: { | ||||
|         title: this.supervisor.localize("backup.created"), | ||||
| @@ -152,8 +156,8 @@ export class HassioBackups extends LitElement { | ||||
|         hidden: narrow, | ||||
|         filterable: true, | ||||
|         sortable: true, | ||||
|         template: (entry: string) => | ||||
|           relativeTime(new Date(entry), this.hass.locale), | ||||
|         template: (backup) => | ||||
|           relativeTime(new Date(backup.date), this.hass.locale), | ||||
|       }, | ||||
|       secondary: { | ||||
|         title: "", | ||||
| @@ -163,7 +167,7 @@ export class HassioBackups extends LitElement { | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
|   private _backupData = memoizeOne((backups: HassioBackup[]) => | ||||
|   private _backupData = memoizeOne((backups: HassioBackup[]): BackupItem[] => | ||||
|     backups.map((backup) => ({ | ||||
|       ...backup, | ||||
|       secondary: this._computeBackupContent(backup), | ||||
|   | ||||
| @@ -33,7 +33,6 @@ export class HassioUploadBackup extends LitElement { | ||||
|         label="Upload backup" | ||||
|         supports="Supports .TAR files" | ||||
|         @file-picked=${this._uploadFile} | ||||
|         auto-open-file-dialog | ||||
|       ></ha-file-upload> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
							
								
								
									
										95
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										95
									
								
								package.json
									
									
									
									
									
								
							| @@ -25,24 +25,24 @@ | ||||
|   "license": "Apache-2.0", | ||||
|   "type": "module", | ||||
|   "dependencies": { | ||||
|     "@babel/runtime": "7.22.11", | ||||
|     "@babel/runtime": "7.22.15", | ||||
|     "@braintree/sanitize-url": "6.0.4", | ||||
|     "@codemirror/autocomplete": "6.9.0", | ||||
|     "@codemirror/autocomplete": "6.9.1", | ||||
|     "@codemirror/commands": "6.2.5", | ||||
|     "@codemirror/language": "6.9.0", | ||||
|     "@codemirror/legacy-modes": "6.3.3", | ||||
|     "@codemirror/search": "6.5.2", | ||||
|     "@codemirror/search": "6.5.3", | ||||
|     "@codemirror/state": "6.2.1", | ||||
|     "@codemirror/view": "6.16.0", | ||||
|     "@codemirror/view": "6.19.0", | ||||
|     "@egjs/hammerjs": "2.0.17", | ||||
|     "@formatjs/intl-datetimeformat": "6.10.0", | ||||
|     "@formatjs/intl-displaynames": "6.5.0", | ||||
|     "@formatjs/intl-datetimeformat": "6.10.2", | ||||
|     "@formatjs/intl-displaynames": "6.5.2", | ||||
|     "@formatjs/intl-getcanonicallocales": "2.2.1", | ||||
|     "@formatjs/intl-listformat": "7.4.0", | ||||
|     "@formatjs/intl-locale": "3.3.2", | ||||
|     "@formatjs/intl-numberformat": "8.7.0", | ||||
|     "@formatjs/intl-pluralrules": "5.2.4", | ||||
|     "@formatjs/intl-relativetimeformat": "11.2.4", | ||||
|     "@formatjs/intl-listformat": "7.4.2", | ||||
|     "@formatjs/intl-locale": "3.3.4", | ||||
|     "@formatjs/intl-numberformat": "8.7.2", | ||||
|     "@formatjs/intl-pluralrules": "5.2.6", | ||||
|     "@formatjs/intl-relativetimeformat": "11.2.6", | ||||
|     "@fullcalendar/core": "6.1.8", | ||||
|     "@fullcalendar/daygrid": "6.1.8", | ||||
|     "@fullcalendar/interaction": "6.1.8", | ||||
| @@ -50,10 +50,10 @@ | ||||
|     "@fullcalendar/luxon3": "6.1.8", | ||||
|     "@fullcalendar/timegrid": "6.1.8", | ||||
|     "@lezer/highlight": "1.1.6", | ||||
|     "@lit-labs/context": "0.4.0", | ||||
|     "@lit-labs/context": "0.4.1", | ||||
|     "@lit-labs/motion": "1.0.4", | ||||
|     "@lit-labs/virtualizer": "2.0.6", | ||||
|     "@lrnwebcomponents/simple-tooltip": "7.0.16", | ||||
|     "@lit-labs/virtualizer": "2.0.7", | ||||
|     "@lrnwebcomponents/simple-tooltip": "7.0.18", | ||||
|     "@material/chips": "=14.0.0-canary.53b3cad2f.0", | ||||
|     "@material/data-table": "=14.0.0-canary.53b3cad2f.0", | ||||
|     "@material/mwc-button": "0.27.0", | ||||
| @@ -79,7 +79,7 @@ | ||||
|     "@material/mwc-top-app-bar": "0.27.0", | ||||
|     "@material/mwc-top-app-bar-fixed": "0.27.0", | ||||
|     "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0", | ||||
|     "@material/web": "=1.0.0-pre.16", | ||||
|     "@material/web": "=1.0.0-pre.17", | ||||
|     "@mdi/js": "7.2.96", | ||||
|     "@mdi/svg": "7.2.96", | ||||
|     "@polymer/iron-flex-layout": "3.0.1", | ||||
| @@ -93,8 +93,8 @@ | ||||
|     "@polymer/paper-toast": "3.0.1", | ||||
|     "@polymer/polymer": "3.5.1", | ||||
|     "@thomasloven/round-slider": "0.6.0", | ||||
|     "@vaadin/combo-box": "24.1.6", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.1.6", | ||||
|     "@vaadin/combo-box": "24.1.7", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.1.7", | ||||
|     "@vibrant/color": "3.2.1-alpha.1", | ||||
|     "@vibrant/core": "3.2.1-alpha.1", | ||||
|     "@vibrant/quantizer-mmcq": "3.2.1-alpha.1", | ||||
| @@ -104,7 +104,7 @@ | ||||
|     "app-datepicker": "5.1.1", | ||||
|     "chart.js": "4.3.3", | ||||
|     "comlink": "4.4.1", | ||||
|     "core-js": "3.32.1", | ||||
|     "core-js": "3.32.2", | ||||
|     "cropperjs": "1.6.0", | ||||
|     "date-fns": "2.30.0", | ||||
|     "date-fns-tz": "2.0.0", | ||||
| @@ -112,15 +112,15 @@ | ||||
|     "deep-freeze": "0.0.1", | ||||
|     "fuse.js": "6.6.2", | ||||
|     "google-timezones-json": "1.2.0", | ||||
|     "hls.js": "1.4.10", | ||||
|     "hls.js": "1.4.12", | ||||
|     "home-assistant-js-websocket": "8.2.0", | ||||
|     "idb-keyval": "6.2.1", | ||||
|     "intl-messageformat": "10.5.0", | ||||
|     "intl-messageformat": "10.5.2", | ||||
|     "js-yaml": "4.1.0", | ||||
|     "leaflet": "1.9.4", | ||||
|     "leaflet-draw": "1.0.4", | ||||
|     "lit": "2.8.0", | ||||
|     "luxon": "3.4.2", | ||||
|     "luxon": "3.4.3", | ||||
|     "marked": "7.0.5", | ||||
|     "memoize-one": "6.0.0", | ||||
|     "node-vibrant": "3.2.1-alpha.1", | ||||
| @@ -137,9 +137,9 @@ | ||||
|     "tinykeys": "2.1.0", | ||||
|     "tsparticles-engine": "2.12.0", | ||||
|     "tsparticles-preset-links": "2.12.0", | ||||
|     "ua-parser-js": "1.0.35", | ||||
|     "ua-parser-js": "1.0.36", | ||||
|     "unfetch": "5.0.0", | ||||
|     "vis-data": "7.1.6", | ||||
|     "vis-data": "7.1.7", | ||||
|     "vis-network": "9.1.6", | ||||
|     "vue": "2.7.14", | ||||
|     "vue2-daterange-picker": "0.6.8", | ||||
| @@ -153,12 +153,13 @@ | ||||
|     "xss": "1.0.14" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "7.22.11", | ||||
|     "@babel/plugin-proposal-decorators": "7.22.10", | ||||
|     "@babel/plugin-transform-runtime": "7.22.10", | ||||
|     "@babel/preset-env": "7.22.10", | ||||
|     "@babel/preset-typescript": "7.22.11", | ||||
|     "@babel/core": "7.22.20", | ||||
|     "@babel/plugin-proposal-decorators": "7.22.15", | ||||
|     "@babel/plugin-transform-runtime": "7.22.15", | ||||
|     "@babel/preset-env": "7.22.20", | ||||
|     "@babel/preset-typescript": "7.22.15", | ||||
|     "@koa/cors": "4.0.0", | ||||
|     "@lokalise/node-api": "11.0.1", | ||||
|     "@octokit/auth-oauth-device": "6.0.0", | ||||
|     "@octokit/plugin-retry": "6.0.0", | ||||
|     "@octokit/rest": "20.0.1", | ||||
| @@ -168,32 +169,32 @@ | ||||
|     "@rollup/plugin-json": "6.0.0", | ||||
|     "@rollup/plugin-node-resolve": "15.2.1", | ||||
|     "@rollup/plugin-replace": "5.0.2", | ||||
|     "@types/babel__plugin-transform-runtime": "7.9.2", | ||||
|     "@types/chromecast-caf-receiver": "6.0.9", | ||||
|     "@types/chromecast-caf-sender": "1.0.5", | ||||
|     "@types/babel__plugin-transform-runtime": "7.9.3", | ||||
|     "@types/chromecast-caf-receiver": "6.0.10", | ||||
|     "@types/chromecast-caf-sender": "1.0.6", | ||||
|     "@types/esprima": "4.0.3", | ||||
|     "@types/glob": "8.1.0", | ||||
|     "@types/html-minifier-terser": "7.0.0", | ||||
|     "@types/js-yaml": "4.0.5", | ||||
|     "@types/leaflet": "1.9.3", | ||||
|     "@types/leaflet-draw": "1.0.7", | ||||
|     "@types/luxon": "3.3.1", | ||||
|     "@types/js-yaml": "4.0.6", | ||||
|     "@types/leaflet": "1.9.4", | ||||
|     "@types/leaflet-draw": "1.0.8", | ||||
|     "@types/luxon": "3.3.2", | ||||
|     "@types/mocha": "10.0.1", | ||||
|     "@types/qrcode": "1.5.1", | ||||
|     "@types/qrcode": "1.5.2", | ||||
|     "@types/serve-handler": "6.1.1", | ||||
|     "@types/sortablejs": "1.15.1", | ||||
|     "@types/tar": "6.1.5", | ||||
|     "@types/ua-parser-js": "0.7.36", | ||||
|     "@types/sortablejs": "1.15.2", | ||||
|     "@types/tar": "6.1.6", | ||||
|     "@types/ua-parser-js": "0.7.37", | ||||
|     "@types/webspeechapi": "0.0.29", | ||||
|     "@typescript-eslint/eslint-plugin": "6.4.1", | ||||
|     "@typescript-eslint/parser": "6.4.1", | ||||
|     "@typescript-eslint/eslint-plugin": "6.7.0", | ||||
|     "@typescript-eslint/parser": "6.7.0", | ||||
|     "@web/dev-server": "0.1.38", | ||||
|     "@web/dev-server-rollup": "0.4.1", | ||||
|     "babel-loader": "9.1.3", | ||||
|     "babel-plugin-template-html-minifier": "4.1.0", | ||||
|     "chai": "4.3.8", | ||||
|     "del": "7.0.0", | ||||
|     "eslint": "8.48.0", | ||||
|     "del": "7.1.0", | ||||
|     "eslint": "8.49.0", | ||||
|     "eslint-config-airbnb-base": "15.0.0", | ||||
|     "eslint-config-airbnb-typescript": "17.1.0", | ||||
|     "eslint-config-prettier": "9.0.0", | ||||
| @@ -207,7 +208,7 @@ | ||||
|     "esprima": "4.0.1", | ||||
|     "fancy-log": "2.0.0", | ||||
|     "fs-extra": "11.1.1", | ||||
|     "glob": "10.3.3", | ||||
|     "glob": "10.3.4", | ||||
|     "gulp": "4.0.2", | ||||
|     "gulp-flatmap": "1.0.2", | ||||
|     "gulp-json-transform": "0.4.8", | ||||
| @@ -227,16 +228,16 @@ | ||||
|     "object-hash": "3.0.0", | ||||
|     "open": "9.1.0", | ||||
|     "pinst": "3.0.0", | ||||
|     "prettier": "3.0.2", | ||||
|     "prettier": "3.0.3", | ||||
|     "rollup": "2.79.1", | ||||
|     "rollup-plugin-string": "3.0.0", | ||||
|     "rollup-plugin-terser": "7.0.2", | ||||
|     "rollup-plugin-visualizer": "5.9.2", | ||||
|     "serve-handler": "6.1.5", | ||||
|     "sinon": "15.2.0", | ||||
|     "sinon": "16.0.0", | ||||
|     "source-map-url": "0.4.1", | ||||
|     "systemjs": "6.14.2", | ||||
|     "tar": "6.1.15", | ||||
|     "tar": "6.2.0", | ||||
|     "terser-webpack-plugin": "5.3.9", | ||||
|     "ts-lit-plugin": "2.0.0-pre.1", | ||||
|     "typescript": "5.2.2", | ||||
|   | ||||
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name         = "home-assistant-frontend" | ||||
| version      = "20230831.0" | ||||
| version      = "20230911.0" | ||||
| license      = {text = "Apache-2.0"} | ||||
| description  = "The Home Assistant frontend" | ||||
| readme       = "README.md" | ||||
|   | ||||
| @@ -8,40 +8,4 @@ set -eu -o pipefail | ||||
|  | ||||
| cd "$(dirname "$0")/.." | ||||
|  | ||||
| if [ -z "${LOKALISE_TOKEN-}" ] && [ ! -f .lokalise_token ] ; then | ||||
|     echo "Lokalise API token is required to download the latest set of" \ | ||||
|         "translations. Please create an account by using the following link:" \ | ||||
|         "https://lokalise.co/signup/3420425759f6d6d241f598.13594006/all/" \ | ||||
|         "Place your token in a new file \".lokalise_token\" in the repo" \ | ||||
|         "root directory." | ||||
|     exit 1 | ||||
| fi | ||||
|  | ||||
| # Load token from file if not already in the environment | ||||
| [ -z "${LOKALISE_TOKEN-}" ] && LOKALISE_TOKEN="$(<.lokalise_token)" | ||||
|  | ||||
| declare -A PROJECT_ID=( \ | ||||
|     [frontend]="3420425759f6d6d241f598.13594006" \ | ||||
|     [backend]="130246255a974bd3b5e8a1.51616605" \ | ||||
|     ) | ||||
|  | ||||
| for project in ${!PROJECT_ID[*]}; do | ||||
| LOCAL_DIR=`pwd`/translations/${project} | ||||
| rm -f ${LOCAL_DIR}/* || mkdir -p ${LOCAL_DIR} | ||||
|     docker run \ | ||||
|         -v ${LOCAL_DIR}:/opt/dest/locale \ | ||||
|         --rm \ | ||||
|         lokalise/lokalise-cli-2@sha256:f1860b26be22fa73b8c93bc5f8690f2afc867610a42de6fc27adc790e5d4425d \ | ||||
|         lokalise2 \ | ||||
|             --token ${LOKALISE_TOKEN} \ | ||||
|             --project-id ${PROJECT_ID[${project}]} \ | ||||
|             file download \ | ||||
|             --export-empty-as skip \ | ||||
|             --format json \ | ||||
|             --json-unescaped-slashes=true \ | ||||
|             --replace-breaks=false \ | ||||
|             --original-filenames=false \ | ||||
|             --unzip-to /opt/dest | ||||
| done | ||||
|  | ||||
| ./node_modules/.bin/gulp check-downloaded-translations | ||||
| ./node_modules/.bin/gulp download-translations | ||||
| @@ -35,20 +35,47 @@ export class HaPasswordManagerPolyfill extends LitElement { | ||||
|     super.connectedCallback(); | ||||
|     this._styleElement = document.createElement("style"); | ||||
|     this._styleElement.textContent = css` | ||||
|       /* Polyfill form is sized and vertically aligned with true form, then positioned offscreen | ||||
|       rather than hiding so it does not create a new stacking context */ | ||||
|       .password-manager-polyfill { | ||||
|         position: absolute; | ||||
|         opacity: 0; | ||||
|         z-index: -1; | ||||
|         box-sizing: border-box; | ||||
|       } | ||||
|       .password-manager-polyfill input { | ||||
|       /* Excluding our wrapper, move any children back on screen, including anything injected that might not already be positioned */ | ||||
|       .password-manager-polyfill > *:not(.wrapper), | ||||
|       .password-manager-polyfill > .wrapper > * { | ||||
|         position: relative; | ||||
|         left: 10000px; | ||||
|       } | ||||
|       /* Size and hide our polyfill fields */ | ||||
|       .password-manager-polyfill .underneath { | ||||
|         display: block; | ||||
|         box-sizing: border-box; | ||||
|         width: 100%; | ||||
|         height: 62px; | ||||
|         padding: 0; | ||||
|         padding: 0 16px; | ||||
|         border: 0; | ||||
|         z-index: -1; | ||||
|         height: 21px; | ||||
|         /* Transparency is only needed to hide during paint or in case of misalignment, | ||||
|         but LastPass will fail if it's 0, so we use 1% */ | ||||
|         opacity: 0.01; | ||||
|       } | ||||
|       .password-manager-polyfill input[type="submit"] { | ||||
|         width: 0; | ||||
|         height: 0; | ||||
|       .password-manager-polyfill input.underneath { | ||||
|         height: 28px; | ||||
|         margin-bottom: 30.5px; | ||||
|       } | ||||
|       /* Button position is not important, but size should not be zero */ | ||||
|       .password-manager-polyfill > input.underneath[type="submit"] { | ||||
|         width: 1px; | ||||
|         height: 1px; | ||||
|         margin: 0 auto; | ||||
|         overflow: hidden; | ||||
|       } | ||||
|       /* Ensure injected elements will be on top */ | ||||
|       .password-manager-polyfill > *:not(.underneath, .wrapper), | ||||
|       .password-manager-polyfill > .wrapper > *:not(.underneath) { | ||||
|         isolation: isolate; | ||||
|         z-index: auto; | ||||
|       } | ||||
|     `.toString(); | ||||
|     document.head.append(this._styleElement); | ||||
| @@ -77,16 +104,25 @@ export class HaPasswordManagerPolyfill extends LitElement { | ||||
|           class="password-manager-polyfill" | ||||
|           style=${styleMap({ | ||||
|             top: `${this.boundingRect?.y || 148}px`, | ||||
|             left: `calc(50% - ${(this.boundingRect?.width || 360) / 2}px)`, | ||||
|             left: `calc(50% - ${ | ||||
|               (this.boundingRect?.width || 360) / 2 | ||||
|             }px - 10000px)`, | ||||
|             width: `${this.boundingRect?.width || 360}px`, | ||||
|           })} | ||||
|           aria-hidden="true" | ||||
|           action="/auth" | ||||
|           method="post" | ||||
|           @submit=${this._handleSubmit} | ||||
|         > | ||||
|           ${autocompleteLoginFields(this.step.data_schema).map((input) => | ||||
|             this.render_input(input) | ||||
|           )} | ||||
|           <input type="submit" /> | ||||
|           <input | ||||
|             type="submit" | ||||
|             value="Login" | ||||
|             class="underneath" | ||||
|             tabindex="-2" | ||||
|             aria-hidden="true" | ||||
|           /> | ||||
|         </form> | ||||
|       `; | ||||
|     } | ||||
| @@ -99,26 +135,35 @@ export class HaPasswordManagerPolyfill extends LitElement { | ||||
|       return ""; | ||||
|     } | ||||
|     return html` | ||||
|       <input | ||||
|         tabindex="-1" | ||||
|         .id=${schema.name} | ||||
|         .name=${schema.name} | ||||
|         .type=${inputType} | ||||
|         .value=${this.stepData[schema.name] || ""} | ||||
|         .autocomplete=${schema.autocomplete} | ||||
|         @input=${this._valueChanged} | ||||
|         @change=${this._valueChanged} | ||||
|       /> | ||||
|       <!-- Label is a sibling so it can be stacked underneath without affecting injections adjacent to input (e.g. LastPass) --> | ||||
|       <label for=${schema.name} class="underneath" aria-hidden="true"> | ||||
|         ${schema.name} | ||||
|       </label> | ||||
|       <!-- LastPass fails if the input is hidden directly, so we trick it and hide a wrapper instead --> | ||||
|       <div class="wrapper" aria-hidden="true"> | ||||
|         <!-- LastPass fails with tabindex of -1, so we trick with -2 --> | ||||
|         <input | ||||
|           class="underneath" | ||||
|           tabindex="-2" | ||||
|           .id=${schema.name} | ||||
|           .name=${schema.name} | ||||
|           .type=${inputType} | ||||
|           .value=${this.stepData[schema.name] || ""} | ||||
|           .autocomplete=${schema.autocomplete} | ||||
|           @input=${this._valueChanged} | ||||
|           @change=${this._valueChanged} | ||||
|         /> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _handleSubmit(ev: Event) { | ||||
|   private _handleSubmit(ev: SubmitEvent) { | ||||
|     ev.preventDefault(); | ||||
|     fireEvent(this, "form-submitted"); | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: Event) { | ||||
|     const target = ev.target! as HTMLInputElement; | ||||
|     const target = ev.target as HTMLInputElement; | ||||
|     this.stepData = { ...this.stepData, [target.id]: target.value }; | ||||
|     fireEvent(this, "value-changed", { | ||||
|       value: this.stepData, | ||||
|   | ||||
| @@ -108,7 +108,7 @@ export const formatNumber = ( | ||||
|  * @returns An `Intl.NumberFormatOptions` object with `maximumFractionDigits` set to 0, or `undefined` | ||||
|  */ | ||||
| export const getNumberFormatOptions = ( | ||||
|   entityState: HassEntity, | ||||
|   entityState?: HassEntity, | ||||
|   entity?: EntityRegistryDisplayEntry | ||||
| ): Intl.NumberFormatOptions | undefined => { | ||||
|   const precision = entity?.display_precision; | ||||
| @@ -119,8 +119,8 @@ export const getNumberFormatOptions = ( | ||||
|     }; | ||||
|   } | ||||
|   if ( | ||||
|     Number.isInteger(Number(entityState.attributes?.step)) && | ||||
|     Number.isInteger(Number(entityState.state)) | ||||
|     Number.isInteger(Number(entityState?.attributes?.step)) && | ||||
|     Number.isInteger(Number(entityState?.state)) | ||||
|   ) { | ||||
|     return { maximumFractionDigits: 0 }; | ||||
|   } | ||||
|   | ||||
| @@ -22,14 +22,7 @@ export type LocalizeKeys = | ||||
|   | `ui.dialogs.unhealthy.reason.${string}` | ||||
|   | `ui.dialogs.unsupported.reason.${string}` | ||||
|   | `ui.panel.config.${string}.${"caption" | "description"}` | ||||
|   | `ui.panel.config.automation.${string}` | ||||
|   | `ui.panel.config.dashboard.${string}` | ||||
|   | `ui.panel.config.devices.${string}` | ||||
|   | `ui.panel.config.energy.${string}` | ||||
|   | `ui.panel.config.info.${string}` | ||||
|   | `ui.panel.config.lovelace.${string}` | ||||
|   | `ui.panel.config.network.${string}` | ||||
|   | `ui.panel.config.scene.${string}` | ||||
|   | `ui.panel.config.zha.${string}` | ||||
|   | `ui.panel.config.zwave_js.${string}` | ||||
|   | `ui.panel.lovelace.card.${string}` | ||||
|   | ||||
| @@ -349,9 +349,6 @@ export class HaChartBase extends LitElement { | ||||
|         height: 0; | ||||
|         transition: height 300ms cubic-bezier(0.4, 0, 0.2, 1); | ||||
|       } | ||||
|       .chartContainer { | ||||
|         position: relative; | ||||
|       } | ||||
|       canvas { | ||||
|         max-height: var(--chart-max-height, 400px); | ||||
|       } | ||||
|   | ||||
| @@ -74,7 +74,7 @@ export interface DataTableColumnData<T = any> extends DataTableSortColumnData { | ||||
|   title: TemplateResult | string; | ||||
|   label?: TemplateResult | string; | ||||
|   type?: "numeric" | "icon" | "icon-button" | "overflow-menu" | "flex"; | ||||
|   template?: (data: any, row: T) => TemplateResult | string | typeof nothing; | ||||
|   template?: (row: T) => TemplateResult | string | typeof nothing; | ||||
|   width?: string; | ||||
|   maxWidth?: string; | ||||
|   grows?: boolean; | ||||
| @@ -431,7 +431,7 @@ export class HaDataTable extends LitElement { | ||||
|                   }) | ||||
|                 : ""} | ||||
|             > | ||||
|               ${column.template ? column.template(row[key], row) : row[key]} | ||||
|               ${column.template ? column.template(row) : row[key]} | ||||
|             </div> | ||||
|           `; | ||||
|         })} | ||||
| @@ -458,7 +458,8 @@ export class HaDataTable extends LitElement { | ||||
|           filteredData, | ||||
|           this._sortColumns[this._sortColumn], | ||||
|           this._sortDirection, | ||||
|           this._sortColumn | ||||
|           this._sortColumn, | ||||
|           this.hass.locale.language | ||||
|         ) | ||||
|       : filteredData; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| // To use comlink under ES5 | ||||
| import "proxy-polyfill"; | ||||
| import { expose } from "comlink"; | ||||
| import "proxy-polyfill"; | ||||
| import { stringCompare } from "../../common/string/compare"; | ||||
| import type { | ||||
|   ClonedDataTableColumnData, | ||||
|   DataTableRowData, | ||||
| @@ -39,7 +40,8 @@ const sortData = ( | ||||
|   data: DataTableRowData[], | ||||
|   column: ClonedDataTableColumnData, | ||||
|   direction: SortingDirection, | ||||
|   sortColumn: string | ||||
|   sortColumn: string, | ||||
|   language?: string | ||||
| ) => | ||||
|   data.sort((a, b) => { | ||||
|     let sort = 1; | ||||
| @@ -58,13 +60,8 @@ const sortData = ( | ||||
|     if (column.type === "numeric") { | ||||
|       valA = isNaN(valA) ? undefined : Number(valA); | ||||
|       valB = isNaN(valB) ? undefined : Number(valB); | ||||
|     } else { | ||||
|       if (typeof valA === "string") { | ||||
|         valA = valA.toUpperCase(); | ||||
|       } | ||||
|       if (typeof valB === "string") { | ||||
|         valB = valB.toUpperCase(); | ||||
|       } | ||||
|     } else if (typeof valA === "string" && typeof valB === "string") { | ||||
|       return sort * stringCompare(valA, valB, language); | ||||
|     } | ||||
|  | ||||
|     // Ensure "undefined" and "null" are always sorted to the bottom | ||||
|   | ||||
| @@ -27,10 +27,12 @@ export const filterData = ( | ||||
|   filter: FilterDataParamTypes[2] | ||||
| ): Promise<ReturnType<FilterDataType>> => | ||||
|   getWorker().filterData(data, columns, filter); | ||||
|  | ||||
| export const sortData = ( | ||||
|   data: SortDataParamTypes[0], | ||||
|   columns: SortDataParamTypes[1], | ||||
|   direction: SortDataParamTypes[2], | ||||
|   sortColumn: SortDataParamTypes[3] | ||||
|   sortColumn: SortDataParamTypes[3], | ||||
|   language?: SortDataParamTypes[4] | ||||
| ): Promise<ReturnType<SortDataType>> => | ||||
|   getWorker().sortData(data, columns, direction, sortColumn); | ||||
|   getWorker().sortData(data, columns, direction, sortColumn, language); | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { html, LitElement, PropertyValues, nothing } from "lit"; | ||||
| import { LitElement, PropertyValues, html, nothing } from "lit"; | ||||
| import { customElement, property, query } from "lit/decorators"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { computeStateDisplay } from "../../common/entity/compute_state_display"; | ||||
| import { getStates } from "../../common/entity/get_states"; | ||||
| import { computeAttributeValueDisplay } from "../../common/entity/compute_attribute_display"; | ||||
| import { ValueChangedEvent, HomeAssistant } from "../../types"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||
| import "../ha-combo-box"; | ||||
| import type { HaComboBox } from "../ha-combo-box"; | ||||
|  | ||||
| @@ -58,20 +56,9 @@ class HaEntityStatePicker extends LitElement { | ||||
|           ? getStates(state, this.attribute).map((key) => ({ | ||||
|               value: key, | ||||
|               label: !this.attribute | ||||
|                 ? computeStateDisplay( | ||||
|                     this.hass.localize, | ||||
|                 ? this.hass.formatEntityState(state, key) | ||||
|                 : this.hass.formatEntityAttributeValue( | ||||
|                     state, | ||||
|                     this.hass.locale, | ||||
|                     this.hass.config, | ||||
|                     this.hass.entities, | ||||
|                     key | ||||
|                   ) | ||||
|                 : computeAttributeValueDisplay( | ||||
|                     this.hass.localize, | ||||
|                     state, | ||||
|                     this.hass.locale, | ||||
|                     this.hass.config, | ||||
|                     this.hass.entities, | ||||
|                     this.attribute, | ||||
|                     key | ||||
|                   ), | ||||
|   | ||||
| @@ -12,7 +12,6 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { arrayLiteralIncludes } from "../../common/array/literal-includes"; | ||||
| import secondsToDuration from "../../common/datetime/seconds_to_duration"; | ||||
| import { computeStateDisplay } from "../../common/entity/compute_state_display"; | ||||
| import { computeStateDomain } from "../../common/entity/compute_state_domain"; | ||||
| import { computeStateName } from "../../common/entity/compute_state_name"; | ||||
| import { FIXED_DOMAIN_STATES } from "../../common/entity/get_states"; | ||||
| @@ -192,13 +191,7 @@ export class HaStateLabelBadge extends LitElement { | ||||
|               this.hass!.locale, | ||||
|               getNumberFormatOptions(entityState, entry) | ||||
|             ) | ||||
|           : computeStateDisplay( | ||||
|               this.hass!.localize, | ||||
|               entityState, | ||||
|               this.hass!.locale, | ||||
|               this.hass!.config, | ||||
|               this.hass!.entities | ||||
|             ); | ||||
|           : this.hass!.formatEntityState(entityState); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -1,9 +1,14 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   TemplateResult, | ||||
| } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display"; | ||||
| import { computeStateDisplay } from "../common/entity/compute_state_display"; | ||||
| import { CLIMATE_PRESET_NONE, ClimateEntity } from "../data/climate"; | ||||
| import { isUnavailableState } from "../data/entity"; | ||||
| import { isUnavailableState, OFF } from "../data/entity"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
|  | ||||
| @customElement("ha-climate-state") | ||||
| @@ -22,26 +27,24 @@ class HaClimateState extends LitElement { | ||||
|                 ${this.stateObj.attributes.preset_mode && | ||||
|                 this.stateObj.attributes.preset_mode !== CLIMATE_PRESET_NONE | ||||
|                   ? html`- | ||||
|                     ${computeAttributeValueDisplay( | ||||
|                       this.hass.localize, | ||||
|                     ${this.hass.formatEntityAttributeValue( | ||||
|                       this.stateObj, | ||||
|                       this.hass.locale, | ||||
|                       this.hass.config, | ||||
|                       this.hass.entities, | ||||
|                       "preset_mode" | ||||
|                     )}` | ||||
|                   : ""} | ||||
|                   : nothing} | ||||
|               </span> | ||||
|               <div class="unit">${this._computeTarget()}</div>` | ||||
|           : this._localizeState()} | ||||
|       </div> | ||||
|  | ||||
|       ${currentStatus && !isUnavailableState(this.stateObj.state) | ||||
|         ? html`<div class="current"> | ||||
|             ${this.hass.localize("ui.card.climate.currently")}: | ||||
|             <div class="unit">${currentStatus}</div> | ||||
|           </div>` | ||||
|         : ""}`; | ||||
|         ? html` | ||||
|             <div class="current"> | ||||
|               ${this.hass.localize("ui.card.climate.currently")}: | ||||
|               <div class="unit">${currentStatus}</div> | ||||
|             </div> | ||||
|           ` | ||||
|         : nothing}`; | ||||
|   } | ||||
|  | ||||
|   private _computeCurrentStatus(): string | undefined { | ||||
| @@ -125,24 +128,17 @@ class HaClimateState extends LitElement { | ||||
|       return this.hass.localize(`state.default.${this.stateObj.state}`); | ||||
|     } | ||||
|  | ||||
|     const stateString = computeStateDisplay( | ||||
|       this.hass.localize, | ||||
|       this.stateObj, | ||||
|       this.hass.locale, | ||||
|       this.hass.config, | ||||
|       this.hass.entities | ||||
|     ); | ||||
|     const stateString = this.hass.formatEntityState(this.stateObj); | ||||
|  | ||||
|     return this.stateObj.attributes.hvac_action | ||||
|       ? `${computeAttributeValueDisplay( | ||||
|           this.hass.localize, | ||||
|           this.stateObj, | ||||
|           this.hass.locale, | ||||
|           this.hass.config, | ||||
|           this.hass.entities, | ||||
|           "hvac_action" | ||||
|         )} (${stateString})` | ||||
|       : stateString; | ||||
|     if (this.stateObj.attributes.hvac_action && this.stateObj.state !== OFF) { | ||||
|       const actionString = this.hass.formatEntityAttributeValue( | ||||
|         this.stateObj, | ||||
|         "hvac_action" | ||||
|       ); | ||||
|       return `${actionString} (${stateString})`; | ||||
|     } | ||||
|  | ||||
|     return stateString; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|   | ||||
| @@ -244,7 +244,6 @@ export class HaComboBox extends LitElement { | ||||
|       ); | ||||
|  | ||||
|       if (overlay) { | ||||
|         overlay.setAttribute("required-vertical-space", "0"); | ||||
|         this._removeInert(overlay); | ||||
|       } | ||||
|       this._observeBody(); | ||||
| @@ -331,7 +330,7 @@ export class HaComboBox extends LitElement { | ||||
|       } | ||||
|       vaadin-combo-box-light { | ||||
|         position: relative; | ||||
|         --vaadin-combo-box-overlay-max-height: calc(45vh); | ||||
|         --vaadin-combo-box-overlay-max-height: calc(45vh - 56px); | ||||
|       } | ||||
|       ha-textfield { | ||||
|         width: 100%; | ||||
|   | ||||
| @@ -81,6 +81,7 @@ export class HaControlNumberButton extends LitElement { | ||||
|   } | ||||
|  | ||||
|   _handleKeyDown(e: KeyboardEvent) { | ||||
|     if (this.disabled) return; | ||||
|     if (!A11Y_KEY_CODES.has(e.code)) return; | ||||
|     e.preventDefault(); | ||||
|     switch (e.code) { | ||||
| @@ -116,7 +117,7 @@ export class HaControlNumberButton extends LitElement { | ||||
|     const displayedValue = | ||||
|       this.value != null | ||||
|         ? formatNumber(this.value, this.locale, this.formatOptions) | ||||
|         : "-"; | ||||
|         : ""; | ||||
|  | ||||
|     return html` | ||||
|       <div class="container"> | ||||
| @@ -124,12 +125,12 @@ export class HaControlNumberButton extends LitElement { | ||||
|           id="input" | ||||
|           class="value" | ||||
|           role="number-button" | ||||
|           tabindex="0" | ||||
|           .tabIndex=${this.disabled ? "-1" : "0"} | ||||
|           aria-valuenow=${this.value} | ||||
|           aria-valuemin=${this.min} | ||||
|           aria-valuemax=${this.max} | ||||
|           aria-label=${ifDefined(this.label)} | ||||
|           .disabled=${this.disabled} | ||||
|           ?disabled=${this.disabled} | ||||
|           @keydown=${this._handleKeyDown} | ||||
|         > | ||||
|           ${displayedValue} | ||||
| @@ -240,6 +241,7 @@ export class HaControlNumberButton extends LitElement { | ||||
|       .button[disabled] { | ||||
|         opacity: 0.4; | ||||
|         pointer-events: none; | ||||
|         cursor: not-allowed; | ||||
|       } | ||||
|       .button.minus { | ||||
|         left: 0; | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| import { Ripple } from "@material/mwc-ripple"; | ||||
| import { RippleHandlers } from "@material/mwc-ripple/ripple-handlers"; | ||||
| import { SelectBase } from "@material/mwc-select/mwc-select-base"; | ||||
| import { mdiMenuDown } from "@mdi/js"; | ||||
| import { css, html, nothing } from "lit"; | ||||
| import { | ||||
|   customElement, | ||||
|   eventOptions, | ||||
|   property, | ||||
|   query, | ||||
|   queryAsync, | ||||
|   state, | ||||
| @@ -24,6 +26,12 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|  | ||||
|   @query(".select-anchor") protected anchorElement!: HTMLDivElement | null; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "show-arrow" }) | ||||
|   public showArrow?: boolean; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "hide-label" }) | ||||
|   public hideLabel?: boolean; | ||||
|  | ||||
|   @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>; | ||||
|  | ||||
|   @state() private _shouldRenderRipple = false; | ||||
| @@ -36,7 +44,9 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|       "select-no-value": !this.selectedText, | ||||
|     }; | ||||
|  | ||||
|     const labelledby = this.label ? "label" : undefined; | ||||
|     const labelledby = this.label && !this.hideLabel ? "label" : undefined; | ||||
|     const labelAttribute = | ||||
|       this.label && this.hideLabel ? this.label : undefined; | ||||
|  | ||||
|     return html` | ||||
|       <div class="select ${classMap(classes)}"> | ||||
| @@ -57,6 +67,7 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|           aria-invalid=${!this.isUiValid} | ||||
|           aria-haspopup="listbox" | ||||
|           aria-labelledby=${ifDefined(labelledby)} | ||||
|           aria-label=${ifDefined(labelAttribute)} | ||||
|           aria-required=${this.required} | ||||
|           @click=${this.onClick} | ||||
|           @focus=${this.onFocus} | ||||
| @@ -72,11 +83,14 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         > | ||||
|           ${this.renderIcon()} | ||||
|           <div class="content"> | ||||
|             <p id="label" class="label">${this.label}</p> | ||||
|             ${this.hideLabel | ||||
|               ? nothing | ||||
|               : html`<p id="label" class="label">${this.label}</p>`} | ||||
|             ${this.selectedText | ||||
|               ? html`<p class="value">${this.selectedText}</p>` | ||||
|               : nothing} | ||||
|           </div> | ||||
|           ${this.renderArrow()} | ||||
|           ${this._shouldRenderRipple && !this.disabled | ||||
|             ? html` <mwc-ripple></mwc-ripple> ` | ||||
|             : nothing} | ||||
| @@ -86,13 +100,29 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private renderArrow() { | ||||
|     if (!this.showArrow) return nothing; | ||||
|  | ||||
|     return html` | ||||
|       <div class="icon"> | ||||
|         <ha-svg-icon .path=${mdiMenuDown}></ha-svg-icon> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private renderIcon() { | ||||
|     const index = this.mdcFoundation?.getSelectedIndex(); | ||||
|     const items = this.menuElement?.items ?? []; | ||||
|     const item = index != null ? items[index] : undefined; | ||||
|     const icon = | ||||
|       item?.querySelector("[slot='graphic']") ?? | ||||
|       (null as HaSvgIcon | HaIcon | null); | ||||
|     const defaultIcon = this.querySelector("[slot='icon']"); | ||||
|     const icon = (item?.querySelector("[slot='graphic']") ?? null) as | ||||
|       | HaSvgIcon | ||||
|       | HaIcon | ||||
|       | null; | ||||
|  | ||||
|     if (!defaultIcon && !icon) { | ||||
|       return null; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <div class="icon"> | ||||
| @@ -171,14 +201,18 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         --control-select-menu-background-color: var(--disabled-color); | ||||
|         --control-select-menu-background-opacity: 0.2; | ||||
|         --control-select-menu-border-radius: 14px; | ||||
|         --control-select-menu-height: 48px; | ||||
|         --control-select-menu-padding: 6px 10px; | ||||
|         --mdc-icon-size: 20px; | ||||
|         font-size: 14px; | ||||
|         line-height: 1.4; | ||||
|         width: auto; | ||||
|         color: var(--primary-text-color); | ||||
|         -webkit-tap-highlight-color: transparent; | ||||
|       } | ||||
|       .select-anchor { | ||||
|         height: 48px; | ||||
|         padding: 6px 10px; | ||||
|         height: var(--control-select-menu-height); | ||||
|         padding: var(--control-select-menu-padding); | ||||
|         overflow: hidden; | ||||
|         position: relative; | ||||
|         cursor: pointer; | ||||
| @@ -193,15 +227,12 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|         --mdc-ripple-color: var(--control-select-menu-background-color); | ||||
|         /* For safari border-radius overflow */ | ||||
|         z-index: 0; | ||||
|         font-size: inherit; | ||||
|         transition: color 180ms ease-in-out; | ||||
|         gap: 10px; | ||||
|         width: 100%; | ||||
|         user-select: none; | ||||
|         font-size: 14px; | ||||
|         font-style: normal; | ||||
|         font-weight: 400; | ||||
|         line-height: 20px; | ||||
|         letter-spacing: 0.25px; | ||||
|       } | ||||
|       .content { | ||||
| @@ -223,8 +254,7 @@ export class HaControlSelectMenu extends SelectBase { | ||||
|       } | ||||
|  | ||||
|       .label { | ||||
|         font-size: 12px; | ||||
|         line-height: 16px; | ||||
|         font-size: 0.85em; | ||||
|         letter-spacing: 0.4px; | ||||
|       } | ||||
|  | ||||
|   | ||||
| @@ -217,6 +217,7 @@ export class HaControlSelect extends LitElement { | ||||
|         transition: box-shadow 180ms ease-in-out; | ||||
|         font-style: normal; | ||||
|         font-weight: 500; | ||||
|         color: var(--primary-text-color); | ||||
|         user-select: none; | ||||
|         -webkit-tap-highlight-color: transparent; | ||||
|       } | ||||
| @@ -267,7 +268,6 @@ export class HaControlSelect extends LitElement { | ||||
|         justify-content: center; | ||||
|         border-radius: var(--control-select-button-border-radius); | ||||
|         overflow: hidden; | ||||
|         color: var(--primary-text-color); | ||||
|         /* For safari border-radius overflow */ | ||||
|         z-index: 0; | ||||
|       } | ||||
| @@ -331,6 +331,7 @@ export class HaControlSelect extends LitElement { | ||||
|       :host([disabled]) { | ||||
|         --control-select-color: var(--disabled-color); | ||||
|         --control-select-focused-opacity: 0; | ||||
|         color: var(--disabled-color); | ||||
|       } | ||||
|       :host([disabled]) .option { | ||||
|         cursor: not-allowed; | ||||
|   | ||||
| @@ -155,11 +155,12 @@ export class HaConversationAgentPicker extends LitElement { | ||||
|     if (!this._configEntry) { | ||||
|       return; | ||||
|     } | ||||
|     showOptionsFlowDialog( | ||||
|       this, | ||||
|       this._configEntry, | ||||
|       await fetchIntegrationManifest(this.hass, this._configEntry.domain) | ||||
|     ); | ||||
|     showOptionsFlowDialog(this, this._configEntry, { | ||||
|       manifest: await fetchIntegrationManifest( | ||||
|         this.hass, | ||||
|         this._configEntry.domain | ||||
|       ), | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|   | ||||
| @@ -27,7 +27,8 @@ export const computeInitialHaFormData = ( | ||||
|       data[field.name] = 0.0; | ||||
|     } else if (field.type === "select") { | ||||
|       if (field.options.length) { | ||||
|         data[field.name] = field.options[0][0]; | ||||
|         const val = field.options[0]; | ||||
|         data[field.name] = Array.isArray(val) ? val[0] : val; | ||||
|       } | ||||
|     } else if (field.type === "positive_time_period_dict") { | ||||
|       data[field.name] = { | ||||
| @@ -60,8 +61,10 @@ export const computeInitialHaFormData = ( | ||||
|         data[field.name] = selector.number?.min ?? 0; | ||||
|       } else if ("select" in selector) { | ||||
|         if (selector.select?.options.length) { | ||||
|           const val = selector.select.options[0]; | ||||
|           data[field.name] = Array.isArray(val) ? val[0] : val; | ||||
|           const firstOption = selector.select.options[0]; | ||||
|           const val = | ||||
|             typeof firstOption === "string" ? firstOption : firstOption.value; | ||||
|           data[field.name] = selector.select.multiple ? [val] : val; | ||||
|         } | ||||
|       } else if ("duration" in selector) { | ||||
|         data[field.name] = { | ||||
|   | ||||
| @@ -7,6 +7,12 @@ import { hsv2rgb, rgb2hex } from "../common/color/convert-color"; | ||||
| import { rgbw2rgb, rgbww2rgb } from "../common/color/convert-light-color"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
|     "cursor-moved": { value?: any }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| function xy2polar(x: number, y: number) { | ||||
|   const r = Math.sqrt(x * x + y * y); | ||||
|   const phi = Math.atan2(y, x); | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display"; | ||||
| import { computeStateDisplay } from "../common/entity/compute_state_display"; | ||||
| import { isUnavailableState, OFF } from "../data/entity"; | ||||
| import { HumidifierEntity } from "../data/humidifier"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| @@ -21,12 +19,8 @@ class HaHumidifierState extends LitElement { | ||||
|                 ${this._localizeState()} | ||||
|                 ${this.stateObj.attributes.mode | ||||
|                   ? html`- | ||||
|                     ${computeAttributeValueDisplay( | ||||
|                       this.hass.localize, | ||||
|                     ${this.hass.formatEntityAttributeValue( | ||||
|                       this.stateObj, | ||||
|                       this.hass.locale, | ||||
|                       this.hass.config, | ||||
|                       this.hass.entities, | ||||
|                       "mode" | ||||
|                     )}` | ||||
|                   : ""} | ||||
| @@ -78,24 +72,17 @@ class HaHumidifierState extends LitElement { | ||||
|       return this.hass.localize(`state.default.${this.stateObj.state}`); | ||||
|     } | ||||
|  | ||||
|     const stateString = computeStateDisplay( | ||||
|       this.hass.localize, | ||||
|       this.stateObj, | ||||
|       this.hass.locale, | ||||
|       this.hass.config, | ||||
|       this.hass.entities | ||||
|     ); | ||||
|     const stateString = this.hass.formatEntityState(this.stateObj); | ||||
|  | ||||
|     return this.stateObj.attributes.action && this.stateObj.state !== OFF | ||||
|       ? `${computeAttributeValueDisplay( | ||||
|           this.hass.localize, | ||||
|           this.stateObj, | ||||
|           this.hass.locale, | ||||
|           this.hass.config, | ||||
|           this.hass.entities, | ||||
|           "action" | ||||
|         )} (${stateString})` | ||||
|       : stateString; | ||||
|     if (this.stateObj.attributes.action && this.stateObj.state !== OFF) { | ||||
|       const actionString = this.hass.formatEntityAttributeValue( | ||||
|         this.stateObj, | ||||
|         "action" | ||||
|       ); | ||||
|       return `${actionString} (${stateString})`; | ||||
|     } | ||||
|  | ||||
|     return stateString; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|   | ||||
| @@ -1,440 +0,0 @@ | ||||
| import { DIRECTION_ALL, Manager, Pan, Tap } from "@egjs/hammerjs"; | ||||
| import { LitElement, PropertyValues, css, html, svg } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { rgb2hex } from "../common/color/convert-color"; | ||||
| import { | ||||
|   DEFAULT_MAX_KELVIN, | ||||
|   DEFAULT_MIN_KELVIN, | ||||
|   temperature2rgb, | ||||
| } from "../common/color/convert-light-color"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
|  | ||||
| const SAFE_ZONE_FACTOR = 0.9; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
|     "cursor-moved": { value?: any }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| const A11Y_KEY_CODES = new Set([ | ||||
|   "ArrowRight", | ||||
|   "ArrowUp", | ||||
|   "ArrowLeft", | ||||
|   "ArrowDown", | ||||
|   "PageUp", | ||||
|   "PageDown", | ||||
|   "Home", | ||||
|   "End", | ||||
| ]); | ||||
|  | ||||
| function xy2polar(x: number, y: number) { | ||||
|   const r = Math.sqrt(x * x + y * y); | ||||
|   const phi = Math.atan2(y, x); | ||||
|   return [r, phi]; | ||||
| } | ||||
|  | ||||
| function polar2xy(r: number, phi: number) { | ||||
|   const x = Math.cos(phi) * r; | ||||
|   const y = Math.sin(phi) * r; | ||||
|   return [x, y]; | ||||
| } | ||||
|  | ||||
| function drawColorWheel( | ||||
|   ctx: CanvasRenderingContext2D, | ||||
|   minTemp: number, | ||||
|   maxTemp: number | ||||
| ) { | ||||
|   ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height); | ||||
|   const radius = ctx.canvas.width / 2; | ||||
|  | ||||
|   const min = Math.max(minTemp, 2000); | ||||
|   const max = Math.min(maxTemp, 40000); | ||||
|  | ||||
|   for (let y = -radius; y < radius; y += 1) { | ||||
|     const x = radius * Math.sqrt(1 - (y / radius) ** 2); | ||||
|  | ||||
|     const fraction = (y / (radius * SAFE_ZONE_FACTOR) + 1) / 2; | ||||
|  | ||||
|     const temperature = Math.max( | ||||
|       Math.min(min + fraction * (max - min), max), | ||||
|       min | ||||
|     ); | ||||
|  | ||||
|     const color = rgb2hex(temperature2rgb(temperature)); | ||||
|  | ||||
|     ctx.fillStyle = color; | ||||
|     ctx.fillRect(radius - x, radius + y - 0.5, 2 * x, 2); | ||||
|     ctx.fill(); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @customElement("ha-temp-color-picker") | ||||
| class HaTempColorPicker extends LitElement { | ||||
|   @property({ type: Boolean, reflect: true }) | ||||
|   public disabled = false; | ||||
|  | ||||
|   @property({ type: Number, attribute: false }) | ||||
|   public renderSize?: number; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public value?: number; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public min = DEFAULT_MIN_KELVIN; | ||||
|  | ||||
|   @property({ type: Number }) | ||||
|   public max = DEFAULT_MAX_KELVIN; | ||||
|  | ||||
|   @query("#canvas") private _canvas!: HTMLCanvasElement; | ||||
|  | ||||
|   private _mc?: HammerManager; | ||||
|  | ||||
|   @state() | ||||
|   private _pressed?: string; | ||||
|  | ||||
|   @state() | ||||
|   private _cursorPosition?: [number, number]; | ||||
|  | ||||
|   @state() | ||||
|   private _localValue?: number; | ||||
|  | ||||
|   protected firstUpdated(changedProps: PropertyValues): void { | ||||
|     super.firstUpdated(changedProps); | ||||
|     this._setupListeners(); | ||||
|     this._generateColorWheel(); | ||||
|     this.setAttribute("role", "slider"); | ||||
|     this.setAttribute("aria-orientation", "vertical"); | ||||
|     if (!this.hasAttribute("tabindex")) { | ||||
|       this.setAttribute("tabindex", "0"); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _generateColorWheel() { | ||||
|     const ctx = this._canvas.getContext("2d")!; | ||||
|     drawColorWheel(ctx, this.min, this.max); | ||||
|   } | ||||
|  | ||||
|   connectedCallback(): void { | ||||
|     super.connectedCallback(); | ||||
|     this._setupListeners(); | ||||
|   } | ||||
|  | ||||
|   disconnectedCallback(): void { | ||||
|     super.disconnectedCallback(); | ||||
|     this._destroyListeners(); | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues): void { | ||||
|     super.updated(changedProps); | ||||
|     if (changedProps.has("_localValue")) { | ||||
|       this.setAttribute("aria-valuenow", this._localValue?.toString() ?? ""); | ||||
|     } | ||||
|     if (changedProps.has("min") || changedProps.has("max")) { | ||||
|       this._generateColorWheel(); | ||||
|       this._resetPosition(); | ||||
|     } | ||||
|     if (changedProps.has("min")) { | ||||
|       this.setAttribute("aria-valuemin", this.min.toString()); | ||||
|     } | ||||
|     if (changedProps.has("max")) { | ||||
|       this.setAttribute("aria-valuemax", this.max.toString()); | ||||
|     } | ||||
|     if (changedProps.has("value")) { | ||||
|       if (this._localValue !== this.value) { | ||||
|         this._resetPosition(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _setupListeners() { | ||||
|     if (this._canvas && !this._mc) { | ||||
|       this._mc = new Manager(this._canvas); | ||||
|       this._mc.add( | ||||
|         new Pan({ | ||||
|           direction: DIRECTION_ALL, | ||||
|           enable: true, | ||||
|           threshold: 0, | ||||
|         }) | ||||
|       ); | ||||
|  | ||||
|       this._mc.add(new Tap({ event: "singletap" })); | ||||
|  | ||||
|       let savedPosition; | ||||
|       this._mc.on("panstart", (e) => { | ||||
|         if (this.disabled) return; | ||||
|         this._pressed = e.pointerType; | ||||
|         savedPosition = this._cursorPosition; | ||||
|       }); | ||||
|       this._mc.on("pancancel", () => { | ||||
|         if (this.disabled) return; | ||||
|         this._pressed = undefined; | ||||
|         this._cursorPosition = savedPosition; | ||||
|       }); | ||||
|       this._mc.on("panmove", (e) => { | ||||
|         if (this.disabled) return; | ||||
|         this._cursorPosition = this._getPositionFromEvent(e); | ||||
|         this._localValue = this._getValueFromCoord(...this._cursorPosition); | ||||
|         fireEvent(this, "cursor-moved", { value: this._localValue }); | ||||
|       }); | ||||
|       this._mc.on("panend", (e) => { | ||||
|         if (this.disabled) return; | ||||
|         this._pressed = undefined; | ||||
|         this._cursorPosition = this._getPositionFromEvent(e); | ||||
|         this._localValue = this._getValueFromCoord(...this._cursorPosition); | ||||
|         fireEvent(this, "cursor-moved", { value: undefined }); | ||||
|         fireEvent(this, "value-changed", { value: this._localValue }); | ||||
|       }); | ||||
|  | ||||
|       this._mc.on("singletap", (e) => { | ||||
|         if (this.disabled) return; | ||||
|         this._cursorPosition = this._getPositionFromEvent(e); | ||||
|         this._localValue = this._getValueFromCoord(...this._cursorPosition); | ||||
|         fireEvent(this, "value-changed", { value: this._localValue }); | ||||
|       }); | ||||
|  | ||||
|       this.addEventListener("keydown", this._handleKeyDown); | ||||
|       this.addEventListener("keyup", this._handleKeyUp); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _resetPosition() { | ||||
|     if (this.value === undefined) { | ||||
|       this._cursorPosition = undefined; | ||||
|       this._localValue = undefined; | ||||
|       return; | ||||
|     } | ||||
|     const [, y] = this._getCoordsFromValue(this.value); | ||||
|     const currentX = this._cursorPosition?.[0] ?? 0; | ||||
|     const x = | ||||
|       Math.sign(currentX) * Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX)); | ||||
|     this._cursorPosition = [x, y]; | ||||
|     this._localValue = this.value; | ||||
|   } | ||||
|  | ||||
|   private _getCoordsFromValue = (temperature: number): [number, number] => { | ||||
|     if (this.value === this.min) { | ||||
|       return [0, -1]; | ||||
|     } | ||||
|     if (this.value === this.max) { | ||||
|       return [0, 1]; | ||||
|     } | ||||
|     const fraction = (temperature - this.min) / (this.max - this.min); | ||||
|     const y = (2 * fraction - 1) * SAFE_ZONE_FACTOR; | ||||
|     return [0, y]; | ||||
|   }; | ||||
|  | ||||
|   private _getValueFromCoord = (_x: number, y: number): number => { | ||||
|     const fraction = (y / SAFE_ZONE_FACTOR + 1) / 2; | ||||
|     const temperature = Math.max( | ||||
|       Math.min(this.min + fraction * (this.max - this.min), this.max), | ||||
|       this.min | ||||
|     ); | ||||
|     return Math.round(temperature); | ||||
|   }; | ||||
|  | ||||
|   private _getPositionFromEvent = (e: HammerInput): [number, number] => { | ||||
|     const x = e.center.x; | ||||
|     const y = e.center.y; | ||||
|     const boundingRect = e.target.getBoundingClientRect(); | ||||
|     const offsetX = boundingRect.left; | ||||
|     const offsetY = boundingRect.top; | ||||
|     const maxX = e.target.clientWidth; | ||||
|     const maxY = e.target.clientHeight; | ||||
|  | ||||
|     const _x = (2 * (x - offsetX)) / maxX - 1; | ||||
|     const _y = (2 * (y - offsetY)) / maxY - 1; | ||||
|  | ||||
|     const [r, phi] = xy2polar(_x, _y); | ||||
|     const [__x, __y] = polar2xy(Math.min(1, r), phi); | ||||
|     return [__x, __y]; | ||||
|   }; | ||||
|  | ||||
|   private _destroyListeners() { | ||||
|     if (this._mc) { | ||||
|       this._mc.destroy(); | ||||
|       this._mc = undefined; | ||||
|     } | ||||
|     this.removeEventListener("keydown", this._handleKeyDown); | ||||
|     this.removeEventListener("keyup", this._handleKeyDown); | ||||
|   } | ||||
|  | ||||
|   _handleKeyDown(e: KeyboardEvent) { | ||||
|     if (!A11Y_KEY_CODES.has(e.code)) return; | ||||
|     e.preventDefault(); | ||||
|  | ||||
|     const step = 1; | ||||
|     const tenPercentStep = Math.max(step, (this.max - this.min) / 10); | ||||
|     const currentValue = | ||||
|       this._localValue ?? Math.round((this.max + this.min) / 2); | ||||
|     switch (e.code) { | ||||
|       case "ArrowRight": | ||||
|       case "ArrowUp": | ||||
|         this._localValue = Math.round(Math.min(currentValue + step, this.max)); | ||||
|         break; | ||||
|       case "ArrowLeft": | ||||
|       case "ArrowDown": | ||||
|         this._localValue = Math.round(Math.max(currentValue - step, this.min)); | ||||
|         break; | ||||
|       case "PageUp": | ||||
|         this._localValue = Math.round( | ||||
|           Math.min(currentValue + tenPercentStep, this.max) | ||||
|         ); | ||||
|         break; | ||||
|       case "PageDown": | ||||
|         this._localValue = Math.round( | ||||
|           Math.max(currentValue - tenPercentStep, this.min) | ||||
|         ); | ||||
|         break; | ||||
|       case "Home": | ||||
|         this._localValue = this.min; | ||||
|         break; | ||||
|       case "End": | ||||
|         this._localValue = this.max; | ||||
|         break; | ||||
|     } | ||||
|     if (this._localValue != null) { | ||||
|       const [_, y] = this._getCoordsFromValue(this._localValue); | ||||
|       const currentX = this._cursorPosition?.[0] ?? 0; | ||||
|       const x = | ||||
|         Math.sign(currentX) * | ||||
|         Math.min(Math.sqrt(1 - y ** 2), Math.abs(currentX)); | ||||
|       this._cursorPosition = [x, y]; | ||||
|       fireEvent(this, "cursor-moved", { value: this._localValue }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   _handleKeyUp(e: KeyboardEvent) { | ||||
|     if (!A11Y_KEY_CODES.has(e.code)) return; | ||||
|     e.preventDefault(); | ||||
|     this.value = this._localValue; | ||||
|     fireEvent(this, "value-changed", { value: this._localValue }); | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const size = this.renderSize || 400; | ||||
|     const canvasSize = size * window.devicePixelRatio; | ||||
|  | ||||
|     const rgb = temperature2rgb( | ||||
|       this._localValue ?? Math.round((this.max + this.min) / 2) | ||||
|     ); | ||||
|  | ||||
|     const [x, y] = this._cursorPosition ?? [0, 0]; | ||||
|  | ||||
|     const cx = ((x + 1) * size) / 2; | ||||
|     const cy = ((y + 1) * size) / 2; | ||||
|  | ||||
|     const markerPosition = `${cx}px, ${cy}px`; | ||||
|     const markerScale = this._pressed | ||||
|       ? this._pressed === "touch" | ||||
|         ? "2.5" | ||||
|         : "1.5" | ||||
|       : "1"; | ||||
|     const markerOffset = | ||||
|       this._pressed === "touch" ? `0px, -${size / 16}px` : "0px, 0px"; | ||||
|  | ||||
|     return html` | ||||
|       <div class="container ${classMap({ pressed: Boolean(this._pressed) })}"> | ||||
|         <canvas id="canvas" .width=${canvasSize} .height=${canvasSize}></canvas> | ||||
|         <svg | ||||
|           id="interaction" | ||||
|           viewBox="0 0 ${size} ${size}" | ||||
|           overflow="visible" | ||||
|           aria-hidden="true" | ||||
|         > | ||||
|           <defs>${this.renderSVGFilter()}</defs> | ||||
|           <g | ||||
|             style=${styleMap({ | ||||
|               fill: rgb2hex(rgb), | ||||
|               transform: `translate(${markerPosition})`, | ||||
|             })} | ||||
|             class="cursor" | ||||
|           > | ||||
|             <circle | ||||
|               cx="0" | ||||
|               cy="0" | ||||
|               r="16" | ||||
|               style=${styleMap({ | ||||
|                 fill: rgb2hex(rgb), | ||||
|                 transform: `translate(${markerOffset}) scale(${markerScale})`, | ||||
|                 visibility: this._cursorPosition ? undefined : "hidden", | ||||
|               })} | ||||
|             ></circle> | ||||
|           </g> | ||||
|         </svg> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   renderSVGFilter() { | ||||
|     return svg` | ||||
|       <filter | ||||
|         id="marker-shadow" | ||||
|         x="-50%" | ||||
|         y="-50%" | ||||
|         width="200%" | ||||
|         height="200%" | ||||
|         filterUnits="objectBoundingBox" | ||||
|       > | ||||
|         <feDropShadow dx="0" dy="1" stdDeviation="2" flood-opacity="0.3" flood-color="rgba(0, 0, 0, 1)"/> | ||||
|         <feDropShadow dx="0" dy="1" stdDeviation="3" flood-opacity="0.15" flood-color="rgba(0, 0, 0, 1)"/> | ||||
|       </filter> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       :host { | ||||
|         display: block; | ||||
|         outline: none; | ||||
|       } | ||||
|       .container { | ||||
|         position: relative; | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         display: flex; | ||||
|       } | ||||
|       canvas { | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         object-fit: contain; | ||||
|         border-radius: 50%; | ||||
|         transition: box-shadow 180ms ease-in-out; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       :host(:focus-visible) canvas { | ||||
|         box-shadow: 0 0 0 2px rgb(255, 160, 0); | ||||
|       } | ||||
|       svg { | ||||
|         position: absolute; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         pointer-events: none; | ||||
|       } | ||||
|       circle { | ||||
|         fill: black; | ||||
|         stroke: white; | ||||
|         stroke-width: 2; | ||||
|         filter: url(#marker-shadow); | ||||
|       } | ||||
|       .container:not(.pressed) circle { | ||||
|         transition: | ||||
|           transform 100ms ease-in-out, | ||||
|           fill 100ms ease-in-out; | ||||
|       } | ||||
|       .container:not(.pressed) .cursor { | ||||
|         transition: transform 200ms ease-in-out; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-temp-color-picker": HaTempColorPicker; | ||||
|   } | ||||
| } | ||||
| @@ -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 { computeStateDisplay } from "../common/entity/compute_state_display"; | ||||
| import { formatNumber } from "../common/number/format_number"; | ||||
| import LocalizeMixin from "../mixins/localize-mixin"; | ||||
|  | ||||
| @@ -84,12 +83,7 @@ class HaWaterHeaterState extends LocalizeMixin(PolymerElement) { | ||||
|   } | ||||
|  | ||||
|   _localizeState(stateObj) { | ||||
|     return computeStateDisplay( | ||||
|       this.hass.localize, | ||||
|       stateObj, | ||||
|       this.hass.locale, | ||||
|       this.hass.entities | ||||
|     ); | ||||
|     return this.hass.formatEntityState(stateObj); | ||||
|   } | ||||
| } | ||||
| customElements.define("ha-water_heater-state", HaWaterHeaterState); | ||||
|   | ||||
| @@ -238,11 +238,13 @@ export interface ZoneCondition extends BaseCondition { | ||||
|   zone: string; | ||||
| } | ||||
|  | ||||
| type Weekday = "sun" | "mon" | "tue" | "wed" | "thu" | "fri" | "sat"; | ||||
|  | ||||
| export interface TimeCondition extends BaseCondition { | ||||
|   condition: "time"; | ||||
|   after?: string; | ||||
|   before?: string; | ||||
|   weekday?: string | string[]; | ||||
|   weekday?: Weekday | Weekday[]; | ||||
| } | ||||
|  | ||||
| export interface TemplateCondition extends BaseCondition { | ||||
|   | ||||
| @@ -6,11 +6,7 @@ import { | ||||
|   formatTimeWithSeconds, | ||||
| } from "../common/datetime/format_time"; | ||||
| import secondsToDuration from "../common/datetime/seconds_to_duration"; | ||||
| import { | ||||
|   computeAttributeNameDisplay, | ||||
|   computeAttributeValueDisplay, | ||||
| } from "../common/entity/compute_attribute_display"; | ||||
| import { computeStateDisplay } from "../common/entity/compute_state_display"; | ||||
| import { computeAttributeNameDisplay } from "../common/entity/compute_attribute_display"; | ||||
| import { computeStateName } from "../common/entity/compute_state_name"; | ||||
| import "../resources/intl-polyfill"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| @@ -235,23 +231,14 @@ const tryDescribeTrigger = ( | ||||
|         for (const state of trigger.from.values()) { | ||||
|           from.push( | ||||
|             trigger.attribute | ||||
|               ? computeAttributeValueDisplay( | ||||
|                   hass.localize, | ||||
|                   stateObj, | ||||
|                   hass.locale, | ||||
|                   hass.config, | ||||
|                   hass.entities, | ||||
|                   trigger.attribute, | ||||
|                   state | ||||
|                 ).toString() | ||||
|               : computeStateDisplay( | ||||
|                   hass.localize, | ||||
|                   stateObj, | ||||
|                   hass.locale, | ||||
|                   hass.config, | ||||
|                   hass.entities, | ||||
|                   state | ||||
|                 ) | ||||
|               ? hass | ||||
|                   .formatEntityAttributeValue( | ||||
|                     stateObj, | ||||
|                     trigger.attribute, | ||||
|                     state | ||||
|                   ) | ||||
|                   .toString() | ||||
|               : hass.formatEntityState(stateObj, state) | ||||
|           ); | ||||
|         } | ||||
|         if (from.length !== 0) { | ||||
| @@ -261,23 +248,16 @@ const tryDescribeTrigger = ( | ||||
|       } else { | ||||
|         base += ` from ${ | ||||
|           trigger.attribute | ||||
|             ? computeAttributeValueDisplay( | ||||
|                 hass.localize, | ||||
|                 stateObj, | ||||
|                 hass.locale, | ||||
|                 hass.config, | ||||
|                 hass.entities, | ||||
|                 trigger.attribute, | ||||
|                 trigger.from | ||||
|               ).toString() | ||||
|             : computeStateDisplay( | ||||
|                 hass.localize, | ||||
|                 stateObj, | ||||
|                 hass.locale, | ||||
|                 hass.config, | ||||
|                 hass.entities, | ||||
|                 trigger.from.toString() | ||||
|               ).toString() | ||||
|             ? hass | ||||
|                 .formatEntityAttributeValue( | ||||
|                   stateObj, | ||||
|                   trigger.attribute, | ||||
|                   trigger.from | ||||
|                 ) | ||||
|                 .toString() | ||||
|             : hass | ||||
|                 .formatEntityState(stateObj, trigger.from.toString()) | ||||
|                 .toString() | ||||
|         }`; | ||||
|       } | ||||
|     } | ||||
| @@ -292,23 +272,14 @@ const tryDescribeTrigger = ( | ||||
|         for (const state of trigger.to.values()) { | ||||
|           to.push( | ||||
|             trigger.attribute | ||||
|               ? computeAttributeValueDisplay( | ||||
|                   hass.localize, | ||||
|                   stateObj, | ||||
|                   hass.locale, | ||||
|                   hass.config, | ||||
|                   hass.entities, | ||||
|                   trigger.attribute, | ||||
|                   state | ||||
|                 ).toString() | ||||
|               : computeStateDisplay( | ||||
|                   hass.localize, | ||||
|                   stateObj, | ||||
|                   hass.locale, | ||||
|                   hass.config, | ||||
|                   hass.entities, | ||||
|                   state | ||||
|                 ).toString() | ||||
|               ? hass | ||||
|                   .formatEntityAttributeValue( | ||||
|                     stateObj, | ||||
|                     trigger.attribute, | ||||
|                     state | ||||
|                   ) | ||||
|                   .toString() | ||||
|               : hass.formatEntityState(stateObj, state).toString() | ||||
|           ); | ||||
|         } | ||||
|         if (to.length !== 0) { | ||||
| @@ -318,23 +289,14 @@ const tryDescribeTrigger = ( | ||||
|       } else { | ||||
|         base += ` to ${ | ||||
|           trigger.attribute | ||||
|             ? computeAttributeValueDisplay( | ||||
|                 hass.localize, | ||||
|                 stateObj, | ||||
|                 hass.locale, | ||||
|                 hass.config, | ||||
|                 hass.entities, | ||||
|                 trigger.attribute, | ||||
|                 trigger.to | ||||
|               ).toString() | ||||
|             : computeStateDisplay( | ||||
|                 hass.localize, | ||||
|                 stateObj, | ||||
|                 hass.locale, | ||||
|                 hass.config, | ||||
|                 hass.entities, | ||||
|                 trigger.to.toString() | ||||
|               ) | ||||
|             ? hass | ||||
|                 .formatEntityAttributeValue( | ||||
|                   stateObj, | ||||
|                   trigger.attribute, | ||||
|                   trigger.to | ||||
|                 ) | ||||
|                 .toString() | ||||
|             : hass.formatEntityState(stateObj, trigger.to.toString()) | ||||
|         }`; | ||||
|       } | ||||
|     } | ||||
| @@ -822,45 +784,27 @@ const tryDescribeCondition = ( | ||||
|       for (const state of condition.state.values()) { | ||||
|         states.push( | ||||
|           condition.attribute | ||||
|             ? computeAttributeValueDisplay( | ||||
|                 hass.localize, | ||||
|                 stateObj, | ||||
|                 hass.locale, | ||||
|                 hass.config, | ||||
|                 hass.entities, | ||||
|                 condition.attribute, | ||||
|                 state | ||||
|               ).toString() | ||||
|             : computeStateDisplay( | ||||
|                 hass.localize, | ||||
|                 stateObj, | ||||
|                 hass.locale, | ||||
|                 hass.config, | ||||
|                 hass.entities, | ||||
|                 state | ||||
|               ) | ||||
|             ? hass | ||||
|                 .formatEntityAttributeValue( | ||||
|                   stateObj, | ||||
|                   condition.attribute, | ||||
|                   state | ||||
|                 ) | ||||
|                 .toString() | ||||
|             : hass.formatEntityState(stateObj, state) | ||||
|         ); | ||||
|       } | ||||
|     } else if (condition.state !== "") { | ||||
|       states.push( | ||||
|         condition.attribute | ||||
|           ? computeAttributeValueDisplay( | ||||
|               hass.localize, | ||||
|               stateObj, | ||||
|               hass.locale, | ||||
|               hass.config, | ||||
|               hass.entities, | ||||
|               condition.attribute, | ||||
|               condition.state | ||||
|             ).toString() | ||||
|           : computeStateDisplay( | ||||
|               hass.localize, | ||||
|               stateObj, | ||||
|               hass.locale, | ||||
|               hass.config, | ||||
|               hass.entities, | ||||
|               condition.state.toString() | ||||
|             ) | ||||
|           ? hass | ||||
|               .formatEntityAttributeValue( | ||||
|                 stateObj, | ||||
|                 condition.attribute, | ||||
|                 condition.state | ||||
|               ) | ||||
|               .toString() | ||||
|           : hass.formatEntityState(stateObj, condition.state.toString()) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| import { EntityFilter } from "../common/entity/entity_filter"; | ||||
| import { PlaceholderContainer } from "../panels/config/automation/thingtalk/dialog-thingtalk"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { AutomationConfig } from "./automation"; | ||||
|  | ||||
| interface CloudStatusNotLoggedIn { | ||||
|   logged_in: false; | ||||
| @@ -13,6 +11,7 @@ export interface CertificateInformation { | ||||
|   common_name: string; | ||||
|   expire_date: string; | ||||
|   fingerprint: string; | ||||
|   alternative_names: string[]; | ||||
| } | ||||
|  | ||||
| export interface CloudPreferences { | ||||
| @@ -66,11 +65,6 @@ export interface CloudWebhook { | ||||
|   managed?: boolean; | ||||
| } | ||||
|  | ||||
| export interface ThingTalkConversion { | ||||
|   config: Partial<AutomationConfig>; | ||||
|   placeholders: PlaceholderContainer; | ||||
| } | ||||
|  | ||||
| export const cloudLogin = ( | ||||
|   hass: HomeAssistant, | ||||
|   email: string, | ||||
| @@ -136,9 +130,6 @@ export const disconnectCloudRemote = (hass: HomeAssistant) => | ||||
| export const fetchCloudSubscriptionInfo = (hass: HomeAssistant) => | ||||
|   hass.callWS<SubscriptionInfo>({ type: "cloud/subscription" }); | ||||
|  | ||||
| export const convertThingTalk = (hass: HomeAssistant, query: string) => | ||||
|   hass.callWS<ThingTalkConversion>({ type: "cloud/thingtalk/convert", query }); | ||||
|  | ||||
| export const updateCloudPref = ( | ||||
|   hass: HomeAssistant, | ||||
|   prefs: { | ||||
|   | ||||
| @@ -1,46 +1,25 @@ | ||||
| import { timeCachePromiseFunc } from "../common/util/time-cache-function-promise"; | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| interface EntitySourceConfigEntry { | ||||
|   source: "config_entry"; | ||||
| interface EntitySource { | ||||
|   domain: string; | ||||
|   custom_component: boolean; | ||||
|   config_entry: string; | ||||
| } | ||||
|  | ||||
| interface EntitySourcePlatformConfig { | ||||
|   source: "platform_config"; | ||||
|   domain: string; | ||||
|   custom_component: boolean; | ||||
| } | ||||
| export type EntitySources = Record<string, EntitySource>; | ||||
|  | ||||
| export type EntitySources = Record< | ||||
|   string, | ||||
|   EntitySourceConfigEntry | EntitySourcePlatformConfig | ||||
| >; | ||||
|  | ||||
| const fetchEntitySources = ( | ||||
|   hass: HomeAssistant, | ||||
|   entity_id?: string | ||||
| ): Promise<EntitySources> => | ||||
|   hass.callWS({ | ||||
|     type: "entity/source", | ||||
|     entity_id, | ||||
|   }); | ||||
| const fetchEntitySources = (hass: HomeAssistant): Promise<EntitySources> => | ||||
|   hass.callWS({ type: "entity/source" }); | ||||
|  | ||||
| export const fetchEntitySourcesWithCache = ( | ||||
|   hass: HomeAssistant, | ||||
|   entity_id?: string | ||||
|   hass: HomeAssistant | ||||
| ): Promise<EntitySources> => | ||||
|   entity_id | ||||
|     ? fetchEntitySources(hass, entity_id) | ||||
|     : timeCachePromiseFunc( | ||||
|         "_entitySources", | ||||
|         // cache for 30 seconds | ||||
|         30000, | ||||
|         fetchEntitySources, | ||||
|         // We base the cache on number of states. If number of states | ||||
|         // changes we force a refresh | ||||
|         (hass2) => Object.keys(hass2.states).length, | ||||
|         hass | ||||
|       ); | ||||
|   timeCachePromiseFunc( | ||||
|     "_entitySources", | ||||
|     // cache for 30 seconds | ||||
|     30000, | ||||
|     fetchEntitySources, | ||||
|     // We base the cache on number of states. If number of states | ||||
|     // changes we force a refresh | ||||
|     (hass2) => Object.keys(hass2.states).length, | ||||
|     hass | ||||
|   ); | ||||
|   | ||||
| @@ -5,14 +5,12 @@ import { | ||||
|   DOMAINS_WITH_DYNAMIC_PICTURE, | ||||
| } from "../common/const"; | ||||
| import { computeDomain } from "../common/entity/compute_domain"; | ||||
| import { computeStateDisplay } from "../common/entity/compute_state_display"; | ||||
| import { computeStateDomain } from "../common/entity/compute_state_domain"; | ||||
| import { autoCaseNoun } from "../common/translations/auto_case_noun"; | ||||
| import { LocalizeFunc } from "../common/translations/localize"; | ||||
| import { HaEntityPickerEntityFilterFunc } from "../components/entity/ha-entity-picker"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { UNAVAILABLE, UNKNOWN } from "./entity"; | ||||
| import { computeAttributeValueDisplay } from "../common/entity/compute_attribute_display"; | ||||
|  | ||||
| const LOGBOOK_LOCALIZE_PATH = "ui.components.logbook.messages"; | ||||
| export const CONTINUOUS_DOMAINS = ["counter", "proximity", "sensor", "zone"]; | ||||
| @@ -339,14 +337,9 @@ export const localizeStateMessage = ( | ||||
|  | ||||
|       // TODO: This is not working yet, as we don't get historic attribute values | ||||
|  | ||||
|       const event_type = computeAttributeValueDisplay( | ||||
|         hass!.localize, | ||||
|         stateObj, | ||||
|         hass.locale, | ||||
|         hass.config, | ||||
|         hass.entities, | ||||
|         "event_type" | ||||
|       )?.toString(); | ||||
|       const event_type = hass | ||||
|         .formatEntityAttributeValue(stateObj, "event_type") | ||||
|         ?.toString(); | ||||
|  | ||||
|       if (!event_type) { | ||||
|         return localize(`${LOGBOOK_LOCALIZE_PATH}.detected_unknown_event`); | ||||
| @@ -392,16 +385,7 @@ export const localizeStateMessage = ( | ||||
|   return hass.localize( | ||||
|     `${LOGBOOK_LOCALIZE_PATH}.changed_to_state`, | ||||
|     "state", | ||||
|     stateObj | ||||
|       ? computeStateDisplay( | ||||
|           localize, | ||||
|           stateObj, | ||||
|           hass.locale, | ||||
|           hass.config, | ||||
|           hass.entities, | ||||
|           state | ||||
|         ) | ||||
|       : state | ||||
|     stateObj ? hass.formatEntityState(stateObj, state) : state | ||||
|   ); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import { | ||||
| } from "home-assistant-js-websocket"; | ||||
| import durationToSeconds from "../common/datetime/duration_to_seconds"; | ||||
| import secondsToDuration from "../common/datetime/seconds_to_duration"; | ||||
| import { computeStateDisplay } from "../common/entity/compute_state_display"; | ||||
| import { HomeAssistant } from "../types"; | ||||
|  | ||||
| export type TimerEntity = HassEntityBase & { | ||||
| @@ -90,25 +89,13 @@ export const computeDisplayTimer = ( | ||||
|   } | ||||
|  | ||||
|   if (stateObj.state === "idle" || timeRemaining === 0) { | ||||
|     return computeStateDisplay( | ||||
|       hass.localize, | ||||
|       stateObj, | ||||
|       hass.locale, | ||||
|       hass.config, | ||||
|       hass.entities | ||||
|     ); | ||||
|     return hass.formatEntityState(stateObj); | ||||
|   } | ||||
|  | ||||
|   let display = secondsToDuration(timeRemaining || 0); | ||||
|  | ||||
|   if (stateObj.state === "paused") { | ||||
|     display = `${display} (${computeStateDisplay( | ||||
|       hass.localize, | ||||
|       stateObj, | ||||
|       hass.locale, | ||||
|       hass.config, | ||||
|       hass.entities | ||||
|     )})`; | ||||
|     display = `${display} (${hass.formatEntityState(stateObj)})`; | ||||
|   } | ||||
|  | ||||
|   return display; | ||||
|   | ||||
| @@ -36,7 +36,9 @@ export const enum WeatherEntityFeature { | ||||
|   FORECAST_TWICE_DAILY = 4, | ||||
| } | ||||
|  | ||||
| export type ForecastType = "legacy" | "hourly" | "daily" | "twice_daily"; | ||||
| export type ModernForecastType = "hourly" | "daily" | "twice_daily"; | ||||
|  | ||||
| export type ForecastType = ModernForecastType | "legacy"; | ||||
|  | ||||
| interface ForecastAttribute { | ||||
|   temperature: number; | ||||
| @@ -636,7 +638,7 @@ export const getForecast = ( | ||||
| export const subscribeForecast = ( | ||||
|   hass: HomeAssistant, | ||||
|   entity_id: string, | ||||
|   forecast_type: "daily" | "hourly" | "twice_daily", | ||||
|   forecast_type: ModernForecastType, | ||||
|   callback: (forecastevent: ForecastEvent) => void | ||||
| ) => | ||||
|   hass.connection.subscribeMessage<ForecastEvent>(callback, { | ||||
| @@ -645,6 +647,22 @@ export const subscribeForecast = ( | ||||
|     entity_id, | ||||
|   }); | ||||
|  | ||||
| export const getSupportedForecastTypes = ( | ||||
|   stateObj: HassEntityBase | ||||
| ): ModernForecastType[] => { | ||||
|   const supported: ModernForecastType[] = []; | ||||
|   if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) { | ||||
|     supported.push("daily"); | ||||
|   } | ||||
|   if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_TWICE_DAILY)) { | ||||
|     supported.push("twice_daily"); | ||||
|   } | ||||
|   if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_HOURLY)) { | ||||
|     supported.push("hourly"); | ||||
|   } | ||||
|   return supported; | ||||
| }; | ||||
|  | ||||
| export const getDefaultForecastType = (stateObj: HassEntityBase) => { | ||||
|   if (supportsFeature(stateObj, WeatherEntityFeature.FORECAST_DAILY)) { | ||||
|     return "daily"; | ||||
|   | ||||
| @@ -6,7 +6,12 @@ export interface RenderTemplateResult { | ||||
|   listeners: TemplateListeners; | ||||
| } | ||||
|  | ||||
| interface TemplateListeners { | ||||
| export interface RenderTemplateError { | ||||
|   error: string; | ||||
|   level: "ERROR" | "WARNING"; | ||||
| } | ||||
|  | ||||
| export interface TemplateListeners { | ||||
|   all: boolean; | ||||
|   domains: string[]; | ||||
|   entities: string[]; | ||||
| @@ -18,6 +23,7 @@ export type TemplatePreview = TemplatePreviewState | TemplatePreviewError; | ||||
| interface TemplatePreviewState { | ||||
|   state: string; | ||||
|   attributes: Record<string, any>; | ||||
|   listeners: TemplateListeners; | ||||
| } | ||||
|  | ||||
| interface TemplatePreviewError { | ||||
| @@ -26,19 +32,23 @@ interface TemplatePreviewError { | ||||
|  | ||||
| export const subscribeRenderTemplate = ( | ||||
|   conn: Connection, | ||||
|   onChange: (result: RenderTemplateResult) => void, | ||||
|   onChange: (result: RenderTemplateResult | RenderTemplateError) => void, | ||||
|   params: { | ||||
|     template: string; | ||||
|     entity_ids?: string | string[]; | ||||
|     variables?: Record<string, unknown>; | ||||
|     timeout?: number; | ||||
|     strict?: boolean; | ||||
|     report_errors?: boolean; | ||||
|   } | ||||
| ): Promise<UnsubscribeFunc> => | ||||
|   conn.subscribeMessage((msg: RenderTemplateResult) => onChange(msg), { | ||||
|     type: "render_template", | ||||
|     ...params, | ||||
|   }); | ||||
|   conn.subscribeMessage( | ||||
|     (msg: RenderTemplateResult | RenderTemplateError) => onChange(msg), | ||||
|     { | ||||
|       type: "render_template", | ||||
|       ...params, | ||||
|     } | ||||
|   ); | ||||
|  | ||||
| export const subscribePreviewTemplate = ( | ||||
|   hass: HomeAssistant, | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { computeStateDisplay } from "../../../common/entity/compute_state_display"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import { isUnavailableState } from "../../../data/entity"; | ||||
| import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; | ||||
| @@ -21,6 +20,7 @@ class EntityPreviewRow extends LitElement { | ||||
|     return html`<state-badge | ||||
|         .hass=${this.hass} | ||||
|         .stateObj=${stateObj} | ||||
|         stateColor | ||||
|       ></state-badge> | ||||
|       <div class="name" .title=${computeStateName(stateObj)}> | ||||
|         ${computeStateName(stateObj)} | ||||
| @@ -35,13 +35,7 @@ class EntityPreviewRow extends LitElement { | ||||
|                 capitalize | ||||
|               ></hui-timestamp-display> | ||||
|             ` | ||||
|           : computeStateDisplay( | ||||
|               this.hass!.localize, | ||||
|               stateObj, | ||||
|               this.hass.locale, | ||||
|               this.hass.config, | ||||
|               this.hass.entities | ||||
|             )} | ||||
|           : this.hass.formatEntityState(stateObj)} | ||||
|       </div>`; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -49,7 +49,7 @@ class FlowPreviewGroup extends LitElement { | ||||
|   private _setPreview = (preview: GroupPreview) => { | ||||
|     const now = new Date().toISOString(); | ||||
|     this._preview = { | ||||
|       entity_id: `${this.stepId}.flow_preview`, | ||||
|       entity_id: `${this.stepId}.___flow_preview___`, | ||||
|       last_changed: now, | ||||
|       last_updated: now, | ||||
|       context: { id: "", parent_id: null, user_id: null }, | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { LitElement, html } from "lit"; | ||||
| import { LitElement, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { debounce } from "../../../common/util/debounce"; | ||||
| import { FlowType } from "../../../data/data_entry_flow"; | ||||
| import { | ||||
|   TemplateListeners, | ||||
|   TemplatePreview, | ||||
|   subscribePreviewTemplate, | ||||
| } from "../../../data/ws-templates"; | ||||
| @@ -27,6 +28,8 @@ class FlowPreviewTemplate extends LitElement { | ||||
|  | ||||
|   @state() private _preview?: HassEntity; | ||||
|  | ||||
|   @state() private _listeners?: TemplateListeners; | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   private _unsub?: Promise<UnsubscribeFunc>; | ||||
| @@ -50,9 +53,69 @@ class FlowPreviewTemplate extends LitElement { | ||||
|       return html`<ha-alert alert-type="error">${this._error}</ha-alert>`; | ||||
|     } | ||||
|     return html`<entity-preview-row | ||||
|       .hass=${this.hass} | ||||
|       .stateObj=${this._preview} | ||||
|     ></entity-preview-row>`; | ||||
|         .hass=${this.hass} | ||||
|         .stateObj=${this._preview} | ||||
|       ></entity-preview-row> | ||||
|       ${this._listeners?.time | ||||
|         ? html` | ||||
|             <p> | ||||
|               ${this.hass.localize("ui.dialogs.helper_settings.template.time")} | ||||
|             </p> | ||||
|           ` | ||||
|         : nothing} | ||||
|       ${!this._listeners | ||||
|         ? nothing | ||||
|         : this._listeners.all | ||||
|         ? html` | ||||
|             <p class="all_listeners"> | ||||
|               ${this.hass.localize( | ||||
|                 "ui.dialogs.helper_settings.template.all_listeners" | ||||
|               )} | ||||
|             </p> | ||||
|           ` | ||||
|         : this._listeners.domains.length || this._listeners.entities.length | ||||
|         ? html` | ||||
|             <p> | ||||
|               ${this.hass.localize( | ||||
|                 "ui.dialogs.helper_settings.template.listeners" | ||||
|               )} | ||||
|             </p> | ||||
|             <ul> | ||||
|               ${this._listeners.domains | ||||
|                 .sort() | ||||
|                 .map( | ||||
|                   (domain) => html` | ||||
|                     <li> | ||||
|                       <b | ||||
|                         >${this.hass.localize( | ||||
|                           "ui.dialogs.helper_settings.template.domain" | ||||
|                         )}</b | ||||
|                       >: ${domain} | ||||
|                     </li> | ||||
|                   ` | ||||
|                 )} | ||||
|               ${this._listeners.entities | ||||
|                 .sort() | ||||
|                 .map( | ||||
|                   (entity_id) => html` | ||||
|                     <li> | ||||
|                       <b | ||||
|                         >${this.hass.localize( | ||||
|                           "ui.dialogs.helper_settings.template.entity" | ||||
|                         )}</b | ||||
|                       >: ${entity_id} | ||||
|                     </li> | ||||
|                   ` | ||||
|                 )} | ||||
|             </ul> | ||||
|           ` | ||||
|         : !this._listeners.time | ||||
|         ? html`<p class="all_listeners"> | ||||
|             ${this.hass.localize( | ||||
|               "ui.dialogs.helper_settings.template.no_listeners" | ||||
|             )} | ||||
|           </p>` | ||||
|         : nothing} `; | ||||
|   } | ||||
|  | ||||
|   private _setPreview = (preview: TemplatePreview) => { | ||||
| @@ -62,13 +125,15 @@ class FlowPreviewTemplate extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|     this._error = undefined; | ||||
|     this._listeners = preview.listeners; | ||||
|     const now = new Date().toISOString(); | ||||
|     this._preview = { | ||||
|       entity_id: `${this.stepId}.flow_preview`, | ||||
|       entity_id: `${this.stepId}.___flow_preview___`, | ||||
|       last_changed: now, | ||||
|       last_updated: now, | ||||
|       context: { id: "", parent_id: null, user_id: null }, | ||||
|       ...preview, | ||||
|       attributes: preview.attributes, | ||||
|       state: preview.state, | ||||
|     }; | ||||
|   }; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import { html } from "lit"; | ||||
| import { ConfigEntry } from "../../data/config_entries"; | ||||
| import { domainToName, IntegrationManifest } from "../../data/integration"; | ||||
| import { domainToName } from "../../data/integration"; | ||||
| import { | ||||
|   createOptionsFlow, | ||||
|   deleteOptionsFlow, | ||||
| @@ -8,6 +8,7 @@ import { | ||||
|   handleOptionsFlowStep, | ||||
| } from "../../data/options_flow"; | ||||
| import { | ||||
|   DataEntryFlowDialogParams, | ||||
|   loadDataEntryFlowDialog, | ||||
|   showFlowDialog, | ||||
| } from "./show-dialog-data-entry-flow"; | ||||
| @@ -17,14 +18,14 @@ export const loadOptionsFlowDialog = loadDataEntryFlowDialog; | ||||
| export const showOptionsFlowDialog = ( | ||||
|   element: HTMLElement, | ||||
|   configEntry: ConfigEntry, | ||||
|   manifest?: IntegrationManifest | null | ||||
|   dialogParams?: Omit<DataEntryFlowDialogParams, "flowConfig"> | ||||
| ): void => | ||||
|   showFlowDialog( | ||||
|     element, | ||||
|     { | ||||
|       startFlowHandler: configEntry.entry_id, | ||||
|       domain: configEntry.domain, | ||||
|       manifest, | ||||
|       ...dialogParams, | ||||
|     }, | ||||
|     { | ||||
|       flowType: "options_flow", | ||||
|   | ||||
| @@ -11,7 +11,6 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { UNIT_F } from "../../../../common/const"; | ||||
| import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display"; | ||||
| import { stateActive } from "../../../../common/entity/state_active"; | ||||
| import { stateColorCss } from "../../../../common/entity/state_color"; | ||||
| import { supportsFeature } from "../../../../common/entity/supports-feature"; | ||||
| @@ -162,14 +161,10 @@ export class HaMoreInfoClimateTemperature extends LitElement { | ||||
|  | ||||
|     const action = this.stateObj.attributes.hvac_action; | ||||
|  | ||||
|     const actionLabel = computeAttributeValueDisplay( | ||||
|       this.hass.localize, | ||||
|     const actionLabel = this.hass.formatEntityAttributeValue( | ||||
|       this.stateObj, | ||||
|       this.hass.locale, | ||||
|       this.hass.config, | ||||
|       this.hass.entities, | ||||
|       "hvac_action" | ||||
|     ) as string; | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <p class="label"> | ||||
|   | ||||
| @@ -35,9 +35,8 @@ export class HaMoreInfoCoverPosition extends LitElement { | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const forcedState = this.stateObj.state === "closed" ? "open" : undefined; | ||||
|  | ||||
|     const color = stateColorCss(this.stateObj, forcedState); | ||||
|     const openColor = stateColorCss(this.stateObj, "open"); | ||||
|     const color = stateColorCss(this.stateObj); | ||||
|  | ||||
|     return html` | ||||
|       <ha-control-slider | ||||
| @@ -55,6 +54,8 @@ export class HaMoreInfoCoverPosition extends LitElement { | ||||
|           "current_position" | ||||
|         )} | ||||
|         style=${styleMap({ | ||||
|           // Use open color for inactive state to avoid grey slider that looks disabled | ||||
|           "--state-cover-inactive-color": openColor, | ||||
|           "--control-slider-color": color, | ||||
|           "--control-slider-background": color, | ||||
|         })} | ||||
|   | ||||
| @@ -72,9 +72,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement { | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const forcedState = this.stateObj.state === "closed" ? "open" : undefined; | ||||
|  | ||||
|     const color = stateColorCss(this.stateObj, forcedState); | ||||
|     const openColor = stateColorCss(this.stateObj, "open"); | ||||
|     const color = stateColorCss(this.stateObj); | ||||
|  | ||||
|     return html` | ||||
|       <ha-control-slider | ||||
| @@ -91,6 +90,8 @@ export class HaMoreInfoCoverTiltPosition extends LitElement { | ||||
|           "current_tilt_position" | ||||
|         )} | ||||
|         style=${styleMap({ | ||||
|           // Use open color for inactive state to avoid grey slider that looks disabled | ||||
|           "--state-cover-inactive-color": openColor, | ||||
|           "--control-slider-color": color, | ||||
|           "--control-slider-background": color, | ||||
|         })} | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { css, CSSResultGroup, html, LitElement } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { computeAttributeNameDisplay } from "../../../../common/entity/compute_attribute_display"; | ||||
| import { computeStateDisplay } from "../../../../common/entity/compute_state_display"; | ||||
| import { stateActive } from "../../../../common/entity/state_active"; | ||||
| import { stateColorCss } from "../../../../common/entity/state_color"; | ||||
| import "../../../../components/ha-control-select"; | ||||
| @@ -12,12 +11,12 @@ import { UNAVAILABLE } from "../../../../data/entity"; | ||||
| import { | ||||
|   computeFanSpeedCount, | ||||
|   computeFanSpeedIcon, | ||||
|   FAN_SPEED_COUNT_MAX_FOR_BUTTONS, | ||||
|   FAN_SPEEDS, | ||||
|   FanEntity, | ||||
|   fanPercentageToSpeed, | ||||
|   FanSpeed, | ||||
|   fanSpeedToPercentage, | ||||
|   FAN_SPEEDS, | ||||
|   FAN_SPEED_COUNT_MAX_FOR_BUTTONS, | ||||
| } from "../../../../data/fan"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
|  | ||||
| @@ -68,14 +67,7 @@ export class HaMoreInfoFanSpeed extends LitElement { | ||||
|  | ||||
|   private _localizeSpeed(speed: FanSpeed) { | ||||
|     if (speed === "on" || speed === "off") { | ||||
|       return computeStateDisplay( | ||||
|         this.hass.localize, | ||||
|         this.stateObj, | ||||
|         this.hass.locale, | ||||
|         this.hass.config, | ||||
|         this.hass.entities, | ||||
|         speed | ||||
|       ); | ||||
|       return this.hass.formatEntityState(this.stateObj, speed); | ||||
|     } | ||||
|     return ( | ||||
|       this.hass.localize(`ui.dialogs.more_info_control.fan.speed.${speed}`) || | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { computeStateDisplay } from "../../../common/entity/compute_state_display"; | ||||
| import "../../../components/ha-absolute-time"; | ||||
| import "../../../components/ha-relative-time"; | ||||
| import { isUnavailableState } from "../../../data/entity"; | ||||
| @@ -20,30 +18,22 @@ export class HaMoreInfoStateHeader extends LitElement { | ||||
|  | ||||
|   @state() private _absoluteTime = false; | ||||
|  | ||||
|   private _computeStateDisplay(stateObj: HassEntity): TemplateResult | string { | ||||
|   private _localizeState(): TemplateResult | string { | ||||
|     if ( | ||||
|       stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP && | ||||
|       !isUnavailableState(stateObj.state) | ||||
|       this.stateObj.attributes.device_class === SENSOR_DEVICE_CLASS_TIMESTAMP && | ||||
|       !isUnavailableState(this.stateObj.state) | ||||
|     ) { | ||||
|       return html` | ||||
|         <hui-timestamp-display | ||||
|           .hass=${this.hass} | ||||
|           .ts=${new Date(stateObj.state)} | ||||
|           .ts=${new Date(this.stateObj.state)} | ||||
|           format="relative" | ||||
|           capitalize | ||||
|         ></hui-timestamp-display> | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     const stateDisplay = computeStateDisplay( | ||||
|       this.hass!.localize, | ||||
|       stateObj, | ||||
|       this.hass!.locale, | ||||
|       this.hass!.config, | ||||
|       this.hass!.entities | ||||
|     ); | ||||
|  | ||||
|     return stateDisplay; | ||||
|     return this.hass.formatEntityState(this.stateObj); | ||||
|   } | ||||
|  | ||||
|   private _toggleAbsolute() { | ||||
| @@ -51,8 +41,7 @@ export class HaMoreInfoStateHeader extends LitElement { | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const stateDisplay = | ||||
|       this.stateOverride ?? this._computeStateDisplay(this.stateObj); | ||||
|     const stateDisplay = this.stateOverride ?? this._localizeState(); | ||||
|  | ||||
|     return html` | ||||
|       <p class="state">${stateDisplay}</p> | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { mdiMinus, mdiPlus } from "@mdi/js"; | ||||
| import { CSSResultGroup, LitElement, PropertyValues, css, html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { computeAttributeValueDisplay } from "../../../../common/entity/compute_attribute_display"; | ||||
| import { stateActive } from "../../../../common/entity/state_active"; | ||||
| import { stateColorCss } from "../../../../common/entity/state_color"; | ||||
| import { clamp } from "../../../../common/number/clamp"; | ||||
| @@ -92,14 +91,10 @@ export class HaMoreInfoHumidifierHumidity extends LitElement { | ||||
|  | ||||
|     const action = this.stateObj.attributes.action; | ||||
|  | ||||
|     const actionLabel = computeAttributeValueDisplay( | ||||
|       this.hass.localize, | ||||
|     const actionLabel = this.hass.formatEntityAttributeValue( | ||||
|       this.stateObj, | ||||
|       this.hass.locale, | ||||
|       this.hass.config, | ||||
|       this.hass.entities, | ||||
|       "action" | ||||
|     ) as string; | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <p class="label"> | ||||
|   | ||||
| @@ -1,6 +1,4 @@ | ||||
| import "@material/mwc-button"; | ||||
| import "@material/mwc-tab-bar/mwc-tab-bar"; | ||||
| import "@material/mwc-tab/mwc-tab"; | ||||
| import { mdiEyedropper } from "@mdi/js"; | ||||
| import { | ||||
|   css, | ||||
| @@ -26,7 +24,6 @@ import "../../../../components/ha-hs-color-picker"; | ||||
| import "../../../../components/ha-icon"; | ||||
| import "../../../../components/ha-icon-button-prev"; | ||||
| import "../../../../components/ha-labeled-slider"; | ||||
| import "../../../../components/ha-temp-color-picker"; | ||||
| import { | ||||
|   getLightCurrentModeRgbColor, | ||||
|   LightColor, | ||||
|   | ||||
| @@ -1,25 +1,31 @@ | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { rgb2hex } from "../../../../common/color/convert-color"; | ||||
| import { | ||||
|   DEFAULT_MAX_KELVIN, | ||||
|   DEFAULT_MIN_KELVIN, | ||||
|   temperature2rgb, | ||||
| } from "../../../../common/color/convert-light-color"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stateColorCss } from "../../../../common/entity/state_color"; | ||||
| import { throttle } from "../../../../common/util/throttle"; | ||||
| import "../../../../components/ha-temp-color-picker"; | ||||
| import "../../../../components/ha-control-slider"; | ||||
| import { UNAVAILABLE } from "../../../../data/entity"; | ||||
| import { | ||||
|   LightColor, | ||||
|   LightColorMode, | ||||
|   LightEntity, | ||||
| } from "../../../../data/light"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { | ||||
|   DEFAULT_MAX_KELVIN, | ||||
|   DEFAULT_MIN_KELVIN, | ||||
| } from "../../../../common/color/convert-light-color"; | ||||
|  | ||||
| declare global { | ||||
|   interface HASSDomEvents { | ||||
| @@ -28,6 +34,26 @@ declare global { | ||||
|   } | ||||
| } | ||||
|  | ||||
| export const generateColorTemperatureGradient = (min: number, max: number) => { | ||||
|   const count = 10; | ||||
|  | ||||
|   const gradient: [number, string][] = []; | ||||
|  | ||||
|   const step = (max - min) / count; | ||||
|   const percentageStep = 1 / count; | ||||
|  | ||||
|   for (let i = 0; i < count + 1; i++) { | ||||
|     const value = min + step * i; | ||||
|  | ||||
|     const hex = rgb2hex(temperature2rgb(value)); | ||||
|     gradient.push([percentageStep * i, hex]); | ||||
|   } | ||||
|  | ||||
|   return gradient | ||||
|     .map(([stop, color]) => `${color} ${(stop as number) * 100}%`) | ||||
|     .join(", "); | ||||
| }; | ||||
|  | ||||
| @customElement("light-color-temp-picker") | ||||
| class LightColorTempPicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -46,18 +72,36 @@ class LightColorTempPicker extends LitElement { | ||||
|     const maxKelvin = | ||||
|       this.stateObj.attributes.max_color_temp_kelvin ?? DEFAULT_MAX_KELVIN; | ||||
|  | ||||
|     const gradient = this._generateTemperatureGradient(minKelvin!, maxKelvin); | ||||
|     const color = stateColorCss(this.stateObj); | ||||
|  | ||||
|     return html` | ||||
|       <ha-temp-color-picker | ||||
|         @value-changed=${this._ctColorChanged} | ||||
|         @cursor-moved=${this._ctColorCursorMoved} | ||||
|       <ha-control-slider | ||||
|         inverted | ||||
|         vertical | ||||
|         .value=${this._ctPickerValue} | ||||
|         .min=${minKelvin} | ||||
|         .max=${maxKelvin} | ||||
|         .value=${this._ctPickerValue} | ||||
|         mode="cursor" | ||||
|         @value-changed=${this._ctColorChanged} | ||||
|         @slider-moved=${this._ctColorCursorMoved} | ||||
|         .ariaLabel=${this.hass.localize( | ||||
|           "ui.dialogs.more_info_control.light.color_temp" | ||||
|         )} | ||||
|         style=${styleMap({ | ||||
|           "--control-slider-color": color, | ||||
|           "--gradient": gradient, | ||||
|         })} | ||||
|         .disabled=${this.stateObj.state === UNAVAILABLE} | ||||
|       > | ||||
|       </ha-temp-color-picker> | ||||
|       </ha-control-slider> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _generateTemperatureGradient = memoizeOne( | ||||
|     (min: number, max: number) => generateColorTemperatureGradient(min, max) | ||||
|   ); | ||||
|  | ||||
|   public _updateSliderValues() { | ||||
|     const stateObj = this.stateObj; | ||||
|  | ||||
| @@ -138,10 +182,18 @@ class LightColorTempPicker extends LitElement { | ||||
|           flex-direction: column; | ||||
|         } | ||||
|  | ||||
|         ha-temp-color-picker { | ||||
|         ha-control-slider { | ||||
|           height: 45vh; | ||||
|           max-height: 320px; | ||||
|           min-height: 200px; | ||||
|           --control-slider-thickness: 100px; | ||||
|           --control-slider-border-radius: 24px; | ||||
|           --control-slider-color: var(--primary-color); | ||||
|           --control-slider-background: -webkit-linear-gradient( | ||||
|             top, | ||||
|             var(--gradient) | ||||
|           ); | ||||
|           --control-slider-background-opacity: 1; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   | ||||
| @@ -1,22 +1,21 @@ | ||||
| import { mdiMenu, mdiSwapVertical } from "@mdi/js"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { computeStateDisplay } from "../../../common/entity/compute_state_display"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import "../../../components/ha-attributes"; | ||||
| import "../../../components/ha-icon-button-group"; | ||||
| import "../../../components/ha-icon-button-toggle"; | ||||
| import { | ||||
|   computeCoverPositionStateDisplay, | ||||
|   CoverEntity, | ||||
|   CoverEntityFeature, | ||||
|   computeCoverPositionStateDisplay, | ||||
| } from "../../../data/cover"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import "../components/cover/ha-more-info-cover-buttons"; | ||||
| @@ -83,12 +82,8 @@ class MoreInfoCover extends LitElement { | ||||
|     const forcedState = | ||||
|       liveValue != null ? (liveValue ? "open" : "closed") : undefined; | ||||
|  | ||||
|     const stateDisplay = computeStateDisplay( | ||||
|       this.hass.localize, | ||||
|     const stateDisplay = this.hass.formatEntityState( | ||||
|       this.stateObj!, | ||||
|       this.hass.locale, | ||||
|       this.hass.config, | ||||
|       this.hass.entities, | ||||
|       forcedState | ||||
|     ); | ||||
|  | ||||
|   | ||||
| @@ -10,11 +10,6 @@ import { | ||||
| import { property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../../../common/dom/stop_propagation"; | ||||
| import { | ||||
|   computeAttributeNameDisplay, | ||||
|   computeAttributeValueDisplay, | ||||
| } from "../../../common/entity/compute_attribute_display"; | ||||
| import { computeStateDisplay } from "../../../common/entity/compute_state_display"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import "../../../components/ha-control-select-menu"; | ||||
| import "../../../components/ha-list-item"; | ||||
| @@ -58,26 +53,21 @@ class MoreInfoHumidifier extends LitElement { | ||||
|       HumidifierEntityFeature.MODES | ||||
|     ); | ||||
|  | ||||
|     const currentHumidity = this.stateObj.attributes.current_humidity as number; | ||||
|  | ||||
|     return html` | ||||
|       <div class="current"> | ||||
|         ${currentHumidity != null | ||||
|         ${this.stateObj.attributes.current_humidity != null | ||||
|           ? html` | ||||
|               <div> | ||||
|                 <p class="label"> | ||||
|                   ${computeAttributeNameDisplay( | ||||
|                     this.hass.localize, | ||||
|                   ${this.hass.formatEntityAttributeName( | ||||
|                     this.stateObj, | ||||
|                     this.hass.entities, | ||||
|                     "current_humidity" | ||||
|                   )} | ||||
|                 </p> | ||||
|                 <p class="value"> | ||||
|                   ${this.hass.formatEntityAttributeValue( | ||||
|                     this.stateObj, | ||||
|                     "current_humidity", | ||||
|                     currentHumidity | ||||
|                     "current_humidity" | ||||
|                   )} | ||||
|                 </p> | ||||
|               </div> | ||||
| @@ -104,24 +94,10 @@ class MoreInfoHumidifier extends LitElement { | ||||
|         > | ||||
|           <ha-svg-icon slot="icon" .path=${mdiPower}></ha-svg-icon> | ||||
|           <ha-list-item value="off"> | ||||
|             ${computeStateDisplay( | ||||
|               this.hass.localize, | ||||
|               this.stateObj, | ||||
|               this.hass.locale, | ||||
|               this.hass.config, | ||||
|               this.hass.entities, | ||||
|               "off" | ||||
|             )} | ||||
|             ${this.hass.formatEntityState(this.stateObj, "off")} | ||||
|           </ha-list-item> | ||||
|           <ha-list-item value="on"> | ||||
|             ${computeStateDisplay( | ||||
|               this.hass.localize, | ||||
|               this.stateObj, | ||||
|               this.hass.locale, | ||||
|               this.hass.config, | ||||
|               this.hass.entities, | ||||
|               "on" | ||||
|             )} | ||||
|             ${this.hass.formatEntityState(this.stateObj, "on")} | ||||
|           </ha-list-item> | ||||
|         </ha-control-select-menu> | ||||
|  | ||||
| @@ -144,12 +120,8 @@ class MoreInfoHumidifier extends LitElement { | ||||
|                         slot="graphic" | ||||
|                         .path=${computeHumidiferModeIcon(mode)} | ||||
|                       ></ha-svg-icon> | ||||
|                       ${computeAttributeValueDisplay( | ||||
|                         hass.localize, | ||||
|                       ${this.hass.formatEntityAttributeValue( | ||||
|                         stateObj!, | ||||
|                         hass.locale, | ||||
|                         hass.config, | ||||
|                         hass.entities, | ||||
|                         "mode", | ||||
|                         mode | ||||
|                       )} | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { mdiHomeImportOutline, mdiPause, mdiPlay } from "@mdi/js"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { computeStateDisplay } from "../../../common/entity/compute_state_display"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import { blankBeforePercent } from "../../../common/translations/blank_before_percent"; | ||||
| @@ -74,15 +73,7 @@ class MoreInfoLawnMower extends LitElement { | ||||
|                 )}: | ||||
|               </span> | ||||
|               <span> | ||||
|                 <strong> | ||||
|                   ${computeStateDisplay( | ||||
|                     this.hass.localize, | ||||
|                     stateObj, | ||||
|                     this.hass.locale, | ||||
|                     this.hass.config, | ||||
|                     this.hass.entities | ||||
|                   )} | ||||
|                 </strong> | ||||
|                 <strong>${this.hass.formatEntityState(stateObj)}</strong> | ||||
|               </span> | ||||
|             </div> | ||||
|             ${this._renderBattery()} | ||||
|   | ||||
| @@ -9,24 +9,23 @@ import { | ||||
|   mdiVolumeOff, | ||||
|   mdiVolumePlus, | ||||
| } from "@mdi/js"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { stopPropagation } from "../../../common/dom/stop_propagation"; | ||||
| import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display"; | ||||
| import { stateActive } from "../../../common/entity/state_active"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import { computeRTLDirection } from "../../../common/util/compute_rtl"; | ||||
| import { stateActive } from "../../../common/entity/state_active"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| import "../../../components/ha-select"; | ||||
| import "../../../components/ha-slider"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { showMediaBrowserDialog } from "../../../components/media-player/show-media-browser-dialog"; | ||||
| import { | ||||
|   computeMediaControls, | ||||
|   handleMediaControlClick, | ||||
|   MediaPickedEvent, | ||||
|   MediaPlayerEntity, | ||||
|   MediaPlayerEntityFeature, | ||||
|   computeMediaControls, | ||||
|   handleMediaControlClick, | ||||
|   mediaPlayerPlayMedia, | ||||
| } from "../../../data/media-player"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
| @@ -157,24 +156,20 @@ class MoreInfoMediaPlayer extends LitElement { | ||||
|               > | ||||
|                 ${stateObj.attributes.source_list!.map( | ||||
|                   (source) => html` | ||||
|                     <mwc-list-item .value=${source} | ||||
|                       >${computeAttributeValueDisplay( | ||||
|                         this.hass.localize, | ||||
|                     <mwc-list-item .value=${source}> | ||||
|                       ${this.hass.formatEntityAttributeValue( | ||||
|                         stateObj, | ||||
|                         this.hass.locale, | ||||
|                         this.hass.config, | ||||
|                         this.hass.entities, | ||||
|                         "source", | ||||
|                         source | ||||
|                       )}</mwc-list-item | ||||
|                     > | ||||
|                       )} | ||||
|                     </mwc-list-item> | ||||
|                   ` | ||||
|                 )} | ||||
|                 <ha-svg-icon .path=${mdiLoginVariant} slot="icon"></ha-svg-icon> | ||||
|               </ha-select> | ||||
|             </div> | ||||
|           ` | ||||
|         : ""} | ||||
|         : nothing} | ||||
|       ${stateActive(stateObj) && | ||||
|       supportsFeature(stateObj, MediaPlayerEntityFeature.SELECT_SOUND_MODE) && | ||||
|       stateObj.attributes.sound_mode_list?.length | ||||
| @@ -191,17 +186,13 @@ class MoreInfoMediaPlayer extends LitElement { | ||||
|               > | ||||
|                 ${stateObj.attributes.sound_mode_list.map( | ||||
|                   (mode) => html` | ||||
|                     <mwc-list-item .value=${mode} | ||||
|                       >${computeAttributeValueDisplay( | ||||
|                         this.hass.localize, | ||||
|                     <mwc-list-item .value=${mode}> | ||||
|                       ${this.hass.formatEntityAttributeValue( | ||||
|                         stateObj, | ||||
|                         this.hass.locale, | ||||
|                         this.hass.config, | ||||
|                         this.hass.entities, | ||||
|                         "sound_mode", | ||||
|                         mode | ||||
|                       )}</mwc-list-item | ||||
|                     > | ||||
|                       )} | ||||
|                     </mwc-list-item> | ||||
|                   ` | ||||
|                 )} | ||||
|                 <ha-svg-icon .path=${mdiMusicNote} slot="icon"></ha-svg-icon> | ||||
|   | ||||
| @@ -2,11 +2,10 @@ import "@material/mwc-list/mwc-list"; | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display"; | ||||
| import { stopPropagation } from "../../../common/dom/stop_propagation"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import "../../../components/ha-attributes"; | ||||
| import { RemoteEntity, REMOTE_SUPPORT_ACTIVITY } from "../../../data/remote"; | ||||
| import { REMOTE_SUPPORT_ACTIVITY, RemoteEntity } from "../../../data/remote"; | ||||
| import { HomeAssistant } from "../../../types"; | ||||
|  | ||||
| const filterExtraAttributes = "activity_list,current_activity"; | ||||
| @@ -40,12 +39,8 @@ class MoreInfoRemote extends LitElement { | ||||
|               ${stateObj.attributes.activity_list!.map( | ||||
|                 (activity) => html` | ||||
|                   <mwc-list-item .value=${activity}> | ||||
|                     ${computeAttributeValueDisplay( | ||||
|                       this.hass.localize, | ||||
|                     ${this.hass.formatEntityAttributeValue( | ||||
|                       stateObj, | ||||
|                       this.hass.locale, | ||||
|                       this.hass.config, | ||||
|                       this.hass.entities, | ||||
|                       "activity", | ||||
|                       activity | ||||
|                     )} | ||||
| @@ -54,7 +49,7 @@ class MoreInfoRemote extends LitElement { | ||||
|               )} | ||||
|             </mwc-list> | ||||
|           ` | ||||
|         : ""} | ||||
|         : nothing} | ||||
|  | ||||
|       <ha-attributes | ||||
|         .hass=${this.hass} | ||||
|   | ||||
| @@ -13,8 +13,6 @@ import { CSSResultGroup, LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { stopPropagation } from "../../../common/dom/stop_propagation"; | ||||
| import { computeAttributeValueDisplay } from "../../../common/entity/compute_attribute_display"; | ||||
| import { computeStateDisplay } from "../../../common/entity/compute_state_display"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import "../../../components/entity/ha-battery-icon"; | ||||
| @@ -127,21 +125,8 @@ class MoreInfoVacuum extends LitElement { | ||||
|                 <strong> | ||||
|                   ${supportsFeature(stateObj, VacuumEntityFeature.STATUS) && | ||||
|                   stateObj.attributes.status | ||||
|                     ? computeAttributeValueDisplay( | ||||
|                         this.hass.localize, | ||||
|                         stateObj, | ||||
|                         this.hass.locale, | ||||
|                         this.hass.config, | ||||
|                         this.hass.entities, | ||||
|                         "status" | ||||
|                       ) | ||||
|                     : computeStateDisplay( | ||||
|                         this.hass.localize, | ||||
|                         stateObj, | ||||
|                         this.hass.locale, | ||||
|                         this.hass.config, | ||||
|                         this.hass.entities | ||||
|                       )} | ||||
|                     ? this.hass.formatEntityAttributeValue(stateObj, "status") | ||||
|                     : this.hass.formatEntityState(stateObj)} | ||||
|                 </strong> | ||||
|               </span> | ||||
|             </div> | ||||
| @@ -197,12 +182,8 @@ class MoreInfoVacuum extends LitElement { | ||||
|                   ${stateObj.attributes.fan_speed_list!.map( | ||||
|                     (mode) => html` | ||||
|                       <mwc-list-item .value=${mode}> | ||||
|                         ${computeAttributeValueDisplay( | ||||
|                           this.hass.localize, | ||||
|                         ${this.hass.formatEntityAttributeValue( | ||||
|                           stateObj, | ||||
|                           this.hass.locale, | ||||
|                           this.hass.config, | ||||
|                           this.hass.entities, | ||||
|                           "fan_speed", | ||||
|                           mode | ||||
|                         )} | ||||
| @@ -215,12 +196,8 @@ class MoreInfoVacuum extends LitElement { | ||||
|                 > | ||||
|                   <span> | ||||
|                     <ha-svg-icon .path=${mdiFan}></ha-svg-icon> | ||||
|                     ${computeAttributeValueDisplay( | ||||
|                       this.hass.localize, | ||||
|                     ${this.hass.formatEntityAttributeValue( | ||||
|                       stateObj, | ||||
|                       this.hass.locale, | ||||
|                       this.hass.config, | ||||
|                       this.hass.entities, | ||||
|                       "fan_speed" | ||||
|                     )} | ||||
|                   </span> | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import "@material/mwc-tab"; | ||||
| import "@material/mwc-tab-bar"; | ||||
| import { | ||||
|   mdiEye, | ||||
|   mdiGauge, | ||||
| @@ -14,14 +16,17 @@ import { | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { formatDateWeekdayDay } from "../../../common/datetime/format_date"; | ||||
| import { formatTimeWeekday } from "../../../common/datetime/format_time"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { | ||||
|   ForecastEvent, | ||||
|   ModernForecastType, | ||||
|   WeatherEntity, | ||||
|   getDefaultForecastType, | ||||
|   getForecast, | ||||
|   getSupportedForecastTypes, | ||||
|   getWind, | ||||
|   subscribeForecast, | ||||
|   weatherIcons, | ||||
| @@ -36,6 +41,8 @@ class MoreInfoWeather extends LitElement { | ||||
|  | ||||
|   @state() private _forecastEvent?: ForecastEvent; | ||||
|  | ||||
|   @state() private _forecastType?: ModernForecastType; | ||||
|  | ||||
|   @state() private _subscribed?: Promise<() => void>; | ||||
|  | ||||
|   private _unsubscribeForecastEvents() { | ||||
| @@ -43,25 +50,28 @@ class MoreInfoWeather extends LitElement { | ||||
|       this._subscribed.then((unsub) => unsub()); | ||||
|       this._subscribed = undefined; | ||||
|     } | ||||
|     this._forecastEvent = undefined; | ||||
|   } | ||||
|  | ||||
|   private async _subscribeForecastEvents() { | ||||
|     this._unsubscribeForecastEvents(); | ||||
|     if (!this.isConnected || !this.hass || !this.stateObj) { | ||||
|     if ( | ||||
|       !this.isConnected || | ||||
|       !this.hass || | ||||
|       !this.stateObj || | ||||
|       !this._forecastType | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const forecastType = getDefaultForecastType(this.stateObj); | ||||
|     if (forecastType) { | ||||
|       this._subscribed = subscribeForecast( | ||||
|         this.hass!, | ||||
|         this.stateObj!.entity_id, | ||||
|         forecastType, | ||||
|         (event) => { | ||||
|           this._forecastEvent = event; | ||||
|         } | ||||
|       ); | ||||
|     } | ||||
|     this._subscribed = subscribeForecast( | ||||
|       this.hass!, | ||||
|       this.stateObj!.entity_id, | ||||
|       this._forecastType, | ||||
|       (event) => { | ||||
|         this._forecastEvent = event; | ||||
|       } | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   public connectedCallback() { | ||||
| @@ -93,10 +103,10 @@ class MoreInfoWeather extends LitElement { | ||||
|     return false; | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues): void { | ||||
|     super.updated(changedProps); | ||||
|   protected willUpdate(changedProps: PropertyValues): void { | ||||
|     super.willUpdate(changedProps); | ||||
|  | ||||
|     if (changedProps.has("stateObj") || !this._subscribed) { | ||||
|     if ((changedProps.has("stateObj") || !this._subscribed) && this.stateObj) { | ||||
|       const oldState = changedProps.get("stateObj") as | ||||
|         | WeatherEntity | ||||
|         | undefined; | ||||
| @@ -104,16 +114,25 @@ class MoreInfoWeather extends LitElement { | ||||
|         oldState?.entity_id !== this.stateObj?.entity_id || | ||||
|         !this._subscribed | ||||
|       ) { | ||||
|         this._forecastType = getDefaultForecastType(this.stateObj); | ||||
|         this._subscribeForecastEvents(); | ||||
|       } | ||||
|     } else if (changedProps.has("_forecastType")) { | ||||
|       this._subscribeForecastEvents(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _supportedForecasts = memoizeOne((stateObj: WeatherEntity) => | ||||
|     getSupportedForecastTypes(stateObj) | ||||
|   ); | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this.hass || !this.stateObj) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     const supportedForecasts = this._supportedForecasts(this.stateObj); | ||||
|  | ||||
|     const forecastData = getForecast( | ||||
|       this.stateObj.attributes, | ||||
|       this._forecastEvent | ||||
| @@ -210,6 +229,23 @@ class MoreInfoWeather extends LitElement { | ||||
|             <div class="section"> | ||||
|               ${this.hass.localize("ui.card.weather.forecast")}: | ||||
|             </div> | ||||
|             ${supportedForecasts.length > 1 | ||||
|               ? html`<mwc-tab-bar | ||||
|                   .activeIndex=${supportedForecasts.findIndex( | ||||
|                     (item) => item === this._forecastType | ||||
|                   )} | ||||
|                   @MDCTabBar:activated=${this._handleForecastTypeChanged} | ||||
|                 > | ||||
|                   ${supportedForecasts.map( | ||||
|                     (forecastType) => | ||||
|                       html`<mwc-tab | ||||
|                         .label=${this.hass!.localize( | ||||
|                           `ui.card.weather.${forecastType}` | ||||
|                         )} | ||||
|                       ></mwc-tab>` | ||||
|                   )} | ||||
|                 </mwc-tab-bar>` | ||||
|               : nothing} | ||||
|             ${forecast.map((item) => | ||||
|               this._showValue(item.templow) || this._showValue(item.temperature) | ||||
|                 ? html`<div class="flex"> | ||||
| @@ -283,12 +319,23 @@ class MoreInfoWeather extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _handleForecastTypeChanged(ev: CustomEvent): void { | ||||
|     this._forecastType = this._supportedForecasts(this.stateObj!)[ | ||||
|       ev.detail.index | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       ha-svg-icon { | ||||
|         color: var(--paper-item-icon-color); | ||||
|         margin-left: 8px; | ||||
|       } | ||||
|  | ||||
|       mwc-tab-bar { | ||||
|         margin-bottom: 4px; | ||||
|       } | ||||
|  | ||||
|       .section { | ||||
|         margin: 16px 0 8px 0; | ||||
|         font-size: 1.2em; | ||||
|   | ||||
| @@ -1,5 +1,3 @@ | ||||
| import "@material/mwc-tab"; | ||||
| import "@material/mwc-tab-bar"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import "@material/mwc-button"; | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { computeStateDisplay } from "../../common/entity/compute_state_display"; | ||||
| import { domainToName } from "../../data/integration"; | ||||
| import { PersitentNotificationEntity } from "../../data/persistent_notification"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| @@ -33,15 +32,9 @@ export class HuiConfiguratorNotificationItem extends LitElement { | ||||
|           )} | ||||
|         </div> | ||||
|  | ||||
|         <mwc-button slot="actions" @click=${this._handleClick} | ||||
|           >${computeStateDisplay( | ||||
|             this.hass.localize, | ||||
|             this.notification, | ||||
|             this.hass.locale, | ||||
|             this.hass.config, | ||||
|             this.hass.entities | ||||
|           )}</mwc-button | ||||
|         > | ||||
|         <mwc-button slot="actions" @click=${this._handleClick}> | ||||
|           ${this.hass.formatEntityState(this.notification)} | ||||
|         </mwc-button> | ||||
|       </notification-item-template> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -1,5 +1,6 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; | ||||
| import { property, state } from "lit/decorators"; | ||||
| import "../components/ha-logo-svg"; | ||||
|  | ||||
| class HaInitPage extends LitElement { | ||||
|   @property({ type: Boolean }) public error = false; | ||||
| @@ -13,36 +14,36 @@ class HaInitPage extends LitElement { | ||||
|   private _retryInterval?: number; | ||||
|  | ||||
|   protected render() { | ||||
|     return this.error | ||||
|       ? html` | ||||
|           <p>Unable to connect to Home Assistant.</p> | ||||
|           <p class="retry-text"> | ||||
|             Retrying in ${this._retryInSeconds} seconds... | ||||
|           </p> | ||||
|           <mwc-button @click=${this._retry}>Retry now</mwc-button> | ||||
|           ${location.host.includes("ui.nabu.casa") | ||||
|             ? html` | ||||
|                 <p> | ||||
|                   It is possible that you are seeing this screen because your | ||||
|                   Home Assistant is not currently connected. You can ask it to | ||||
|                   come online from your | ||||
|                   <a href="https://account.nabucasa.com/" | ||||
|                     >Nabu Casa account page</a | ||||
|                   >. | ||||
|                 </p> | ||||
|               ` | ||||
|             : ""} | ||||
|         ` | ||||
|       : html` | ||||
|           <div id="progress-indicator-wrapper"> | ||||
|             <ha-circular-progress active></ha-circular-progress> | ||||
|           </div> | ||||
|           <div id="loading-text"> | ||||
|             ${this.migration | ||||
|               ? "Database migration in progress, please wait this might take some time" | ||||
|               : "Loading data"} | ||||
|           </div> | ||||
|         `; | ||||
|     return html`<ha-logo-svg></ha-logo-svg>${this.error | ||||
|         ? html` | ||||
|             <p>Unable to connect to Home Assistant.</p> | ||||
|             <p class="retry-text"> | ||||
|               Retrying in ${this._retryInSeconds} seconds... | ||||
|             </p> | ||||
|             <mwc-button @click=${this._retry}>Retry now</mwc-button> | ||||
|             ${location.host.includes("ui.nabu.casa") | ||||
|               ? html` | ||||
|                   <p> | ||||
|                     It is possible that you are seeing this screen because your | ||||
|                     Home Assistant is not currently connected. You can ask it to | ||||
|                     come online from your | ||||
|                     <a href="https://account.nabucasa.com/" | ||||
|                       >Nabu Casa account page</a | ||||
|                     >. | ||||
|                   </p> | ||||
|                 ` | ||||
|               : ""} | ||||
|           ` | ||||
|         : html` | ||||
|             <div id="progress-indicator-wrapper"> | ||||
|               <ha-circular-progress active></ha-circular-progress> | ||||
|             </div> | ||||
|             <div id="loading-text"> | ||||
|               ${this.migration | ||||
|                 ? "Database migration in progress, please wait this might take some time" | ||||
|                 : "Loading data"} | ||||
|             </div> | ||||
|           `}`; | ||||
|   } | ||||
|  | ||||
|   disconnectedCallback() { | ||||
| @@ -63,12 +64,15 @@ class HaInitPage extends LitElement { | ||||
|  | ||||
|   protected firstUpdated() { | ||||
|     this._showProgressIndicatorTimeout = window.setTimeout(() => { | ||||
|       this._showProgressIndicatorTimeout = undefined; | ||||
|       import("../components/ha-circular-progress"); | ||||
|     }, 5000); | ||||
|  | ||||
|     this._retryInterval = window.setInterval(() => { | ||||
|       const remainingSeconds = this._retryInSeconds--; | ||||
|       if (remainingSeconds <= 0) { | ||||
|         clearInterval(this._retryInterval); | ||||
|         this._retryInterval = undefined; | ||||
|         this._retry(); | ||||
|       } | ||||
|     }, 1000); | ||||
| @@ -86,6 +90,11 @@ class HaInitPage extends LitElement { | ||||
|         flex-direction: column; | ||||
|         align-items: center; | ||||
|       } | ||||
|       ha-logo-svg { | ||||
|         height: 170px; | ||||
|         width: 170px; | ||||
|         padding: 12px; | ||||
|       } | ||||
|       #progress-indicator-wrapper { | ||||
|         display: flex; | ||||
|         align-items: center; | ||||
|   | ||||
| @@ -9,15 +9,11 @@ import { HassElement } from "../state/hass-element"; | ||||
| import QuickBarMixin from "../state/quick-bar-mixin"; | ||||
| import { HomeAssistant, Route } from "../types"; | ||||
| import { storeState } from "../util/ha-pref-storage"; | ||||
| import { | ||||
|   renderLaunchScreenInfoBox, | ||||
|   removeLaunchScreen, | ||||
| } from "../util/launch-screen"; | ||||
| import { renderLaunchScreen, removeLaunchScreen } from "../util/launch-screen"; | ||||
| import { | ||||
|   registerServiceWorker, | ||||
|   supportsServiceWorker, | ||||
| } from "../util/register-service-worker"; | ||||
| import "./ha-init-page"; | ||||
| import "./home-assistant-main"; | ||||
|  | ||||
| const useHash = __DEMO__; | ||||
| @@ -39,8 +35,12 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { | ||||
|  | ||||
|   private _haVersion?: string; | ||||
|  | ||||
|   private _error?: boolean; | ||||
|  | ||||
|   private _hiddenTimeout?: number; | ||||
|  | ||||
|   private _renderInitTimeout?: number; | ||||
|  | ||||
|   private _visiblePromiseResolve?: () => void; | ||||
|  | ||||
|   constructor() { | ||||
| @@ -89,6 +89,10 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { | ||||
|     ) { | ||||
|       this.render = this.renderHass; | ||||
|       this.update = super.update; | ||||
|       if (this._renderInitTimeout) { | ||||
|         clearTimeout(this._renderInitTimeout); | ||||
|         this._renderInitTimeout = undefined; | ||||
|       } | ||||
|       removeLaunchScreen(); | ||||
|     } | ||||
|     super.update(changedProps); | ||||
| @@ -139,7 +143,9 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { | ||||
|     // Render launch screen info box (loading data / error message) | ||||
|     // if Home Assistant is not loaded yet. | ||||
|     if (this.render !== this.renderHass) { | ||||
|       this._renderInitInfo(false); | ||||
|       this._renderInitTimeout = window.setTimeout(() => { | ||||
|         this._renderInitInfo(); | ||||
|       }, 1000); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -153,7 +159,7 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { | ||||
|     } | ||||
|     if (changedProps.has("_databaseMigration")) { | ||||
|       if (this.render !== this.renderHass) { | ||||
|         this._renderInitInfo(false); | ||||
|         this._renderInitInfo(); | ||||
|       } else if (this._databaseMigration) { | ||||
|         // we already removed the launch screen, so we refresh to add it again to show the migration screen | ||||
|         location.reload(); | ||||
| @@ -233,7 +239,8 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { | ||||
|       this._haVersion = conn.haVersion; | ||||
|       this.initializeHass(auth, conn); | ||||
|     } catch (err: any) { | ||||
|       this._renderInitInfo(true); | ||||
|       this._error = true; | ||||
|       this._renderInitInfo(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -290,10 +297,15 @@ export class HomeAssistantAppEl extends QuickBarMixin(HassElement) { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _renderInitInfo(error: boolean) { | ||||
|     renderLaunchScreenInfoBox( | ||||
|   private async _renderInitInfo() { | ||||
|     if (this._renderInitTimeout) { | ||||
|       clearTimeout(this._renderInitTimeout); | ||||
|     } | ||||
|     this._renderInitTimeout = undefined; | ||||
|     await import("./ha-init-page"); | ||||
|     renderLaunchScreen( | ||||
|       html`<ha-init-page | ||||
|         .error=${error} | ||||
|         .error=${this._error} | ||||
|         .migration=${this._databaseMigration} | ||||
|       ></ha-init-page>` | ||||
|     ); | ||||
|   | ||||
| @@ -15,7 +15,11 @@ import { | ||||
| } from "../common/auth/token_storage"; | ||||
| import { applyThemesOnElement } from "../common/dom/apply_themes_on_element"; | ||||
| import { HASSDomEvent } from "../common/dom/fire_event"; | ||||
| import { extractSearchParamsObject } from "../common/url/search-params"; | ||||
| import { | ||||
|   addSearchParam, | ||||
|   extractSearchParam, | ||||
|   extractSearchParamsObject, | ||||
| } from "../common/url/search-params"; | ||||
| import { subscribeOne } from "../common/util/subscribe-one"; | ||||
| import "../components/ha-card"; | ||||
| import "../components/ha-language-picker"; | ||||
| @@ -39,6 +43,8 @@ import "./onboarding-loading"; | ||||
| import "./onboarding-welcome"; | ||||
| import "./onboarding-welcome-links"; | ||||
| import { makeDialogManager } from "../dialogs/make-dialog-manager"; | ||||
| import { navigate } from "../common/navigate"; | ||||
| import { mainWindow } from "../common/dom/get_main_window"; | ||||
|  | ||||
| type OnboardingEvent = | ||||
|   | { | ||||
| @@ -96,6 +102,27 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { | ||||
|  | ||||
|   @state() private _steps?: OnboardingStep[]; | ||||
|  | ||||
|   @state() private _page = extractSearchParam("page"); | ||||
|  | ||||
|   private _mobileApp = | ||||
|     extractSearchParam("redirect_uri") === "homeassistant://auth-callback"; | ||||
|  | ||||
|   connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     mainWindow.addEventListener("location-changed", this._updatePage); | ||||
|     mainWindow.addEventListener("popstate", this._updatePage); | ||||
|   } | ||||
|  | ||||
|   disconnectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     mainWindow.removeEventListener("location-changed", this._updatePage); | ||||
|     mainWindow.removeEventListener("popstate", this._updatePage); | ||||
|   } | ||||
|  | ||||
|   private _updatePage = () => { | ||||
|     this._page = extractSearchParam("page"); | ||||
|   }; | ||||
|  | ||||
|   protected render() { | ||||
|     return html`<mwc-linear-progress | ||||
|         .progress=${this._progress} | ||||
| @@ -103,9 +130,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { | ||||
|       <ha-card> | ||||
|         <div class="card-content">${this._renderStep()}</div> | ||||
|       </ha-card> | ||||
|       ${this._init | ||||
|       ${this._init && !this._restoring | ||||
|         ? html`<onboarding-welcome-links | ||||
|             .localize=${this.localize} | ||||
|             .mobileApp=${this._mobileApp} | ||||
|           ></onboarding-welcome-links>` | ||||
|         : nothing} | ||||
|       <div class="footer"> | ||||
| @@ -125,6 +153,14 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { | ||||
|   } | ||||
|  | ||||
|   private _renderStep() { | ||||
|     if (this._restoring) { | ||||
|       return html`<onboarding-restore-backup | ||||
|         .hass=${this.hass} | ||||
|         .localize=${this.localize} | ||||
|       > | ||||
|       </onboarding-restore-backup>`; | ||||
|     } | ||||
|  | ||||
|     if (this._init) { | ||||
|       return html`<onboarding-welcome | ||||
|         .localize=${this.localize} | ||||
| @@ -133,11 +169,6 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { | ||||
|       ></onboarding-welcome>`; | ||||
|     } | ||||
|  | ||||
|     if (this._restoring) { | ||||
|       return html`<onboarding-restore-backup .localize=${this.localize}> | ||||
|       </onboarding-restore-backup>`; | ||||
|     } | ||||
|  | ||||
|     const step = this._curStep()!; | ||||
|  | ||||
|     if (this._loading || !step) { | ||||
| @@ -195,6 +226,12 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     super.updated(changedProps); | ||||
|     if (changedProps.has("_page")) { | ||||
|       this._restoring = this._page === "restore_backup"; | ||||
|       if (this._page === null && this._steps && !this._steps[0].done) { | ||||
|         this._init = true; | ||||
|       } | ||||
|     } | ||||
|     if (changedProps.has("language")) { | ||||
|       document.querySelector("html")!.setAttribute("lang", this.language!); | ||||
|     } | ||||
| @@ -312,6 +349,10 @@ class HaOnboarding extends litLocalizeLiteMixin(HassElement) { | ||||
|       this._restoring = stepResult.result.restore; | ||||
|       if (!this._restoring) { | ||||
|         this._progress = 0.25; | ||||
|       } else { | ||||
|         navigate( | ||||
|           `${location.pathname}?${addSearchParam({ page: "restore_backup" })}` | ||||
|         ); | ||||
|       } | ||||
|     } else if (stepResult.type === "user") { | ||||
|       const result = stepResult.result as OnboardingResponses["user"]; | ||||
|   | ||||
| @@ -29,6 +29,7 @@ const HIDDEN_DOMAINS = new Set([ | ||||
|   "radio_browser", | ||||
|   "rpi_power", | ||||
|   "sun", | ||||
|   "google_translate", | ||||
| ]); | ||||
|  | ||||
| @customElement("onboarding-integrations") | ||||
|   | ||||
| @@ -1,42 +1,55 @@ | ||||
| import "@material/mwc-button/mwc-button"; | ||||
| import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { showBackupUploadDialog } from "../../hassio/src/dialogs/backup/show-dialog-backup-upload"; | ||||
| import { showHassioBackupDialog } from "../../hassio/src/dialogs/backup/show-dialog-hassio-backup"; | ||||
| import "../../hassio/src/components/hassio-upload-backup"; | ||||
| import type { LocalizeFunc } from "../common/translations/localize"; | ||||
| import "../components/ha-ansi-to-html"; | ||||
| import "../components/ha-card"; | ||||
| import { fetchInstallationType } from "../data/onboarding"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./onboarding-loading"; | ||||
| import { onBoardingStyles } from "./styles"; | ||||
| import { removeSearchParam } from "../common/url/search-params"; | ||||
| import { navigate } from "../common/navigate"; | ||||
|  | ||||
| @customElement("onboarding-restore-backup") | ||||
| class OnboardingRestoreBackup extends LitElement { | ||||
|   @property() public localize!: LocalizeFunc; | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public localize!: LocalizeFunc; | ||||
|  | ||||
|   @property() public language!: string; | ||||
|  | ||||
|   @state() public _restoring = false; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return this._restoring | ||||
|       ? html`<h1> | ||||
|             ${this.localize("ui.panel.page-onboarding.restore.in_progress")} | ||||
|           </h1> | ||||
|           <onboarding-loading></onboarding-loading>` | ||||
|       : html` | ||||
|           <h1>${this.localize("ui.panel.page-onboarding.restore.header")}</h1> | ||||
|           <ha-button unelevated @click=${this._uploadBackup}> | ||||
|             ${this.localize("ui.panel.page-onboarding.restore.upload_backup")} | ||||
|           </ha-button> | ||||
|         `; | ||||
|     return html`${this._restoring | ||||
|         ? html`<h1> | ||||
|               ${this.localize("ui.panel.page-onboarding.restore.in_progress")} | ||||
|             </h1> | ||||
|             <onboarding-loading></onboarding-loading>` | ||||
|         : html` <h1> | ||||
|               ${this.localize("ui.panel.page-onboarding.restore.header")} | ||||
|             </h1> | ||||
|             <hassio-upload-backup | ||||
|               @backup-uploaded=${this._backupUploaded} | ||||
|               .hass=${this.hass} | ||||
|             ></hassio-upload-backup>`} | ||||
|       <div class="footer"> | ||||
|         <mwc-button @click=${this._back} .disabled=${this._restoring}> | ||||
|           ${this.localize("ui.panel.page-onboarding.back")} | ||||
|         </mwc-button> | ||||
|       </div> `; | ||||
|   } | ||||
|  | ||||
|   private _uploadBackup(): void { | ||||
|     showBackupUploadDialog(this, { | ||||
|       showBackup: (slug: string) => this._showBackupDialog(slug), | ||||
|       onboarding: true, | ||||
|     }); | ||||
|   private _back(): void { | ||||
|     navigate(`${location.pathname}?${removeSearchParam("page")}`); | ||||
|   } | ||||
|  | ||||
|   private _backupUploaded(ev) { | ||||
|     const backup = ev.detail.backup; | ||||
|     this._showBackupDialog(backup.slug); | ||||
|   } | ||||
|  | ||||
|   protected firstUpdated(changedProps) { | ||||
| @@ -76,6 +89,13 @@ class OnboardingRestoreBackup extends LitElement { | ||||
|           flex-direction: column; | ||||
|           align-items: center; | ||||
|         } | ||||
|         hassio-upload-backup { | ||||
|           width: 100%; | ||||
|         } | ||||
|         .footer { | ||||
|           width: 100%; | ||||
|           text-align: left; | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -17,6 +17,8 @@ class OnboardingWelcomeLink extends LitElement { | ||||
|  | ||||
|   @property() public iconPath!: string; | ||||
|  | ||||
|   @property({ attribute: true, type: Boolean }) public noninteractive?: boolean; | ||||
|  | ||||
|   @queryAsync("mwc-ripple") private _ripple!: Promise<Ripple | null>; | ||||
|  | ||||
|   @state() private _shouldRenderRipple = false; | ||||
| @@ -24,6 +26,7 @@ class OnboardingWelcomeLink extends LitElement { | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <ha-card | ||||
|         .tabIndex=${this.noninteractive ? "-1" : "0"} | ||||
|         @focus=${this.handleRippleFocus} | ||||
|         @blur=${this.handleRippleBlur} | ||||
|         @mousedown=${this.handleRippleActivate} | ||||
| @@ -33,6 +36,7 @@ class OnboardingWelcomeLink extends LitElement { | ||||
|         @touchstart=${this.handleRippleActivate} | ||||
|         @touchend=${this.handleRippleDeactivate} | ||||
|         @touchcancel=${this.handleRippleDeactivate} | ||||
|         @keydown=${this._handleKeyDown} | ||||
|       > | ||||
|         <ha-svg-icon .path=${this.iconPath}></ha-svg-icon> | ||||
|         ${this.label} | ||||
| @@ -41,6 +45,12 @@ class OnboardingWelcomeLink extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _handleKeyDown(ev: KeyboardEvent): void { | ||||
|     if (ev.key === "Enter" || ev.key === " ") { | ||||
|       (ev.target as HTMLElement).click(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _rippleHandlers: RippleHandlers = new RippleHandlers(() => { | ||||
|     this._shouldRenderRipple = true; | ||||
|     return this._ripple; | ||||
| @@ -84,6 +94,7 @@ class OnboardingWelcomeLink extends LitElement { | ||||
|         text-align: center; | ||||
|         font-weight: 500; | ||||
|         padding: 32px 16px; | ||||
|         height: 100%; | ||||
|       } | ||||
|       ha-svg-icon { | ||||
|         color: var(--text-primary-color); | ||||
|   | ||||
| @@ -1,5 +1,12 @@ | ||||
| import { mdiAccountGroup, mdiFileDocument, mdiTabletCellphone } from "@mdi/js"; | ||||
| import { CSSResultGroup, LitElement, TemplateResult, css, html } from "lit"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import type { LocalizeFunc } from "../common/translations/localize"; | ||||
| import "../components/ha-card"; | ||||
| @@ -14,6 +21,8 @@ class OnboardingWelcomeLinks extends LitElement { | ||||
|  | ||||
|   @property() public localize!: LocalizeFunc; | ||||
|  | ||||
|   @property({ type: Boolean }) public mobileApp!: boolean; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html`<a | ||||
|         target="_blank" | ||||
| @@ -21,6 +30,7 @@ class OnboardingWelcomeLinks extends LitElement { | ||||
|         href="https://www.home-assistant.io/blog/2016/01/19/perfect-home-automation/" | ||||
|       > | ||||
|         <onboarding-welcome-link | ||||
|           noninteractive | ||||
|           .iconPath=${mdiFileDocument} | ||||
|           .label=${this.localize("ui.panel.page-onboarding.welcome.vision")} | ||||
|         > | ||||
| @@ -33,13 +43,17 @@ class OnboardingWelcomeLinks extends LitElement { | ||||
|         .label=${this.localize("ui.panel.page-onboarding.welcome.community")} | ||||
|       > | ||||
|       </onboarding-welcome-link> | ||||
|       <onboarding-welcome-link | ||||
|         class="app" | ||||
|         @click=${this._openApp} | ||||
|         .iconPath=${mdiTabletCellphone} | ||||
|         .label=${this.localize("ui.panel.page-onboarding.welcome.download_app")} | ||||
|       > | ||||
|       </onboarding-welcome-link>`; | ||||
|       ${this.mobileApp | ||||
|         ? nothing | ||||
|         : html`<onboarding-welcome-link | ||||
|             class="app" | ||||
|             @click=${this._openApp} | ||||
|             .iconPath=${mdiTabletCellphone} | ||||
|             .label=${this.localize( | ||||
|               "ui.panel.page-onboarding.welcome.download_app" | ||||
|             )} | ||||
|           > | ||||
|           </onboarding-welcome-link>`}`; | ||||
|   } | ||||
|  | ||||
|   private _openCommunity(): void { | ||||
|   | ||||
| @@ -80,9 +80,7 @@ export class DialogAddApplicationCredential extends LitElement { | ||||
|       name: domainToName(this.hass.localize, domain), | ||||
|     })); | ||||
|     await this.hass.loadBackendTranslation("application_credentials"); | ||||
|     if (this._domain) { | ||||
|       this._updateDescription(); | ||||
|     } | ||||
|     this._updateDescription(); | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
| @@ -265,11 +263,15 @@ export class DialogAddApplicationCredential extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private async _updateDescription() { | ||||
|     if (!this._domain) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     await this.hass.loadBackendTranslation( | ||||
|       "application_credentials", | ||||
|       this._domain | ||||
|     ); | ||||
|     const info = this._config!.integrations[this._domain!]; | ||||
|     const info = this._config!.integrations[this._domain]; | ||||
|     this._description = this.hass.localize( | ||||
|       `component.${this._domain}.application_credentials.description`, | ||||
|       info.description_placeholders | ||||
|   | ||||
| @@ -62,17 +62,16 @@ export class HaConfigApplicationCredentials extends LitElement { | ||||
|           ), | ||||
|           direction: "asc", | ||||
|           grows: true, | ||||
|           template: (_, entry: ApplicationCredential) => html`${entry.name}`, | ||||
|           template: (entry) => html`${entry.name}`, | ||||
|         }, | ||||
|         clientId: { | ||||
|         client_id: { | ||||
|           title: localize( | ||||
|             "ui.panel.config.application_credentials.picker.headers.client_id" | ||||
|           ), | ||||
|           width: "30%", | ||||
|           direction: "asc", | ||||
|           hidden: narrow, | ||||
|           template: (_, entry: ApplicationCredential) => | ||||
|             html`${entry.client_id}`, | ||||
|           template: (entry) => html`${entry.client_id}`, | ||||
|         }, | ||||
|         application: { | ||||
|           title: localize( | ||||
| @@ -81,7 +80,7 @@ export class HaConfigApplicationCredentials extends LitElement { | ||||
|           sortable: true, | ||||
|           width: "30%", | ||||
|           direction: "asc", | ||||
|           template: (_, entry) => html`${domainToName(localize, entry.domain)}`, | ||||
|           template: (entry) => html`${domainToName(localize, entry.domain)}`, | ||||
|         }, | ||||
|       }; | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import { ActionDetail } from "@material/mwc-list/mwc-list-foundation"; | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import { | ||||
| @@ -25,7 +26,6 @@ import { | ||||
| } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { consume } from "@lit-labs/context"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
| import { dynamicElement } from "../../../../common/dom/dynamic-element-directive"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| @@ -40,6 +40,7 @@ import type { HaYamlEditor } from "../../../../components/ha-yaml-editor"; | ||||
| import { ACTION_TYPES, YAML_ONLY_ACTION_TYPES } from "../../../../data/action"; | ||||
| import { AutomationClipboard } from "../../../../data/automation"; | ||||
| import { validateConfig } from "../../../../data/config"; | ||||
| import { fullEntitiesContext } from "../../../../data/context"; | ||||
| import { EntityRegistryEntry } from "../../../../data/entity_registry"; | ||||
| import { | ||||
|   Action, | ||||
| @@ -70,19 +71,20 @@ import "./types/ha-automation-action-service"; | ||||
| import "./types/ha-automation-action-stop"; | ||||
| import "./types/ha-automation-action-wait_for_trigger"; | ||||
| import "./types/ha-automation-action-wait_template"; | ||||
| import { fullEntitiesContext } from "../../../../data/context"; | ||||
|  | ||||
| export const getType = (action: Action | undefined) => { | ||||
|   if (!action) { | ||||
|     return undefined; | ||||
|   } | ||||
|   if ("service" in action || "scene" in action) { | ||||
|     return getActionType(action); | ||||
|     return getActionType(action) as "activate_scene" | "service" | "play_media"; | ||||
|   } | ||||
|   if (["and", "or", "not"].some((key) => key in action)) { | ||||
|     return "condition"; | ||||
|     return "condition" as const; | ||||
|   } | ||||
|   return Object.keys(ACTION_TYPES).find((option) => option in action); | ||||
|   return Object.keys(ACTION_TYPES).find( | ||||
|     (option) => option in action | ||||
|   ) as keyof typeof ACTION_TYPES; | ||||
| }; | ||||
|  | ||||
| export interface ActionElement extends LitElement { | ||||
|   | ||||
| @@ -3,41 +3,42 @@ import type { ActionDetail } from "@material/mwc-list"; | ||||
| import { | ||||
|   mdiArrowDown, | ||||
|   mdiArrowUp, | ||||
|   mdiContentPaste, | ||||
|   mdiDrag, | ||||
|   mdiPlus, | ||||
|   mdiContentPaste, | ||||
| } from "@mdi/js"; | ||||
| import deepClone from "deep-clone-simple"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import type { SortableEvent } from "sortablejs"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stringCompare } from "../../../../common/string/compare"; | ||||
| import { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-button-menu"; | ||||
| import "../../../../components/ha-button"; | ||||
| import "../../../../components/ha-button-menu"; | ||||
| import type { HaSelect } from "../../../../components/ha-select"; | ||||
| import "../../../../components/ha-svg-icon"; | ||||
| import { ACTION_TYPES } from "../../../../data/action"; | ||||
| import { Action } from "../../../../data/script"; | ||||
| import { AutomationClipboard } from "../../../../data/automation"; | ||||
| import { Action } from "../../../../data/script"; | ||||
| import { sortableStyles } from "../../../../resources/ha-sortable-style"; | ||||
| import { | ||||
|   loadSortable, | ||||
|   SortableInstance, | ||||
|   loadSortable, | ||||
| } from "../../../../resources/sortable.ondemand"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { getType } from "./ha-automation-action-row"; | ||||
| import { Entries, HomeAssistant } from "../../../../types"; | ||||
| import type HaAutomationActionRow from "./ha-automation-action-row"; | ||||
| import { getType } from "./ha-automation-action-row"; | ||||
| import "./types/ha-automation-action-activate_scene"; | ||||
| import "./types/ha-automation-action-choose"; | ||||
| import "./types/ha-automation-action-condition"; | ||||
| @@ -52,7 +53,6 @@ import "./types/ha-automation-action-service"; | ||||
| import "./types/ha-automation-action-stop"; | ||||
| import "./types/ha-automation-action-wait_for_trigger"; | ||||
| import "./types/ha-automation-action-wait_template"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
|  | ||||
| const PASTE_VALUE = "__paste__"; | ||||
|  | ||||
| @@ -174,9 +174,9 @@ export default class HaAutomationAction extends LitElement { | ||||
|                 "ui.panel.config.automation.editor.actions.paste" | ||||
|               )} | ||||
|               (${this.hass.localize( | ||||
|                 `ui.panel.config.automation.editor.actions.type.${getType( | ||||
|                   this._clipboard.action | ||||
|                 )}.label` | ||||
|                 `ui.panel.config.automation.editor.actions.type.${ | ||||
|                   getType(this._clipboard.action) || "unknown" | ||||
|                 }.label` | ||||
|               )}) | ||||
|               <ha-svg-icon slot="graphic" .path=${mdiContentPaste}></ha-svg-icon | ||||
|             ></mwc-list-item>` | ||||
| @@ -333,7 +333,7 @@ export default class HaAutomationAction extends LitElement { | ||||
|  | ||||
|   private _processedTypes = memoizeOne( | ||||
|     (localize: LocalizeFunc): [string, string, string][] => | ||||
|       Object.entries(ACTION_TYPES) | ||||
|       (Object.entries(ACTION_TYPES) as Entries<typeof ACTION_TYPES>) | ||||
|         .map( | ||||
|           ([action, icon]) => | ||||
|             [ | ||||
|   | ||||
| @@ -8,7 +8,7 @@ import "../../../../../components/ha-select"; | ||||
| import type { HaSelect } from "../../../../../components/ha-select"; | ||||
| import type { Condition } from "../../../../../data/automation"; | ||||
| import { CONDITION_TYPES } from "../../../../../data/condition"; | ||||
| import { HomeAssistant } from "../../../../../types"; | ||||
| import { Entries, HomeAssistant } from "../../../../../types"; | ||||
| import "../../condition/ha-automation-condition-editor"; | ||||
| import type { ActionElement } from "../ha-automation-action-row"; | ||||
|  | ||||
| @@ -55,7 +55,7 @@ export class HaConditionAction extends LitElement implements ActionElement { | ||||
|  | ||||
|   private _processedTypes = memoizeOne( | ||||
|     (localize: LocalizeFunc): [string, string, string][] => | ||||
|       Object.entries(CONDITION_TYPES) | ||||
|       (Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>) | ||||
|         .map( | ||||
|           ([condition, icon]) => | ||||
|             [ | ||||
|   | ||||
| @@ -28,12 +28,13 @@ import type { | ||||
|   AutomationClipboard, | ||||
|   Condition, | ||||
| } from "../../../../data/automation"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { Entries, HomeAssistant } from "../../../../types"; | ||||
| import "./ha-automation-condition-row"; | ||||
| import type HaAutomationConditionRow from "./ha-automation-condition-row"; | ||||
| // Uncommenting these and this element doesn't load | ||||
| // import "./types/ha-automation-condition-not"; | ||||
| // import "./types/ha-automation-condition-or"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
| import { stringCompare } from "../../../../common/string/compare"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import type { HaSelect } from "../../../../components/ha-select"; | ||||
| @@ -52,7 +53,6 @@ import "./types/ha-automation-condition-template"; | ||||
| import "./types/ha-automation-condition-time"; | ||||
| import "./types/ha-automation-condition-trigger"; | ||||
| import "./types/ha-automation-condition-zone"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
|  | ||||
| const PASTE_VALUE = "__paste__"; | ||||
|  | ||||
| @@ -364,7 +364,7 @@ export default class HaAutomationCondition extends LitElement { | ||||
|  | ||||
|   private _processedTypes = memoizeOne( | ||||
|     (localize: LocalizeFunc): [string, string, string][] => | ||||
|       Object.entries(CONDITION_TYPES) | ||||
|       (Object.entries(CONDITION_TYPES) as Entries<typeof CONDITION_TYPES>) | ||||
|         .map( | ||||
|           ([condition, icon]) => | ||||
|             [ | ||||
|   | ||||
| @@ -53,11 +53,6 @@ export class HaZoneCondition extends LitElement { | ||||
|         allow-custom-entity | ||||
|         .includeDomains=${includeDomains} | ||||
|       ></ha-entity-picker> | ||||
|       <label id="eventlabel"> | ||||
|         ${this.hass.localize( | ||||
|           "ui.panel.config.automation.editor.conditions.type.zone.event" | ||||
|         )} | ||||
|       </label> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -49,6 +49,8 @@ import { | ||||
|   showAutomationEditor, | ||||
|   triggerAutomationActions, | ||||
| } from "../../../data/automation"; | ||||
| import { validateConfig } from "../../../data/config"; | ||||
| import { UNAVAILABLE } from "../../../data/entity"; | ||||
| import { fetchEntityRegistry } from "../../../data/entity_registry"; | ||||
| import { | ||||
|   showAlertDialog, | ||||
| @@ -57,15 +59,13 @@ import { | ||||
| import "../../../layouts/hass-subpage"; | ||||
| import { KeyboardShortcutMixin } from "../../../mixins/keyboard-shortcut-mixin"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| import { HomeAssistant, Route } from "../../../types"; | ||||
| import { Entries, HomeAssistant, Route } from "../../../types"; | ||||
| import { showToast } from "../../../util/toast"; | ||||
| import "../ha-config-section"; | ||||
| import { showAutomationModeDialog } from "./automation-mode-dialog/show-dialog-automation-mode"; | ||||
| import { showAutomationRenameDialog } from "./automation-rename-dialog/show-dialog-automation-rename"; | ||||
| import "./blueprint-automation-editor"; | ||||
| import "./manual-automation-editor"; | ||||
| import { UNAVAILABLE } from "../../../data/entity"; | ||||
| import { validateConfig } from "../../../data/config"; | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
| @@ -489,7 +489,9 @@ export class HaAutomationEditor extends KeyboardShortcutMixin(LitElement) { | ||||
|       condition: this._config.condition, | ||||
|       action: this._config.action, | ||||
|     }); | ||||
|     this._validationErrors = Object.entries(validation).map(([key, value]) => | ||||
|     this._validationErrors = ( | ||||
|       Object.entries(validation) as Entries<typeof validation> | ||||
|     ).map(([key, value]) => | ||||
|       value.valid | ||||
|         ? "" | ||||
|         : html`${this.hass.localize( | ||||
|   | ||||
| @@ -55,6 +55,12 @@ import { findRelated } from "../../../data/search"; | ||||
| import { fetchBlueprints } from "../../../data/blueprint"; | ||||
| import { UNAVAILABLE } from "../../../data/entity"; | ||||
|  | ||||
| type AutomationItem = AutomationEntity & { | ||||
|   name: string; | ||||
|   last_triggered?: string | undefined; | ||||
|   disabled: boolean; | ||||
| }; | ||||
|  | ||||
| @customElement("ha-automation-picker") | ||||
| class HaAutomationPicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -79,7 +85,7 @@ class HaAutomationPicker extends LitElement { | ||||
|     ( | ||||
|       automations: AutomationEntity[], | ||||
|       filteredAutomations?: string[] | null | ||||
|     ) => { | ||||
|     ): AutomationItem[] => { | ||||
|       if (filteredAutomations === null) { | ||||
|         return []; | ||||
|       } | ||||
| @@ -100,14 +106,14 @@ class HaAutomationPicker extends LitElement { | ||||
|  | ||||
|   private _columns = memoizeOne( | ||||
|     (narrow: boolean, _locale): DataTableColumnContainer => { | ||||
|       const columns: DataTableColumnContainer = { | ||||
|       const columns: DataTableColumnContainer<AutomationItem> = { | ||||
|         icon: { | ||||
|           title: "", | ||||
|           label: this.hass.localize( | ||||
|             "ui.panel.config.automation.picker.headers.state" | ||||
|           ), | ||||
|           type: "icon", | ||||
|           template: (_, automation) => | ||||
|           template: (automation) => | ||||
|             html`<ha-state-icon | ||||
|               .state=${automation} | ||||
|               style=${styleMap({ | ||||
| @@ -128,12 +134,12 @@ class HaAutomationPicker extends LitElement { | ||||
|           direction: "asc", | ||||
|           grows: true, | ||||
|           template: narrow | ||||
|             ? (name, automation: any) => { | ||||
|             ? (automation) => { | ||||
|                 const date = new Date(automation.attributes.last_triggered); | ||||
|                 const now = new Date(); | ||||
|                 const dayDifference = differenceInDays(now, date); | ||||
|                 return html` | ||||
|                   ${name} | ||||
|                   ${automation.name} | ||||
|                   <div class="secondary"> | ||||
|                     ${this.hass.localize("ui.card.automation.last_triggered")}: | ||||
|                     ${automation.attributes.last_triggered | ||||
| @@ -156,20 +162,17 @@ class HaAutomationPicker extends LitElement { | ||||
|           sortable: true, | ||||
|           width: "20%", | ||||
|           title: this.hass.localize("ui.card.automation.last_triggered"), | ||||
|           template: (last_triggered) => { | ||||
|             const date = new Date(last_triggered); | ||||
|           template: (automation) => { | ||||
|             if (!automation.last_triggered) { | ||||
|               return this.hass.localize("ui.components.relative_time.never"); | ||||
|             } | ||||
|             const date = new Date(automation.last_triggered); | ||||
|             const now = new Date(); | ||||
|             const dayDifference = differenceInDays(now, date); | ||||
|             return html` | ||||
|               ${last_triggered | ||||
|                 ? dayDifference > 3 | ||||
|                   ? formatShortDateTime( | ||||
|                       date, | ||||
|                       this.hass.locale, | ||||
|                       this.hass.config | ||||
|                     ) | ||||
|                   : relativeTime(date, this.hass.locale) | ||||
|                 : this.hass.localize("ui.components.relative_time.never")} | ||||
|               ${dayDifference > 3 | ||||
|                 ? formatShortDateTime(date, this.hass.locale, this.hass.config) | ||||
|                 : relativeTime(date, this.hass.locale)} | ||||
|             `; | ||||
|           }, | ||||
|         }; | ||||
| @@ -178,8 +181,8 @@ class HaAutomationPicker extends LitElement { | ||||
|       columns.disabled = this.narrow | ||||
|         ? { | ||||
|             title: "", | ||||
|             template: (disabled: boolean) => | ||||
|               disabled | ||||
|             template: (automation) => | ||||
|               automation.disabled | ||||
|                 ? html` | ||||
|                     <simple-tooltip animation-delay="0" position="left"> | ||||
|                       ${this.hass.localize( | ||||
| @@ -196,8 +199,8 @@ class HaAutomationPicker extends LitElement { | ||||
|         : { | ||||
|             width: "20%", | ||||
|             title: "", | ||||
|             template: (disabled: boolean) => | ||||
|               disabled | ||||
|             template: (automation) => | ||||
|               automation.disabled | ||||
|                 ? html` | ||||
|                     <ha-chip> | ||||
|                       ${this.hass.localize( | ||||
| @@ -212,7 +215,7 @@ class HaAutomationPicker extends LitElement { | ||||
|         title: "", | ||||
|         width: this.narrow ? undefined : "10%", | ||||
|         type: "overflow-menu", | ||||
|         template: (_: string, automation: any) => html` | ||||
|         template: (automation) => html` | ||||
|           <ha-icon-overflow-menu | ||||
|             .hass=${this.hass} | ||||
|             narrow | ||||
|   | ||||
| @@ -1,273 +0,0 @@ | ||||
| import "@material/mwc-button"; | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/ha-circular-progress"; | ||||
| import "../../../../components/ha-dialog"; | ||||
| import "../../../../components/ha-textfield"; | ||||
| import type { HaTextField } from "../../../../components/ha-textfield"; | ||||
| import type { AutomationConfig } from "../../../../data/automation"; | ||||
| import { convertThingTalk } from "../../../../data/cloud"; | ||||
| import { haStyle, haStyleDialog } from "../../../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import "./ha-thingtalk-placeholders"; | ||||
| import type { PlaceholderValues } from "./ha-thingtalk-placeholders"; | ||||
| import type { ThingtalkDialogParams } from "./show-dialog-thingtalk"; | ||||
|  | ||||
| export interface Placeholder { | ||||
|   name: string; | ||||
|   index: number; | ||||
|   fields: string[]; | ||||
|   domains: string[]; | ||||
|   device_classes?: string[]; | ||||
| } | ||||
|  | ||||
| export interface PlaceholderContainer { | ||||
|   [key: string]: Placeholder[]; | ||||
| } | ||||
|  | ||||
| @customElement("ha-dialog-thinktalk") | ||||
| class DialogThingtalk extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   @state() private _params?: ThingtalkDialogParams; | ||||
|  | ||||
|   @state() private _submitting = false; | ||||
|  | ||||
|   @state() private _placeholders?: PlaceholderContainer; | ||||
|  | ||||
|   @query("#input") private _input?: HaTextField; | ||||
|  | ||||
|   private _value?: string; | ||||
|  | ||||
|   private _config!: Partial<AutomationConfig>; | ||||
|  | ||||
|   public async showDialog(params: ThingtalkDialogParams): Promise<void> { | ||||
|     this._params = params; | ||||
|     this._error = undefined; | ||||
|     if (params.input) { | ||||
|       this._value = params.input; | ||||
|       await this.updateComplete; | ||||
|       this._generate(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public closeDialog() { | ||||
|     this._placeholders = undefined; | ||||
|     this._params = undefined; | ||||
|     if (this._input) { | ||||
|       this._input.value = ""; | ||||
|     } | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
|   public closeInitDialog() { | ||||
|     if (this._placeholders) { | ||||
|       return; | ||||
|     } | ||||
|     this.closeDialog(); | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this._params) { | ||||
|       return nothing; | ||||
|     } | ||||
|     if (this._placeholders) { | ||||
|       return html` | ||||
|         <ha-thingtalk-placeholders | ||||
|           .hass=${this.hass} | ||||
|           .placeholders=${this._placeholders} | ||||
|           .skip=${this._skip} | ||||
|           @closed=${this.closeDialog} | ||||
|           @placeholders-filled=${this._handlePlaceholders} | ||||
|         > | ||||
|         </ha-thingtalk-placeholders> | ||||
|       `; | ||||
|     } | ||||
|     return html` | ||||
|       <ha-dialog | ||||
|         open | ||||
|         @closed=${this.closeInitDialog} | ||||
|         .heading=${this.hass.localize( | ||||
|           `ui.panel.config.automation.thingtalk.task_selection.header` | ||||
|         )} | ||||
|       > | ||||
|         <div> | ||||
|           ${this._error ? html` <div class="error">${this._error}</div> ` : ""} | ||||
|           ${this.hass.localize( | ||||
|             `ui.panel.config.automation.thingtalk.task_selection.introduction` | ||||
|           )}<br /><br /> | ||||
|           ${this.hass.localize( | ||||
|             `ui.panel.config.automation.thingtalk.task_selection.language_note` | ||||
|           )}<br /><br /> | ||||
|           ${this.hass.localize( | ||||
|             `ui.panel.config.automation.thingtalk.task_selection.for_example` | ||||
|           )} | ||||
|           <ul @click=${this._handleExampleClick}> | ||||
|             <li> | ||||
|               <button class="link"> | ||||
|                 Turn off the lights when I leave home | ||||
|               </button> | ||||
|             </li> | ||||
|             <li> | ||||
|               <button class="link"> | ||||
|                 Turn on the lights when the sun is set | ||||
|               </button> | ||||
|             </li> | ||||
|             <li> | ||||
|               <button class="link"> | ||||
|                 Notify me if the door opens and I am not at home | ||||
|               </button> | ||||
|             </li> | ||||
|             <li> | ||||
|               <button class="link"> | ||||
|                 Turn the light on when motion is detected | ||||
|               </button> | ||||
|             </li> | ||||
|           </ul> | ||||
|           <ha-textfield | ||||
|             id="input" | ||||
|             label="What should this automation do?" | ||||
|             .value=${this._value} | ||||
|             autofocus | ||||
|             @keyup=${this._handleKeyUp} | ||||
|           ></ha-textfield> | ||||
|           <a | ||||
|             href="https://almond.stanford.edu/" | ||||
|             target="_blank" | ||||
|             rel="noreferrer" | ||||
|             class="attribution" | ||||
|             >Powered by Almond</a | ||||
|           > | ||||
|         </div> | ||||
|         <mwc-button class="left" @click=${this._skip} slot="secondaryAction"> | ||||
|           ${this.hass.localize(`ui.common.skip`)} | ||||
|         </mwc-button> | ||||
|         <mwc-button | ||||
|           @click=${this._generate} | ||||
|           .disabled=${this._submitting} | ||||
|           slot="primaryAction" | ||||
|         > | ||||
|           ${this._submitting | ||||
|             ? html`<ha-circular-progress | ||||
|                 active | ||||
|                 size="small" | ||||
|                 title="Creating your automation..." | ||||
|               ></ha-circular-progress>` | ||||
|             : ""} | ||||
|           ${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)} | ||||
|         </mwc-button> | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private async _generate() { | ||||
|     this._value = this._input!.value as string; | ||||
|     if (!this._value) { | ||||
|       this._error = this.hass.localize( | ||||
|         `ui.panel.config.automation.thingtalk.task_selection.error_empty` | ||||
|       ); | ||||
|       return; | ||||
|     } | ||||
|     this._submitting = true; | ||||
|     let config: Partial<AutomationConfig>; | ||||
|     let placeholders: PlaceholderContainer; | ||||
|     try { | ||||
|       const result = await convertThingTalk(this.hass, this._value); | ||||
|       config = result.config; | ||||
|       placeholders = result.placeholders; | ||||
|     } catch (err: any) { | ||||
|       this._error = err.message; | ||||
|       this._submitting = false; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._submitting = false; | ||||
|  | ||||
|     if (!Object.keys(config).length) { | ||||
|       this._error = this.hass.localize( | ||||
|         `ui.panel.config.automation.thingtalk.task_selection.error_unsupported` | ||||
|       ); | ||||
|     } else if (Object.keys(placeholders).length) { | ||||
|       this._config = config; | ||||
|       this._placeholders = placeholders; | ||||
|     } else { | ||||
|       this._sendConfig(this._value, config); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _handlePlaceholders(ev: CustomEvent) { | ||||
|     const placeholderValues = ev.detail.value as PlaceholderValues; | ||||
|     Object.entries(placeholderValues).forEach(([type, values]) => { | ||||
|       Object.entries(values).forEach(([index, placeholder]) => { | ||||
|         const devices = Object.values(placeholder); | ||||
|         if (devices.length === 1) { | ||||
|           Object.entries(devices[0]).forEach(([field, value]) => { | ||||
|             this._config[type][index][field] = value; | ||||
|           }); | ||||
|           return; | ||||
|         } | ||||
|         const automation = { ...this._config[type][index] }; | ||||
|         const newAutomations: any[] = []; | ||||
|         devices.forEach((fields) => { | ||||
|           const newAutomation = { ...automation }; | ||||
|           Object.entries(fields).forEach(([field, value]) => { | ||||
|             newAutomation[field] = value; | ||||
|           }); | ||||
|           newAutomations.push(newAutomation); | ||||
|         }); | ||||
|         this._config[type].splice(index, 1, ...newAutomations); | ||||
|       }); | ||||
|     }); | ||||
|     this._sendConfig(this._value, this._config); | ||||
|   } | ||||
|  | ||||
|   private _sendConfig(input, config) { | ||||
|     this._params!.callback({ alias: input, ...config }); | ||||
|     this.closeDialog(); | ||||
|   } | ||||
|  | ||||
|   private _skip = () => { | ||||
|     this._params!.callback(undefined); | ||||
|     this.closeDialog(); | ||||
|   }; | ||||
|  | ||||
|   private _handleKeyUp(ev: KeyboardEvent) { | ||||
|     if (ev.key === "Enter") { | ||||
|       this._generate(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _handleExampleClick(ev: Event) { | ||||
|     this._input!.value = (ev.target as HTMLAnchorElement).innerText; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyle, | ||||
|       haStyleDialog, | ||||
|       css` | ||||
|         ha-dialog { | ||||
|           max-width: 500px; | ||||
|         } | ||||
|         mwc-button.left { | ||||
|           margin-right: auto; | ||||
|         } | ||||
|         .error { | ||||
|           color: var(--error-color); | ||||
|         } | ||||
|         .attribution { | ||||
|           color: var(--secondary-text-color); | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-dialog-thinktalk": DialogThingtalk; | ||||
|   } | ||||
| } | ||||
| @@ -1,483 +0,0 @@ | ||||
| /* eslint-disable lit/no-template-arrow */ | ||||
| import { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
| import { applyPatch, getPath } from "../../../../common/util/patch"; | ||||
| import "../../../../components/device/ha-area-devices-picker"; | ||||
| import "../../../../components/entity/ha-entity-picker"; | ||||
| import { | ||||
|   AreaRegistryEntry, | ||||
|   subscribeAreaRegistry, | ||||
| } from "../../../../data/area_registry"; | ||||
| import { | ||||
|   DeviceRegistryEntry, | ||||
|   subscribeDeviceRegistry, | ||||
| } from "../../../../data/device_registry"; | ||||
| import { subscribeEntityRegistry } from "../../../../data/entity_registry"; | ||||
| import { domainToName } from "../../../../data/integration"; | ||||
| import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; | ||||
| import { haStyleDialog } from "../../../../resources/styles"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { Placeholder, PlaceholderContainer } from "./dialog-thingtalk"; | ||||
|  | ||||
| declare global { | ||||
|   // for fire event | ||||
|   interface HASSDomEvents { | ||||
|     "placeholders-filled": { value: PlaceholderValues }; | ||||
|   } | ||||
| } | ||||
|  | ||||
| export interface PlaceholderValues { | ||||
|   [key: string]: { | ||||
|     [index: number]: { | ||||
|       [index: number]: { device_id?: string; entity_id?: string }; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export interface ExtraInfo { | ||||
|   [key: string]: { | ||||
|     [index: number]: { | ||||
|       [index: number]: { | ||||
|         area_id?: string; | ||||
|         device_ids?: string[]; | ||||
|         manualEntity: boolean; | ||||
|       }; | ||||
|     }; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| interface DeviceEntitiesLookup { | ||||
|   [deviceId: string]: string[]; | ||||
| } | ||||
|  | ||||
| @customElement("ha-thingtalk-placeholders") | ||||
| export class ThingTalkPlaceholders extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public opened!: boolean; | ||||
|  | ||||
|   public skip!: () => void; | ||||
|  | ||||
|   @property() public placeholders!: PlaceholderContainer; | ||||
|  | ||||
|   @state() private _error?: string; | ||||
|  | ||||
|   private _deviceEntityLookup: DeviceEntitiesLookup = {}; | ||||
|  | ||||
|   @state() private _extraInfo: ExtraInfo = {}; | ||||
|  | ||||
|   @state() private _placeholderValues: PlaceholderValues = {}; | ||||
|  | ||||
|   private _devices?: DeviceRegistryEntry[]; | ||||
|  | ||||
|   private _areas?: AreaRegistryEntry[]; | ||||
|  | ||||
|   private _search = false; | ||||
|  | ||||
|   public hassSubscribe() { | ||||
|     return [ | ||||
|       subscribeEntityRegistry(this.hass.connection, (entries) => { | ||||
|         for (const entity of entries) { | ||||
|           if (!entity.device_id) { | ||||
|             continue; | ||||
|           } | ||||
|           if (!(entity.device_id in this._deviceEntityLookup)) { | ||||
|             this._deviceEntityLookup[entity.device_id] = []; | ||||
|           } | ||||
|           if ( | ||||
|             !this._deviceEntityLookup[entity.device_id].includes( | ||||
|               entity.entity_id | ||||
|             ) | ||||
|           ) { | ||||
|             this._deviceEntityLookup[entity.device_id].push(entity.entity_id); | ||||
|           } | ||||
|         } | ||||
|       }), | ||||
|       subscribeDeviceRegistry(this.hass.connection!, (devices) => { | ||||
|         this._devices = devices; | ||||
|         this._searchNames(); | ||||
|       }), | ||||
|       subscribeAreaRegistry(this.hass.connection!, (areas) => { | ||||
|         this._areas = areas; | ||||
|         this._searchNames(); | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if (changedProps.has("placeholders")) { | ||||
|       this._search = true; | ||||
|       this._searchNames(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <ha-dialog | ||||
|         open | ||||
|         scrimClickAction | ||||
|         .heading=${this.hass.localize( | ||||
|           `ui.panel.config.automation.thingtalk.link_devices.header` | ||||
|         )} | ||||
|       > | ||||
|         <div> | ||||
|           ${this._error ? html` <div class="error">${this._error}</div> ` : ""} | ||||
|           ${Object.entries(this.placeholders).map( | ||||
|             ([type, placeholders]) => html` | ||||
|               <h3> | ||||
|                 ${this.hass.localize( | ||||
|                   `ui.panel.config.automation.editor.${type}s.name` | ||||
|                 )}: | ||||
|               </h3> | ||||
|               ${placeholders.map((placeholder) => { | ||||
|                 if (placeholder.fields.includes("device_id")) { | ||||
|                   const extraInfo = getPath(this._extraInfo, [ | ||||
|                     type, | ||||
|                     placeholder.index, | ||||
|                   ]); | ||||
|                   return html` | ||||
|                     <ha-area-devices-picker | ||||
|                       .type=${type} | ||||
|                       .placeholder=${placeholder} | ||||
|                       @value-changed=${this._devicePicked} | ||||
|                       .hass=${this.hass} | ||||
|                       .area=${extraInfo ? extraInfo.area_id : undefined} | ||||
|                       .devices=${extraInfo && extraInfo.device_ids | ||||
|                         ? extraInfo.device_ids | ||||
|                         : undefined} | ||||
|                       .includeDomains=${placeholder.domains} | ||||
|                       .includeDeviceClasses=${placeholder.device_classes} | ||||
|                       .label=${this._getLabel( | ||||
|                         placeholder.domains, | ||||
|                         placeholder.device_classes | ||||
|                       )} | ||||
|                     ></ha-area-devices-picker> | ||||
|                     ${extraInfo && extraInfo.manualEntity | ||||
|                       ? html` | ||||
|                           <h3> | ||||
|                             ${this.hass.localize( | ||||
|                               `ui.panel.config.automation.thingtalk.link_devices.ambiguous_entities` | ||||
|                             )} | ||||
|                           </h3> | ||||
|                           ${Object.keys(extraInfo.manualEntity).map( | ||||
|                             (idx) => html` | ||||
|                               <ha-entity-picker | ||||
|                                 id="device-entity-picker" | ||||
|                                 .type=${type} | ||||
|                                 .placeholder=${placeholder} | ||||
|                                 .index=${idx} | ||||
|                                 @change=${this._entityPicked} | ||||
|                                 .includeDomains=${placeholder.domains} | ||||
|                                 .includeDeviceClasses=${placeholder.device_classes} | ||||
|                                 .hass=${this.hass} | ||||
|                                 .label=${`${this._getLabel( | ||||
|                                   placeholder.domains, | ||||
|                                   placeholder.device_classes | ||||
|                                 )} of device ${this._getDeviceName( | ||||
|                                   getPath(this._placeholderValues, [ | ||||
|                                     type, | ||||
|                                     placeholder.index, | ||||
|                                     idx, | ||||
|                                     "device_id", | ||||
|                                   ]) | ||||
|                                 )}`} | ||||
|                                 .entityFilter=${(entityState: HassEntity) => { | ||||
|                                   const devId = | ||||
|                                     this._placeholderValues[type][ | ||||
|                                       placeholder.index | ||||
|                                     ][idx].device_id; | ||||
|                                   return this._deviceEntityLookup[ | ||||
|                                     devId | ||||
|                                   ].includes(entityState.entity_id); | ||||
|                                 }} | ||||
|                               ></ha-entity-picker> | ||||
|                             ` | ||||
|                           )} | ||||
|                         ` | ||||
|                       : ""} | ||||
|                   `; | ||||
|                 } | ||||
|                 if (placeholder.fields.includes("entity_id")) { | ||||
|                   return html` | ||||
|                     <ha-entity-picker | ||||
|                       .type=${type} | ||||
|                       .placeholder=${placeholder} | ||||
|                       @change=${this._entityPicked} | ||||
|                       .includeDomains=${placeholder.domains} | ||||
|                       .includeDeviceClasses=${placeholder.device_classes} | ||||
|                       .hass=${this.hass} | ||||
|                       .label=${this._getLabel( | ||||
|                         placeholder.domains, | ||||
|                         placeholder.device_classes | ||||
|                       )} | ||||
|                     ></ha-entity-picker> | ||||
|                   `; | ||||
|                 } | ||||
|                 return html` | ||||
|                   <div class="error"> | ||||
|                     ${this.hass.localize( | ||||
|                       `ui.panel.config.automation.thingtalk.link_devices.unknown_placeholder` | ||||
|                     )}<br /> | ||||
|                     ${placeholder.domains}<br /> | ||||
|                     ${placeholder.fields.map((field) => html` ${field}<br /> `)} | ||||
|                   </div> | ||||
|                 `; | ||||
|               })} | ||||
|             ` | ||||
|           )} | ||||
|         </div> | ||||
|         <mwc-button @click=${this.skip} slot="secondaryAction"> | ||||
|           ${this.hass.localize(`ui.common.skip`)} | ||||
|         </mwc-button> | ||||
|         <mwc-button | ||||
|           @click=${this._done} | ||||
|           .disabled=${!this._isDone} | ||||
|           slot="primaryAction" | ||||
|         > | ||||
|           ${this.hass.localize(`ui.panel.config.automation.thingtalk.create`)} | ||||
|         </mwc-button> | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _getDeviceName(deviceId: string): string { | ||||
|     if (!this._devices) { | ||||
|       return ""; | ||||
|     } | ||||
|     const foundDevice = this._devices.find((device) => device.id === deviceId); | ||||
|     if (!foundDevice) { | ||||
|       return ""; | ||||
|     } | ||||
|     return foundDevice.name_by_user || foundDevice.name || ""; | ||||
|   } | ||||
|  | ||||
|   private _searchNames() { | ||||
|     if (!this._search || !this._areas || !this._devices) { | ||||
|       return; | ||||
|     } | ||||
|     this._search = false; | ||||
|     Object.entries(this.placeholders).forEach(([type, placeholders]) => | ||||
|       placeholders.forEach((placeholder) => { | ||||
|         if (!placeholder.name) { | ||||
|           return; | ||||
|         } | ||||
|         const name = placeholder.name; | ||||
|         const foundArea = this._areas!.find((area) => | ||||
|           area.name.toLowerCase().includes(name) | ||||
|         ); | ||||
|         if (foundArea) { | ||||
|           applyPatch( | ||||
|             this._extraInfo, | ||||
|             [type, placeholder.index, "area_id"], | ||||
|             foundArea.area_id | ||||
|           ); | ||||
|           this.requestUpdate("_extraInfo"); | ||||
|           return; | ||||
|         } | ||||
|         const foundDevices = this._devices!.filter((device) => { | ||||
|           const deviceName = device.name_by_user || device.name; | ||||
|           if (!deviceName) { | ||||
|             return false; | ||||
|           } | ||||
|           return deviceName.toLowerCase().includes(name); | ||||
|         }); | ||||
|         if (foundDevices.length) { | ||||
|           applyPatch( | ||||
|             this._extraInfo, | ||||
|             [type, placeholder.index, "device_ids"], | ||||
|             foundDevices.map((device) => device.id) | ||||
|           ); | ||||
|           this.requestUpdate("_extraInfo"); | ||||
|         } | ||||
|       }) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private get _isDone(): boolean { | ||||
|     return Object.entries(this.placeholders).every(([type, placeholders]) => | ||||
|       placeholders.every((placeholder) => | ||||
|         placeholder.fields.every((field) => { | ||||
|           const entries: { | ||||
|             [key: number]: { device_id?: string; entity_id?: string }; | ||||
|           } = getPath(this._placeholderValues, [type, placeholder.index]); | ||||
|           if (!entries) { | ||||
|             return false; | ||||
|           } | ||||
|           const values = Object.values(entries); | ||||
|           return values.every( | ||||
|             (entry) => entry[field] !== undefined && entry[field] !== "" | ||||
|           ); | ||||
|         }) | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   private _getLabel(domains: string[], deviceClasses?: string[]) { | ||||
|     return `${domains | ||||
|       .map((domain) => domainToName(this.hass.localize, domain)) | ||||
|       .join(", ")}${ | ||||
|       deviceClasses ? ` of type ${deviceClasses.join(", ")}` : "" | ||||
|     }`; | ||||
|   } | ||||
|  | ||||
|   private _devicePicked(ev: CustomEvent): void { | ||||
|     const value: string[] = ev.detail.value; | ||||
|     if (!value) { | ||||
|       return; | ||||
|     } | ||||
|     const target = ev.target as any; | ||||
|     const placeholder = target.placeholder as Placeholder; | ||||
|     const type = target.type; | ||||
|  | ||||
|     let oldValues = getPath(this._placeholderValues, [type, placeholder.index]); | ||||
|     if (oldValues) { | ||||
|       oldValues = Object.values(oldValues); | ||||
|     } | ||||
|     const oldExtraInfo = getPath(this._extraInfo, [type, placeholder.index]); | ||||
|  | ||||
|     if (this._placeholderValues[type]) { | ||||
|       delete this._placeholderValues[type][placeholder.index]; | ||||
|     } | ||||
|  | ||||
|     if (this._extraInfo[type]) { | ||||
|       delete this._extraInfo[type][placeholder.index]; | ||||
|     } | ||||
|  | ||||
|     if (!value.length) { | ||||
|       this.requestUpdate("_placeholderValues"); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     value.forEach((deviceId, index) => { | ||||
|       let oldIndex; | ||||
|       if (oldValues) { | ||||
|         const oldDevice = oldValues.find((oldVal, idx) => { | ||||
|           oldIndex = idx; | ||||
|           return oldVal.device_id === deviceId; | ||||
|         }); | ||||
|  | ||||
|         if (oldDevice) { | ||||
|           applyPatch( | ||||
|             this._placeholderValues, | ||||
|             [type, placeholder.index, index], | ||||
|             oldDevice | ||||
|           ); | ||||
|           if (oldExtraInfo) { | ||||
|             applyPatch( | ||||
|               this._extraInfo, | ||||
|               [type, placeholder.index, index], | ||||
|               oldExtraInfo[oldIndex] | ||||
|             ); | ||||
|           } | ||||
|           return; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       applyPatch( | ||||
|         this._placeholderValues, | ||||
|         [type, placeholder.index, index, "device_id"], | ||||
|         deviceId | ||||
|       ); | ||||
|  | ||||
|       if (!placeholder.fields.includes("entity_id")) { | ||||
|         return; | ||||
|       } | ||||
|  | ||||
|       const devEntities = this._deviceEntityLookup[deviceId]; | ||||
|  | ||||
|       const entities = devEntities.filter((eid) => { | ||||
|         if (placeholder.device_classes) { | ||||
|           const stateObj = this.hass.states[eid]; | ||||
|           if (!stateObj) { | ||||
|             return false; | ||||
|           } | ||||
|           return ( | ||||
|             placeholder.domains.includes(computeDomain(eid)) && | ||||
|             stateObj.attributes.device_class && | ||||
|             placeholder.device_classes.includes( | ||||
|               stateObj.attributes.device_class | ||||
|             ) | ||||
|           ); | ||||
|         } | ||||
|         return placeholder.domains.includes(computeDomain(eid)); | ||||
|       }); | ||||
|       if (entities.length === 0) { | ||||
|         // Should not happen because we filter the device picker on domain | ||||
|         this._error = `No ${placeholder.domains | ||||
|           .map((domain) => domainToName(this.hass.localize, domain)) | ||||
|           .join(", ")} entities found in this device.`; | ||||
|       } else if (entities.length === 1) { | ||||
|         applyPatch( | ||||
|           this._placeholderValues, | ||||
|           [type, placeholder.index, index, "entity_id"], | ||||
|           entities[0] | ||||
|         ); | ||||
|         this.requestUpdate("_placeholderValues"); | ||||
|       } else { | ||||
|         delete this._placeholderValues[type][placeholder.index][index] | ||||
|           .entity_id; | ||||
|         applyPatch( | ||||
|           this._extraInfo, | ||||
|           [type, placeholder.index, "manualEntity", index], | ||||
|           true | ||||
|         ); | ||||
|         this.requestUpdate("_placeholderValues"); | ||||
|       } | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   private _entityPicked(ev: Event): void { | ||||
|     const target = ev.target as any; | ||||
|     const placeholder = target.placeholder as Placeholder; | ||||
|     const value = target.value; | ||||
|     const type = target.type; | ||||
|     const index = target.index || 0; | ||||
|     applyPatch( | ||||
|       this._placeholderValues, | ||||
|       [type, placeholder.index, index, "entity_id"], | ||||
|       value | ||||
|     ); | ||||
|     this.requestUpdate("_placeholderValues"); | ||||
|   } | ||||
|  | ||||
|   private _done(): void { | ||||
|     fireEvent(this, "placeholders-filled", { value: this._placeholderValues }); | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return [ | ||||
|       haStyleDialog, | ||||
|       css` | ||||
|         ha-dialog { | ||||
|           max-width: 500px; | ||||
|         } | ||||
|         mwc-button.left { | ||||
|           margin-right: auto; | ||||
|         } | ||||
|         h3 { | ||||
|           margin: 10px 0 0 0; | ||||
|           font-weight: 500; | ||||
|         } | ||||
|         .error { | ||||
|           color: var(--error-color); | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-thingtalk-placeholders": ThingTalkPlaceholders; | ||||
|   } | ||||
| } | ||||
| @@ -1,20 +0,0 @@ | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { AutomationConfig } from "../../../../data/automation"; | ||||
|  | ||||
| export interface ThingtalkDialogParams { | ||||
|   callback: (config: Partial<AutomationConfig> | undefined) => void; | ||||
|   input?: string; | ||||
| } | ||||
|  | ||||
| export const loadThingtalkDialog = () => import("./dialog-thingtalk"); | ||||
|  | ||||
| export const showThingtalkDialog = ( | ||||
|   element: HTMLElement, | ||||
|   dialogParams: ThingtalkDialogParams | ||||
| ): void => { | ||||
|   fireEvent(element, "show-dialog", { | ||||
|     dialogTag: "ha-dialog-thinktalk", | ||||
|     dialogImport: loadThingtalkDialog, | ||||
|     dialogParams, | ||||
|   }); | ||||
| }; | ||||
| @@ -3,39 +3,41 @@ import type { ActionDetail } from "@material/mwc-list"; | ||||
| import { | ||||
|   mdiArrowDown, | ||||
|   mdiArrowUp, | ||||
|   mdiContentPaste, | ||||
|   mdiDrag, | ||||
|   mdiPlus, | ||||
|   mdiContentPaste, | ||||
| } from "@mdi/js"; | ||||
| import deepClone from "deep-clone-simple"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
|   css, | ||||
|   html, | ||||
|   nothing, | ||||
| } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import type { SortableEvent } from "sortablejs"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { stringCompare } from "../../../../common/string/compare"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-button-menu"; | ||||
| import "../../../../components/ha-button"; | ||||
| import "../../../../components/ha-button-menu"; | ||||
| import type { HaSelect } from "../../../../components/ha-select"; | ||||
| import "../../../../components/ha-svg-icon"; | ||||
| import { Trigger, AutomationClipboard } from "../../../../data/automation"; | ||||
| import { AutomationClipboard, Trigger } from "../../../../data/automation"; | ||||
| import { TRIGGER_TYPES } from "../../../../data/trigger"; | ||||
| import { sortableStyles } from "../../../../resources/ha-sortable-style"; | ||||
| import { SortableInstance } from "../../../../resources/sortable"; | ||||
| import { loadSortable } from "../../../../resources/sortable.ondemand"; | ||||
| import { HomeAssistant } from "../../../../types"; | ||||
| import { Entries, HomeAssistant } from "../../../../types"; | ||||
| import "./ha-automation-trigger-row"; | ||||
| import type HaAutomationTriggerRow from "./ha-automation-trigger-row"; | ||||
| import "./types/ha-automation-trigger-calendar"; | ||||
| import "./types/ha-automation-trigger-conversation"; | ||||
| import "./types/ha-automation-trigger-device"; | ||||
| import "./types/ha-automation-trigger-event"; | ||||
| import "./types/ha-automation-trigger-geo_location"; | ||||
| @@ -43,7 +45,6 @@ import "./types/ha-automation-trigger-homeassistant"; | ||||
| import "./types/ha-automation-trigger-mqtt"; | ||||
| import "./types/ha-automation-trigger-numeric_state"; | ||||
| import "./types/ha-automation-trigger-persistent_notification"; | ||||
| import "./types/ha-automation-trigger-conversation"; | ||||
| import "./types/ha-automation-trigger-state"; | ||||
| import "./types/ha-automation-trigger-sun"; | ||||
| import "./types/ha-automation-trigger-tag"; | ||||
| @@ -52,7 +53,6 @@ import "./types/ha-automation-trigger-time"; | ||||
| import "./types/ha-automation-trigger-time_pattern"; | ||||
| import "./types/ha-automation-trigger-webhook"; | ||||
| import "./types/ha-automation-trigger-zone"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
|  | ||||
| const PASTE_VALUE = "__paste__"; | ||||
|  | ||||
| @@ -339,7 +339,7 @@ export default class HaAutomationTrigger extends LitElement { | ||||
|  | ||||
|   private _processedTypes = memoizeOne( | ||||
|     (localize: LocalizeFunc): [string, string, string][] => | ||||
|       Object.entries(TRIGGER_TYPES) | ||||
|       (Object.entries(TRIGGER_TYPES) as Entries<typeof TRIGGER_TYPES>) | ||||
|         .map( | ||||
|           ([action, icon]) => | ||||
|             [ | ||||
|   | ||||
| @@ -123,10 +123,17 @@ export class HaCalendarTrigger extends LitElement implements TriggerElement { | ||||
|  | ||||
|   private _computeLabelCallback = ( | ||||
|     schema: SchemaUnion<ReturnType<typeof this._schema>> | ||||
|   ): string => | ||||
|     this.hass.localize( | ||||
|       `ui.panel.config.automation.editor.triggers.type.calendar.${schema.name}` | ||||
|     ); | ||||
|   ): string => { | ||||
|     switch (schema.name) { | ||||
|       case "entity_id": | ||||
|         return this.hass.localize("ui.components.entity.entity-picker.entity"); | ||||
|       case "event": | ||||
|         return this.hass.localize( | ||||
|           "ui.panel.config.automation.editor.triggers.type.calendar.event" | ||||
|         ); | ||||
|     } | ||||
|     return ""; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -1,12 +1,12 @@ | ||||
| import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js"; | ||||
| import "@lrnwebcomponents/simple-tooltip/simple-tooltip"; | ||||
| import { mdiDelete, mdiDownload, mdiPlus } from "@mdi/js"; | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
|   css, | ||||
|   html, | ||||
| } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoize from "memoize-one"; | ||||
| @@ -48,15 +48,15 @@ class HaConfigBackup extends LitElement { | ||||
|   @state() private _backupData?: BackupData; | ||||
|  | ||||
|   private _columns = memoize( | ||||
|     (narrow, _language): DataTableColumnContainer => ({ | ||||
|     (narrow, _language): DataTableColumnContainer<BackupContent> => ({ | ||||
|       name: { | ||||
|         title: this.hass.localize("ui.panel.config.backup.name"), | ||||
|         main: true, | ||||
|         sortable: true, | ||||
|         filterable: true, | ||||
|         grows: true, | ||||
|         template: (entry: string, backup: BackupContent) => | ||||
|           html`${entry} | ||||
|         template: (backup) => | ||||
|           html`${backup.name} | ||||
|             <div class="secondary">${backup.path}</div>`, | ||||
|       }, | ||||
|       size: { | ||||
| @@ -65,7 +65,7 @@ class HaConfigBackup extends LitElement { | ||||
|         hidden: narrow, | ||||
|         filterable: true, | ||||
|         sortable: true, | ||||
|         template: (entry: number) => Math.ceil(entry * 10) / 10 + " MB", | ||||
|         template: (backup) => Math.ceil(backup.size * 10) / 10 + " MB", | ||||
|       }, | ||||
|       date: { | ||||
|         title: this.hass.localize("ui.panel.config.backup.created"), | ||||
| @@ -74,15 +74,15 @@ class HaConfigBackup extends LitElement { | ||||
|         hidden: narrow, | ||||
|         filterable: true, | ||||
|         sortable: true, | ||||
|         template: (entry: string) => | ||||
|           relativeTime(new Date(entry), this.hass.locale), | ||||
|         template: (backup) => | ||||
|           relativeTime(new Date(backup.date), this.hass.locale), | ||||
|       }, | ||||
|  | ||||
|       actions: { | ||||
|         title: "", | ||||
|         width: "15%", | ||||
|         type: "overflow-menu", | ||||
|         template: (_: string, backup: BackupContent) => | ||||
|         template: (backup) => | ||||
|           html`<ha-icon-overflow-menu | ||||
|             .hass=${this.hass} | ||||
|             .narrow=${this.narrow} | ||||
|   | ||||
| @@ -10,14 +10,14 @@ import { | ||||
| } from "@mdi/js"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   PropertyValues, | ||||
|   TemplateResult, | ||||
|   html, | ||||
| } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent, HASSDomEvent } from "../../../common/dom/fire_event"; | ||||
| import { HASSDomEvent, fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import { navigate } from "../../../common/navigate"; | ||||
| import { extractSearchParam } from "../../../common/url/search-params"; | ||||
| @@ -32,7 +32,6 @@ import "../../../components/ha-icon-overflow-menu"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import { showAutomationEditor } from "../../../data/automation"; | ||||
| import { | ||||
|   BlueprintDomain, | ||||
|   BlueprintMetaData, | ||||
|   Blueprints, | ||||
|   deleteBlueprint, | ||||
| @@ -50,10 +49,12 @@ import { documentationUrl } from "../../../util/documentation-url"; | ||||
| import { configSections } from "../ha-panel-config"; | ||||
| import { showAddBlueprintDialog } from "./show-dialog-import-blueprint"; | ||||
|  | ||||
| interface BlueprintMetaDataPath extends BlueprintMetaData { | ||||
| type BlueprintMetaDataPath = BlueprintMetaData & { | ||||
|   path: string; | ||||
|   error: boolean; | ||||
| } | ||||
|   type: "automation" | "script"; | ||||
|   fullpath: string; | ||||
| }; | ||||
|  | ||||
| const createNewFunctions = { | ||||
|   automation: (blueprintMeta: BlueprintMetaDataPath) => { | ||||
| @@ -86,7 +87,7 @@ class HaBlueprintOverview extends LitElement { | ||||
|   >; | ||||
|  | ||||
|   private _processedBlueprints = memoizeOne( | ||||
|     (blueprints: Record<string, Blueprints>) => { | ||||
|     (blueprints: Record<string, Blueprints>): BlueprintMetaDataPath[] => { | ||||
|       const result: any[] = []; | ||||
|       Object.entries(blueprints).forEach(([type, typeBlueprints]) => | ||||
|         Object.entries(typeBlueprints).forEach(([path, blueprint]) => { | ||||
| @@ -125,9 +126,9 @@ class HaBlueprintOverview extends LitElement { | ||||
|         direction: "asc", | ||||
|         grows: true, | ||||
|         template: narrow | ||||
|           ? (name, entity: any) => html` | ||||
|               ${name}<br /> | ||||
|               <div class="secondary">${entity.path}</div> | ||||
|           ? (blueprint) => html` | ||||
|               ${blueprint.name}<br /> | ||||
|               <div class="secondary">${blueprint.path}</div> | ||||
|             ` | ||||
|           : undefined, | ||||
|       }, | ||||
| @@ -135,9 +136,9 @@ class HaBlueprintOverview extends LitElement { | ||||
|         title: this.hass.localize( | ||||
|           "ui.panel.config.blueprint.overview.headers.type" | ||||
|         ), | ||||
|         template: (type: BlueprintDomain) => | ||||
|         template: (blueprint) => | ||||
|           html`${this.hass.localize( | ||||
|             `ui.panel.config.blueprint.overview.types.${type}` | ||||
|             `ui.panel.config.blueprint.overview.types.${blueprint.type}` | ||||
|           )}`, | ||||
|         sortable: true, | ||||
|         filterable: true, | ||||
| @@ -163,7 +164,7 @@ class HaBlueprintOverview extends LitElement { | ||||
|         title: "", | ||||
|         width: this.narrow ? undefined : "10%", | ||||
|         type: "overflow-menu", | ||||
|         template: (_: string, blueprint) => | ||||
|         template: (blueprint) => | ||||
|           blueprint.error | ||||
|             ? html`<ha-svg-icon | ||||
|                 style="color: var(--error-color); display: block; margin-inline-end: 12px; margin-inline-start: auto;" | ||||
| @@ -177,7 +178,7 @@ class HaBlueprintOverview extends LitElement { | ||||
|                     { | ||||
|                       path: mdiPlus, | ||||
|                       label: this.hass.localize( | ||||
|                         `ui.panel.config.blueprint.overview.create_${blueprint.domain}` | ||||
|                         `ui.panel.config.blueprint.overview.create_${blueprint.type}` | ||||
|                       ), | ||||
|                       action: () => this._createNew(blueprint), | ||||
|                     }, | ||||
| @@ -324,7 +325,7 @@ class HaBlueprintOverview extends LitElement { | ||||
|   private _handleRowClicked(ev: HASSDomEvent<RowClickedEvent>) { | ||||
|     const blueprint = this._processedBlueprints(this.blueprints).find( | ||||
|       (b) => b.fullpath === ev.detail.id | ||||
|     ); | ||||
|     )!; | ||||
|     if (blueprint.error) { | ||||
|       showAlertDialog(this, { | ||||
|         title: this.hass.localize("ui.panel.config.blueprint.overview.error", { | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user