mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-11-04 00:19:47 +00:00 
			
		
		
		
	Compare commits
	
		
			147 Commits
		
	
	
		
			boolean_se
			...
			break-out-
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					adf77e1e80 | ||
| 
						 | 
					344ff9da5b | ||
| 
						 | 
					3478bd309b | ||
| 
						 | 
					64b8b7658d | ||
| 
						 | 
					a1af8718a0 | ||
| 
						 | 
					fd9e2b647d | ||
| 
						 | 
					caee4ba7bc | ||
| 
						 | 
					915036006d | ||
| 
						 | 
					48887f2066 | ||
| 
						 | 
					68d9ce7923 | ||
| 
						 | 
					a36f3c8fb1 | ||
| 
						 | 
					4dfadea9e9 | ||
| 
						 | 
					71dc26edab | ||
| 
						 | 
					f260c95add | ||
| 
						 | 
					dc6f1efffb | ||
| 
						 | 
					b7763882f4 | ||
| 
						 | 
					7de5c46f14 | ||
| 
						 | 
					5920efa2b2 | ||
| 
						 | 
					d2194d55f9 | ||
| 
						 | 
					c0043af4c9 | ||
| 
						 | 
					dcf763438b | ||
| 
						 | 
					858a00e28c | ||
| 
						 | 
					ab407e8274 | ||
| 
						 | 
					14f96a6262 | ||
| 
						 | 
					2b33c70e04 | ||
| 
						 | 
					717443e2d6 | ||
| 
						 | 
					2aba9099a0 | ||
| 
						 | 
					3079f126a8 | ||
| 
						 | 
					1cdfb746bf | ||
| 
						 | 
					39a1844991 | ||
| 
						 | 
					9e4dc0d39e | ||
| 
						 | 
					ab91a4b814 | ||
| 
						 | 
					ca66c02fb3 | ||
| 
						 | 
					97bb052d71 | ||
| 
						 | 
					137bb473c0 | ||
| 
						 | 
					326b57f91b | ||
| 
						 | 
					32feab6a70 | ||
| 
						 | 
					68a0d04f04 | ||
| 
						 | 
					9078ab4026 | ||
| 
						 | 
					8605684906 | ||
| 
						 | 
					9f17d17d6e | ||
| 
						 | 
					ba5f176d52 | ||
| 
						 | 
					7115d14699 | ||
| 
						 | 
					23e37daff3 | ||
| 
						 | 
					ed6c2dfe39 | ||
| 
						 | 
					b48a28f2a6 | ||
| 
						 | 
					3166fec7db | ||
| 
						 | 
					1a67bd0414 | ||
| 
						 | 
					d34c43e292 | ||
| 
						 | 
					c7cfbb5b6c | ||
| 
						 | 
					bde2fd8202 | ||
| 
						 | 
					e5327c0903 | ||
| 
						 | 
					1a0ca1b78f | ||
| 
						 | 
					ed141b1d12 | ||
| 
						 | 
					5a7a71c551 | ||
| 
						 | 
					f09e0d187b | ||
| 
						 | 
					7f6325fa5e | ||
| 
						 | 
					de292a8143 | ||
| 
						 | 
					84b2005844 | ||
| 
						 | 
					0d93432a2c | ||
| 
						 | 
					8bc9927ee2 | ||
| 
						 | 
					484bed4dab | ||
| 
						 | 
					3d7e243707 | ||
| 
						 | 
					f8a432c89e | ||
| 
						 | 
					d484b2f63d | ||
| 
						 | 
					30d9186031 | ||
| 
						 | 
					cd74367acc | ||
| 
						 | 
					618cd9d9e5 | ||
| 
						 | 
					0ff2f1bf75 | ||
| 
						 | 
					d28f1f07e7 | ||
| 
						 | 
					6aa5bc2d8b | ||
| 
						 | 
					76fc0c7ab1 | ||
| 
						 | 
					7aa7019386 | ||
| 
						 | 
					b69f0964c9 | ||
| 
						 | 
					2f9b6d000b | ||
| 
						 | 
					9ff3218964 | ||
| 
						 | 
					94f186c436 | ||
| 
						 | 
					449f858ac8 | ||
| 
						 | 
					91a2f2cf24 | ||
| 
						 | 
					2c975d4f41 | ||
| 
						 | 
					ab534933fc | ||
| 
						 | 
					e353aaa339 | ||
| 
						 | 
					020904f8f6 | ||
| 
						 | 
					fa8b3f006d | ||
| 
						 | 
					d9ce20992c | ||
| 
						 | 
					a09f44dcd2 | ||
| 
						 | 
					c709059c00 | ||
| 
						 | 
					5613df1d01 | ||
| 
						 | 
					d8013a4db9 | ||
| 
						 | 
					216dbc4d41 | ||
| 
						 | 
					c40751dadd | ||
| 
						 | 
					f58d3ad670 | ||
| 
						 | 
					682f5345cc | ||
| 
						 | 
					a69771c1f8 | ||
| 
						 | 
					22de449dda | ||
| 
						 | 
					05a27b9399 | ||
| 
						 | 
					32083ea13d | ||
| 
						 | 
					18210f35b5 | ||
| 
						 | 
					362a6f46fe | ||
| 
						 | 
					1c9d411d3a | ||
| 
						 | 
					6d84523456 | ||
| 
						 | 
					2a18706a13 | ||
| 
						 | 
					87b58b0bbd | ||
| 
						 | 
					3ebb268b57 | ||
| 
						 | 
					2df097cd1b | ||
| 
						 | 
					8349e47c17 | ||
| 
						 | 
					4913932c97 | ||
| 
						 | 
					19f057a51b | ||
| 
						 | 
					c556742ff4 | ||
| 
						 | 
					8b5f731d0c | ||
| 
						 | 
					9568677926 | ||
| 
						 | 
					93ee5de1b4 | ||
| 
						 | 
					2f68ee0efc | ||
| 
						 | 
					5a229e3c88 | ||
| 
						 | 
					7c5f947865 | ||
| 
						 | 
					e9cbd54979 | ||
| 
						 | 
					cf55824899 | ||
| 
						 | 
					395586ddeb | ||
| 
						 | 
					9c48dbf232 | ||
| 
						 | 
					00eb820e36 | ||
| 
						 | 
					6b99cda982 | ||
| 
						 | 
					9bde0e876d | ||
| 
						 | 
					9482fcb04b | ||
| 
						 | 
					883ad58f52 | ||
| 
						 | 
					11ace6002a | ||
| 
						 | 
					c416daeb92 | ||
| 
						 | 
					199b7d9bc3 | ||
| 
						 | 
					a9f8eb5ab1 | ||
| 
						 | 
					ab38dad156 | ||
| 
						 | 
					1ddeca3eeb | ||
| 
						 | 
					f08f455698 | ||
| 
						 | 
					735560c552 | ||
| 
						 | 
					b5c2b555bc | ||
| 
						 | 
					b368f886f9 | ||
| 
						 | 
					5f0497a3b8 | ||
| 
						 | 
					da75eecfa5 | ||
| 
						 | 
					fdf829bc81 | ||
| 
						 | 
					0447247add | ||
| 
						 | 
					895333aa05 | ||
| 
						 | 
					58ba9f628a | ||
| 
						 | 
					28ced4bfd3 | ||
| 
						 | 
					fd6a192db1 | ||
| 
						 | 
					d72e8c35d8 | ||
| 
						 | 
					5bc3ad4c63 | ||
| 
						 | 
					530745d20d | ||
| 
						 | 
					a16cae0671 | ||
| 
						 | 
					8d0c4e4a52 | 
							
								
								
									
										4
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							@@ -89,7 +89,7 @@ jobs:
 | 
				
			|||||||
        env:
 | 
					        env:
 | 
				
			||||||
          IS_TEST: "true"
 | 
					          IS_TEST: "true"
 | 
				
			||||||
      - name: Upload bundle stats
 | 
					      - name: Upload bundle stats
 | 
				
			||||||
        uses: actions/upload-artifact@v4.3.6
 | 
					        uses: actions/upload-artifact@v4.4.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: frontend-bundle-stats
 | 
					          name: frontend-bundle-stats
 | 
				
			||||||
          path: build/stats/*.json
 | 
					          path: build/stats/*.json
 | 
				
			||||||
@@ -113,7 +113,7 @@ jobs:
 | 
				
			|||||||
        env:
 | 
					        env:
 | 
				
			||||||
          IS_TEST: "true"
 | 
					          IS_TEST: "true"
 | 
				
			||||||
      - name: Upload bundle stats
 | 
					      - name: Upload bundle stats
 | 
				
			||||||
        uses: actions/upload-artifact@v4.3.6
 | 
					        uses: actions/upload-artifact@v4.4.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: supervisor-bundle-stats
 | 
					          name: supervisor-bundle-stats
 | 
				
			||||||
          path: build/stats/*.json
 | 
					          path: build/stats/*.json
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							@@ -57,14 +57,14 @@ jobs:
 | 
				
			|||||||
        run: tar -czvf translations.tar.gz translations
 | 
					        run: tar -czvf translations.tar.gz translations
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Upload build artifacts
 | 
					      - name: Upload build artifacts
 | 
				
			||||||
        uses: actions/upload-artifact@v4.3.6
 | 
					        uses: actions/upload-artifact@v4.4.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: wheels
 | 
					          name: wheels
 | 
				
			||||||
          path: dist/home_assistant_frontend*.whl
 | 
					          path: dist/home_assistant_frontend*.whl
 | 
				
			||||||
          if-no-files-found: error
 | 
					          if-no-files-found: error
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Upload translations
 | 
					      - name: Upload translations
 | 
				
			||||||
        uses: actions/upload-artifact@v4.3.6
 | 
					        uses: actions/upload-artifact@v4.4.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: translations
 | 
					          name: translations
 | 
				
			||||||
          path: translations.tar.gz
 | 
					          path: translations.tar.gz
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,16 +1,7 @@
 | 
				
			|||||||
diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
 | 
					diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
 | 
				
			||||||
index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
 | 
					index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
 | 
				
			||||||
--- a/modular/sortable.core.esm.js
 | 
					--- a/modular/sortable.core.esm.js
 | 
				
			||||||
+++ b/modular/sortable.core.esm.js
 | 
					+++ b/modular/sortable.core.esm.js
 | 
				
			||||||
@@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | 
					 | 
				
			||||||
           }
 | 
					 | 
				
			||||||
           target = parent; // store last element
 | 
					 | 
				
			||||||
         }
 | 
					 | 
				
			||||||
-        /* jshint boss:true */ while (parent = parent.parentNode);
 | 
					 | 
				
			||||||
+        /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
 | 
					 | 
				
			||||||
       }
 | 
					 | 
				
			||||||
       _unhideGhostForTarget();
 | 
					 | 
				
			||||||
     }
 | 
					 | 
				
			||||||
@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | 
					@@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | 
				
			||||||
         }
 | 
					         }
 | 
				
			||||||
         if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
 | 
					         if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) {
 | 
				
			||||||
@@ -33,7 +24,7 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
 | 
				
			|||||||
           }
 | 
					           }
 | 
				
			||||||
           parentEl = el; // actualization
 | 
					           parentEl = el; // actualization
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
@@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | 
					@@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | 
				
			||||||
         targetRect = getRect(target);
 | 
					         targetRect = getRect(target);
 | 
				
			||||||
         if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
 | 
					         if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) {
 | 
				
			||||||
           capture();
 | 
					           capture();
 | 
				
			||||||
@@ -44,11 +35,10 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
 | 
				
			|||||||
+          catch(err) {
 | 
					+          catch(err) {
 | 
				
			||||||
+            return completed(false);
 | 
					+            return completed(false);
 | 
				
			||||||
+          }
 | 
					+          }
 | 
				
			||||||
+          
 | 
					 | 
				
			||||||
           parentEl = el; // actualization
 | 
					           parentEl = el; // actualization
 | 
				
			||||||
 
 | 
					 
 | 
				
			||||||
           changed();
 | 
					           changed();
 | 
				
			||||||
@@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | 
					@@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | 
				
			||||||
           _silent = true;
 | 
					           _silent = true;
 | 
				
			||||||
           setTimeout(_unsilent, 30);
 | 
					           setTimeout(_unsilent, 30);
 | 
				
			||||||
           capture();
 | 
					           capture();
 | 
				
			||||||
@@ -56,8 +46,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
 | 
				
			|||||||
-            el.appendChild(dragEl);
 | 
					-            el.appendChild(dragEl);
 | 
				
			||||||
-          } else {
 | 
					-          } else {
 | 
				
			||||||
-            target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
 | 
					-            target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
 | 
				
			||||||
-          }
 | 
					 | 
				
			||||||
 
 | 
					 | 
				
			||||||
+          try {
 | 
					+          try {
 | 
				
			||||||
+            if (after && !nextSibling) {
 | 
					+            if (after && !nextSibling) {
 | 
				
			||||||
+              el.appendChild(dragEl);
 | 
					+              el.appendChild(dragEl);
 | 
				
			||||||
@@ -67,7 +55,6 @@ index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b
 | 
				
			|||||||
+          }
 | 
					+          }
 | 
				
			||||||
+          catch(err) {
 | 
					+          catch(err) {
 | 
				
			||||||
+            return completed(false);
 | 
					+            return completed(false);
 | 
				
			||||||
+          }
 | 
					           }
 | 
				
			||||||
 | 
					 
 | 
				
			||||||
           // Undo chrome's scroll adjustment (has no effect on other browsers)
 | 
					           // Undo chrome's scroll adjustment (has no effect on other browsers)
 | 
				
			||||||
           if (scrolledPastTop) {
 | 
					 | 
				
			||||||
             scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop);
 | 
					 | 
				
			||||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							@@ -6,4 +6,4 @@ enableGlobalCache: false
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
nodeLinker: node-modules
 | 
					nodeLinker: node-modules
 | 
				
			||||||
 | 
					
 | 
				
			||||||
yarnPath: .yarn/releases/yarn-4.4.0.cjs
 | 
					yarnPath: .yarn/releases/yarn-4.5.0.cjs
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,23 +15,29 @@ const brotliOptions = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
const zopfliOptions = { threshold: 150 };
 | 
					const zopfliOptions = { threshold: 150 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const compressDistBrotli = (rootDir, modernDir) =>
 | 
					const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) =>
 | 
				
			||||||
  gulp
 | 
					  gulp
 | 
				
			||||||
    .src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], {
 | 
					    .src(
 | 
				
			||||||
      base: rootDir,
 | 
					      [
 | 
				
			||||||
    })
 | 
					        `${modernDir}/**/${filesGlob}`,
 | 
				
			||||||
 | 
					        compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined,
 | 
				
			||||||
 | 
					      ].filter(Boolean),
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        base: rootDir,
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
    .pipe(brotli(brotliOptions))
 | 
					    .pipe(brotli(brotliOptions))
 | 
				
			||||||
    .pipe(gulp.dest(rootDir));
 | 
					    .pipe(gulp.dest(rootDir));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const compressDistZopfli = (rootDir, modernDir) =>
 | 
					const compressDistZopfli = (rootDir, modernDir, compressModern = false) =>
 | 
				
			||||||
  gulp
 | 
					  gulp
 | 
				
			||||||
    .src(
 | 
					    .src(
 | 
				
			||||||
      [
 | 
					      [
 | 
				
			||||||
        `${rootDir}/**/${filesGlob}`,
 | 
					        `${rootDir}/**/${filesGlob}`,
 | 
				
			||||||
        `!${modernDir}/**/${filesGlob}`,
 | 
					        compressModern ? undefined : `!${modernDir}/**/${filesGlob}`,
 | 
				
			||||||
        `!${rootDir}/{sw-modern,service_worker}.js`,
 | 
					        `!${rootDir}/{sw-modern,service_worker}.js`,
 | 
				
			||||||
        `${rootDir}/{authorize,onboarding}.html`,
 | 
					        `${rootDir}/{authorize,onboarding}.html`,
 | 
				
			||||||
      ],
 | 
					      ].filter(Boolean),
 | 
				
			||||||
      { base: rootDir }
 | 
					      { base: rootDir }
 | 
				
			||||||
    )
 | 
					    )
 | 
				
			||||||
    .pipe(zopfli(zopfliOptions))
 | 
					    .pipe(zopfli(zopfliOptions))
 | 
				
			||||||
@@ -40,12 +46,20 @@ const compressDistZopfli = (rootDir, modernDir) =>
 | 
				
			|||||||
const compressAppBrotli = () =>
 | 
					const compressAppBrotli = () =>
 | 
				
			||||||
  compressDistBrotli(paths.app_output_root, paths.app_output_latest);
 | 
					  compressDistBrotli(paths.app_output_root, paths.app_output_latest);
 | 
				
			||||||
const compressHassioBrotli = () =>
 | 
					const compressHassioBrotli = () =>
 | 
				
			||||||
  compressDistBrotli(paths.hassio_output_root, paths.hassio_output_latest);
 | 
					  compressDistBrotli(
 | 
				
			||||||
 | 
					    paths.hassio_output_root,
 | 
				
			||||||
 | 
					    paths.hassio_output_latest,
 | 
				
			||||||
 | 
					    false
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const compressAppZopfli = () =>
 | 
					const compressAppZopfli = () =>
 | 
				
			||||||
  compressDistZopfli(paths.app_output_root, paths.app_output_latest);
 | 
					  compressDistZopfli(paths.app_output_root, paths.app_output_latest);
 | 
				
			||||||
const compressHassioZopfli = () =>
 | 
					const compressHassioZopfli = () =>
 | 
				
			||||||
  compressDistZopfli(paths.hassio_output_root, paths.hassio_output_latest);
 | 
					  compressDistZopfli(
 | 
				
			||||||
 | 
					    paths.hassio_output_root,
 | 
				
			||||||
 | 
					    paths.hassio_output_latest,
 | 
				
			||||||
 | 
					    true
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
 | 
					gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli));
 | 
				
			||||||
gulp.task(
 | 
					gulp.task(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -60,6 +60,12 @@ function copyPolyfills(staticDir) {
 | 
				
			|||||||
    npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
 | 
					    npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
 | 
				
			||||||
    staticPath("polyfills/")
 | 
					    staticPath("polyfills/")
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // dialog-polyfill css
 | 
				
			||||||
 | 
					  copyFileDir(
 | 
				
			||||||
 | 
					    npmPath("dialog-polyfill/dialog-polyfill.css"),
 | 
				
			||||||
 | 
					    staticPath("polyfills/")
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
function copyLoaderJS(staticDir) {
 | 
					function copyLoaderJS(staticDir) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -111,9 +111,37 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
 | 
				
			|||||||
        friendly_name: "Living room Temperature",
 | 
					        friendly_name: "Living room Temperature",
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					    "sensor.outdoor_temperature": {
 | 
				
			||||||
 | 
					      entity_id: "sensor.outdoor_temperature",
 | 
				
			||||||
 | 
					      state: "10.5",
 | 
				
			||||||
 | 
					      attributes: {
 | 
				
			||||||
 | 
					        state_class: "measurement",
 | 
				
			||||||
 | 
					        unit_of_measurement: "°C",
 | 
				
			||||||
 | 
					        device_class: "temperature",
 | 
				
			||||||
 | 
					        friendly_name: "Outdoor temperature",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "sensor.outdoor_humidity": {
 | 
				
			||||||
 | 
					      entity_id: "sensor.outdoor_humidity",
 | 
				
			||||||
 | 
					      state: "70.4",
 | 
				
			||||||
 | 
					      attributes: {
 | 
				
			||||||
 | 
					        state_class: "measurement",
 | 
				
			||||||
 | 
					        unit_of_measurement: "%",
 | 
				
			||||||
 | 
					        device_class: "humidity",
 | 
				
			||||||
 | 
					        friendly_name: "Outdoor humidity",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
 | 
					    "device_tracker.car": {
 | 
				
			||||||
 | 
					      entity_id: "sensor.outdoor_humidity",
 | 
				
			||||||
 | 
					      state: "not_home",
 | 
				
			||||||
 | 
					      attributes: {
 | 
				
			||||||
 | 
					        friendly_name: "Car",
 | 
				
			||||||
 | 
					        icon: "mdi:car",
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					    },
 | 
				
			||||||
    "media_player.living_room_nest_mini": {
 | 
					    "media_player.living_room_nest_mini": {
 | 
				
			||||||
      entity_id: "media_player.living_room_nest_mini",
 | 
					      entity_id: "media_player.living_room_nest_mini",
 | 
				
			||||||
      state: "on",
 | 
					      state: "playing",
 | 
				
			||||||
      attributes: {
 | 
					      attributes: {
 | 
				
			||||||
        device_class: "speaker",
 | 
					        device_class: "speaker",
 | 
				
			||||||
        volume_level: 0.18,
 | 
					        volume_level: 0.18,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,6 +9,22 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
				
			|||||||
      title: isFrontpageEmbed ? "Home Assistant" : "Demo",
 | 
					      title: isFrontpageEmbed ? "Home Assistant" : "Demo",
 | 
				
			||||||
      path: "home",
 | 
					      path: "home",
 | 
				
			||||||
      icon: "mdi:home-assistant",
 | 
					      icon: "mdi:home-assistant",
 | 
				
			||||||
 | 
					      badges: [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: "entity",
 | 
				
			||||||
 | 
					          entity: "sensor.outdoor_temperature",
 | 
				
			||||||
 | 
					          color: "red",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: "entity",
 | 
				
			||||||
 | 
					          entity: "sensor.outdoor_humidity",
 | 
				
			||||||
 | 
					          color: "indigo",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          type: "entity",
 | 
				
			||||||
 | 
					          entity: "device_tracker.car",
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ],
 | 
				
			||||||
      sections: [
 | 
					      sections: [
 | 
				
			||||||
        ...(isFrontpageEmbed
 | 
					        ...(isFrontpageEmbed
 | 
				
			||||||
          ? []
 | 
					          ? []
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -232,7 +232,7 @@ export const basicTrace: DemoTrace = {
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
      action: [
 | 
					      action: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          service: "input_boolean.toggle",
 | 
					          action: "input_boolean.toggle",
 | 
				
			||||||
          target: {
 | 
					          target: {
 | 
				
			||||||
            entity_id: "input_boolean.toggle_4",
 | 
					            entity_id: "input_boolean.toggle_4",
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
 | 
				
			|||||||
          ],
 | 
					          ],
 | 
				
			||||||
          default: [
 | 
					          default: [
 | 
				
			||||||
            {
 | 
					            {
 | 
				
			||||||
              service: "input_boolean.toggle",
 | 
					              action: "input_boolean.toggle",
 | 
				
			||||||
              alias: "Toggle 2",
 | 
					              alias: "Toggle 2",
 | 
				
			||||||
              target: {
 | 
					              target: {
 | 
				
			||||||
                entity_id: "input_boolean.toggle_2",
 | 
					                entity_id: "input_boolean.toggle_2",
 | 
				
			||||||
@@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
 | 
				
			|||||||
          ],
 | 
					          ],
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          service: "input_boolean.toggle",
 | 
					          action: "input_boolean.toggle",
 | 
				
			||||||
          target: {
 | 
					          target: {
 | 
				
			||||||
            entity_id: "input_boolean.toggle_4",
 | 
					            entity_id: "input_boolean.toggle_4",
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -143,7 +143,7 @@ export const motionLightTrace: DemoTrace = {
 | 
				
			|||||||
      ],
 | 
					      ],
 | 
				
			||||||
      action: [
 | 
					      action: [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          service: "light.turn_on",
 | 
					          action: "light.turn_on",
 | 
				
			||||||
          target: {
 | 
					          target: {
 | 
				
			||||||
            entity_id: "light.elgato_key_light_air",
 | 
					            entity_id: "light.elgato_key_light_air",
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
@@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
 | 
				
			|||||||
          delay: 0,
 | 
					          delay: 0,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          service: "light.turn_off",
 | 
					          action: "light.turn_off",
 | 
				
			||||||
          target: {
 | 
					          target: {
 | 
				
			||||||
            entity_id: "light.elgato_key_light_air",
 | 
					            entity_id: "light.elgato_key_light_air",
 | 
				
			||||||
          },
 | 
					          },
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,6 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis
 | 
				
			|||||||
import type { ConditionWithShorthand } from "../../../../src/data/automation";
 | 
					import type { ConditionWithShorthand } from "../../../../src/data/automation";
 | 
				
			||||||
import "../../../../src/panels/config/automation/condition/ha-automation-condition";
 | 
					import "../../../../src/panels/config/automation/condition/ha-automation-condition";
 | 
				
			||||||
import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
 | 
					import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device";
 | 
				
			||||||
import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical";
 | 
					 | 
				
			||||||
import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
 | 
					import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state";
 | 
				
			||||||
import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
 | 
					import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state";
 | 
				
			||||||
import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
 | 
					import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun";
 | 
				
			||||||
@@ -19,62 +18,67 @@ import { HaTemplateCondition } from "../../../../src/panels/config/automation/co
 | 
				
			|||||||
import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
 | 
					import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time";
 | 
				
			||||||
import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
 | 
					import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger";
 | 
				
			||||||
import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
 | 
					import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone";
 | 
				
			||||||
 | 
					import { HaAndCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-and";
 | 
				
			||||||
 | 
					import { HaOrCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-or";
 | 
				
			||||||
 | 
					import { HaNotCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-not";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
 | 
					const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "State",
 | 
					    name: "State",
 | 
				
			||||||
    conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaStateCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Numeric State",
 | 
					    name: "Numeric State",
 | 
				
			||||||
    conditions: [
 | 
					    conditions: [{ ...HaNumericStateCondition.defaultConfig }],
 | 
				
			||||||
      { condition: "numeric_state", ...HaNumericStateCondition.defaultConfig },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Sun",
 | 
					    name: "Sun",
 | 
				
			||||||
    conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaSunCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Zone",
 | 
					    name: "Zone",
 | 
				
			||||||
    conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaZoneCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Time",
 | 
					    name: "Time",
 | 
				
			||||||
    conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaTimeCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Template",
 | 
					    name: "Template",
 | 
				
			||||||
    conditions: [
 | 
					    conditions: [{ ...HaTemplateCondition.defaultConfig }],
 | 
				
			||||||
      { condition: "template", ...HaTemplateCondition.defaultConfig },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Device",
 | 
					    name: "Device",
 | 
				
			||||||
    conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaDeviceCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "And",
 | 
					    name: "And",
 | 
				
			||||||
    conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaAndCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Or",
 | 
					    name: "Or",
 | 
				
			||||||
    conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaOrCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Not",
 | 
					    name: "Not",
 | 
				
			||||||
    conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaNotCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Trigger",
 | 
					    name: "Trigger",
 | 
				
			||||||
    conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }],
 | 
					    conditions: [{ ...HaTriggerCondition.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Shorthand",
 | 
					    name: "Shorthand",
 | 
				
			||||||
    conditions: [
 | 
					    conditions: [
 | 
				
			||||||
      { and: HaLogicalCondition.defaultConfig.conditions },
 | 
					      {
 | 
				
			||||||
      { or: HaLogicalCondition.defaultConfig.conditions },
 | 
					        ...HaAndCondition.defaultConfig,
 | 
				
			||||||
      { not: HaLogicalCondition.defaultConfig.conditions },
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        ...HaOrCondition.defaultConfig,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
 | 
					      {
 | 
				
			||||||
 | 
					        ...HaNotCondition.defaultConfig,
 | 
				
			||||||
 | 
					      },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,55 +30,48 @@ import { HaConversationTrigger } from "../../../../src/panels/config/automation/
 | 
				
			|||||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
 | 
					const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "State",
 | 
					    name: "State",
 | 
				
			||||||
    triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaStateTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "MQTT",
 | 
					    name: "MQTT",
 | 
				
			||||||
    triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaMQTTTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "GeoLocation",
 | 
					    name: "GeoLocation",
 | 
				
			||||||
    triggers: [
 | 
					    triggers: [{ ...HaGeolocationTrigger.defaultConfig }],
 | 
				
			||||||
      { platform: "geo_location", ...HaGeolocationTrigger.defaultConfig },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Home Assistant",
 | 
					    name: "Home Assistant",
 | 
				
			||||||
    triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaHassTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Numeric State",
 | 
					    name: "Numeric State",
 | 
				
			||||||
    triggers: [
 | 
					    triggers: [{ ...HaNumericStateTrigger.defaultConfig }],
 | 
				
			||||||
      { platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Sun",
 | 
					    name: "Sun",
 | 
				
			||||||
    triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaSunTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Time Pattern",
 | 
					    name: "Time Pattern",
 | 
				
			||||||
    triggers: [
 | 
					    triggers: [{ ...HaTimePatternTrigger.defaultConfig }],
 | 
				
			||||||
      { platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig },
 | 
					 | 
				
			||||||
    ],
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Webhook",
 | 
					    name: "Webhook",
 | 
				
			||||||
    triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaWebhookTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Persistent Notification",
 | 
					    name: "Persistent Notification",
 | 
				
			||||||
    triggers: [
 | 
					    triggers: [
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        platform: "persistent_notification",
 | 
					 | 
				
			||||||
        ...HaPersistentNotificationTrigger.defaultConfig,
 | 
					        ...HaPersistentNotificationTrigger.defaultConfig,
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
    ],
 | 
					    ],
 | 
				
			||||||
@@ -86,37 +79,37 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Zone",
 | 
					    name: "Zone",
 | 
				
			||||||
    triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaZoneTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Tag",
 | 
					    name: "Tag",
 | 
				
			||||||
    triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaTagTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Time",
 | 
					    name: "Time",
 | 
				
			||||||
    triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaTimeTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Template",
 | 
					    name: "Template",
 | 
				
			||||||
    triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaTemplateTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Event",
 | 
					    name: "Event",
 | 
				
			||||||
    triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaEventTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Device Trigger",
 | 
					    name: "Device Trigger",
 | 
				
			||||||
    triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }],
 | 
					    triggers: [{ ...HaDeviceTrigger.defaultConfig }],
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    name: "Sentence",
 | 
					    name: "Sentence",
 | 
				
			||||||
    triggers: [
 | 
					    triggers: [
 | 
				
			||||||
      { platform: "conversation", ...HaConversationTrigger.defaultConfig },
 | 
					      { ...HaConversationTrigger.defaultConfig },
 | 
				
			||||||
      {
 | 
					      {
 | 
				
			||||||
        platform: "conversation",
 | 
					        platform: "conversation",
 | 
				
			||||||
        command: ["Turn on the lights", "Turn the lights on"],
 | 
					        command: ["Turn on the lights", "Turn the lights on"],
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,6 +64,7 @@ const DEVICES: DeviceRegistryEntry[] = [
 | 
				
			|||||||
    labels: [],
 | 
					    labels: [],
 | 
				
			||||||
    created_at: 0,
 | 
					    created_at: 0,
 | 
				
			||||||
    modified_at: 0,
 | 
					    modified_at: 0,
 | 
				
			||||||
 | 
					    primary_config_entry: null,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    area_id: "backyard",
 | 
					    area_id: "backyard",
 | 
				
			||||||
@@ -86,6 +87,7 @@ const DEVICES: DeviceRegistryEntry[] = [
 | 
				
			|||||||
    labels: [],
 | 
					    labels: [],
 | 
				
			||||||
    created_at: 0,
 | 
					    created_at: 0,
 | 
				
			||||||
    modified_at: 0,
 | 
					    modified_at: 0,
 | 
				
			||||||
 | 
					    primary_config_entry: null,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    area_id: null,
 | 
					    area_id: null,
 | 
				
			||||||
@@ -108,6 +110,7 @@ const DEVICES: DeviceRegistryEntry[] = [
 | 
				
			|||||||
    labels: [],
 | 
					    labels: [],
 | 
				
			||||||
    created_at: 0,
 | 
					    created_at: 0,
 | 
				
			||||||
    modified_at: 0,
 | 
					    modified_at: 0,
 | 
				
			||||||
 | 
					    primary_config_entry: null,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -64,6 +64,7 @@ const DEVICES: DeviceRegistryEntry[] = [
 | 
				
			|||||||
    labels: [],
 | 
					    labels: [],
 | 
				
			||||||
    created_at: 0,
 | 
					    created_at: 0,
 | 
				
			||||||
    modified_at: 0,
 | 
					    modified_at: 0,
 | 
				
			||||||
 | 
					    primary_config_entry: null,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    area_id: "backyard",
 | 
					    area_id: "backyard",
 | 
				
			||||||
@@ -86,6 +87,7 @@ const DEVICES: DeviceRegistryEntry[] = [
 | 
				
			|||||||
    labels: [],
 | 
					    labels: [],
 | 
				
			||||||
    created_at: 0,
 | 
					    created_at: 0,
 | 
				
			||||||
    modified_at: 0,
 | 
					    modified_at: 0,
 | 
				
			||||||
 | 
					    primary_config_entry: null,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  {
 | 
				
			||||||
    area_id: null,
 | 
					    area_id: null,
 | 
				
			||||||
@@ -108,6 +110,7 @@ const DEVICES: DeviceRegistryEntry[] = [
 | 
				
			|||||||
    labels: [],
 | 
					    labels: [],
 | 
				
			||||||
    created_at: 0,
 | 
					    created_at: 0,
 | 
				
			||||||
    modified_at: 0,
 | 
					    modified_at: 0,
 | 
				
			||||||
 | 
					    primary_config_entry: null,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										3
									
								
								gallery/src/pages/misc/ha-markdown.markdown
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										3
									
								
								gallery/src/pages/misc/ha-markdown.markdown
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,3 @@
 | 
				
			|||||||
 | 
					---
 | 
				
			||||||
 | 
					title: Markdown
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
							
								
								
									
										93
									
								
								gallery/src/pages/misc/ha-markdown.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								gallery/src/pages/misc/ha-markdown.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
				
			|||||||
 | 
					import { css, html, LitElement } from "lit";
 | 
				
			||||||
 | 
					import "../../../../src/components/ha-card";
 | 
				
			||||||
 | 
					import "../../../../src/components/ha-markdown";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import { customElement } from "lit/decorators";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface MarkdownContent {
 | 
				
			||||||
 | 
					  content: string;
 | 
				
			||||||
 | 
					  breaks: boolean;
 | 
				
			||||||
 | 
					  allowSvg: boolean;
 | 
				
			||||||
 | 
					  lazyImages: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const mdContentwithDefaults = (md: Partial<MarkdownContent>) =>
 | 
				
			||||||
 | 
					  ({
 | 
				
			||||||
 | 
					    breaks: false,
 | 
				
			||||||
 | 
					    allowSvg: false,
 | 
				
			||||||
 | 
					    lazyImages: false,
 | 
				
			||||||
 | 
					    ...md,
 | 
				
			||||||
 | 
					  }) as MarkdownContent;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const generateContent = (md) => `
 | 
				
			||||||
 | 
					\`\`\`json
 | 
				
			||||||
 | 
					${JSON.stringify({ ...md, content: undefined })}
 | 
				
			||||||
 | 
					\`\`\`
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					---
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					${md.content}
 | 
				
			||||||
 | 
					`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const markdownContents: MarkdownContent[] = [
 | 
				
			||||||
 | 
					  mdContentwithDefaults({
 | 
				
			||||||
 | 
					    content: "_Hello_ **there** 👋, ~~nice~~ of you ||to|| show up.",
 | 
				
			||||||
 | 
					  }),
 | 
				
			||||||
 | 
					  ...[true, false].map((breaks) =>
 | 
				
			||||||
 | 
					    mdContentwithDefaults({
 | 
				
			||||||
 | 
					      breaks,
 | 
				
			||||||
 | 
					      content: `
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					> [!TIP]
 | 
				
			||||||
 | 
					> Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer dictum quis ante eu eleifend. Integer sed [consectetur est, nec elementum magna](#). Fusce lobortis lectus ac rutrum tincidunt. Quisque suscipit gravida ante, in convallis risus vulputate non.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					key | description
 | 
				
			||||||
 | 
					--  | --
 | 
				
			||||||
 | 
					lorem | ipsum
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					- list item 1
 | 
				
			||||||
 | 
					- list item 2
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					  ),
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("demo-misc-ha-markdown")
 | 
				
			||||||
 | 
					export class DemoMiscMarkdown extends LitElement {
 | 
				
			||||||
 | 
					  protected render() {
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <div class="container">
 | 
				
			||||||
 | 
					        ${markdownContents.map(
 | 
				
			||||||
 | 
					          (md) =>
 | 
				
			||||||
 | 
					            html`<ha-card>
 | 
				
			||||||
 | 
					              <ha-markdown
 | 
				
			||||||
 | 
					                .content=${generateContent(md)}
 | 
				
			||||||
 | 
					                .breaks=${md.breaks}
 | 
				
			||||||
 | 
					                .allowSvg=${md.allowSvg}
 | 
				
			||||||
 | 
					                .lazyImages=${md.lazyImages}
 | 
				
			||||||
 | 
					              ></ha-markdown>
 | 
				
			||||||
 | 
					            </ha-card>`
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static get styles() {
 | 
				
			||||||
 | 
					    return css`
 | 
				
			||||||
 | 
					      ha-card {
 | 
				
			||||||
 | 
					        margin: 12px;
 | 
				
			||||||
 | 
					        padding: 12px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					    "demo-misc-ha-markdown": DemoMiscMarkdown;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -232,6 +232,7 @@ const createDeviceRegistryEntries = (
 | 
				
			|||||||
    labels: [],
 | 
					    labels: [],
 | 
				
			||||||
    created_at: 0,
 | 
					    created_at: 0,
 | 
				
			||||||
    modified_at: 0,
 | 
					    modified_at: 0,
 | 
				
			||||||
 | 
					    primary_config_entry: null,
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
 | 
				
			|||||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
 | 
					import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
 | 
				
			||||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
 | 
					import type { HaTextField } from "../../../../src/components/ha-textfield";
 | 
				
			||||||
import "../../../../src/components/ha-textfield";
 | 
					import "../../../../src/components/ha-textfield";
 | 
				
			||||||
import "../../../../src/components/ha-list-new";
 | 
					import "../../../../src/components/ha-md-list";
 | 
				
			||||||
import "../../../../src/components/ha-list-item-new";
 | 
					import "../../../../src/components/ha-md-list-item";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("dialog-hassio-repositories")
 | 
					@customElement("dialog-hassio-repositories")
 | 
				
			||||||
class HassioRepositoriesDialog extends LitElement {
 | 
					class HassioRepositoriesDialog extends LitElement {
 | 
				
			||||||
@@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
 | 
				
			|||||||
          ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
 | 
					          ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
 | 
				
			||||||
          : ""}
 | 
					          : ""}
 | 
				
			||||||
        <div class="form">
 | 
					        <div class="form">
 | 
				
			||||||
          <ha-list-new>
 | 
					          <ha-md-list>
 | 
				
			||||||
            ${repositories.length
 | 
					            ${repositories.length
 | 
				
			||||||
              ? repositories.map(
 | 
					              ? repositories.map(
 | 
				
			||||||
                  (repo) => html`
 | 
					                  (repo) => html`
 | 
				
			||||||
                    <ha-list-item-new class="option">
 | 
					                    <ha-md-list-item class="option">
 | 
				
			||||||
                      ${repo.name}
 | 
					                      ${repo.name}
 | 
				
			||||||
                      <div slot="supporting-text">
 | 
					                      <div slot="supporting-text">
 | 
				
			||||||
                        <div>${repo.maintainer}</div>
 | 
					                        <div>${repo.maintainer}</div>
 | 
				
			||||||
@@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
 | 
				
			|||||||
                          )}
 | 
					                          )}
 | 
				
			||||||
                        </simple-tooltip>
 | 
					                        </simple-tooltip>
 | 
				
			||||||
                      </div>
 | 
					                      </div>
 | 
				
			||||||
                    </ha-list-item-new>
 | 
					                    </ha-md-list-item>
 | 
				
			||||||
                  `
 | 
					                  `
 | 
				
			||||||
                )
 | 
					                )
 | 
				
			||||||
              : html`<ha-list-item-new> No repositories </ha-list-item-new>`}
 | 
					              : html`<ha-md-list-item> No repositories </ha-md-list-item>`}
 | 
				
			||||||
          </ha-list-new>
 | 
					          </ha-md-list>
 | 
				
			||||||
          <div class="layout horizontal bottom">
 | 
					          <div class="layout horizontal bottom">
 | 
				
			||||||
            <ha-textfield
 | 
					            <ha-textfield
 | 
				
			||||||
              class="flex-auto"
 | 
					              class="flex-auto"
 | 
				
			||||||
@@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
 | 
				
			|||||||
        div.delete ha-icon-button {
 | 
					        div.delete ha-icon-button {
 | 
				
			||||||
          color: var(--error-color);
 | 
					          color: var(--error-color);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        ha-list-item-new {
 | 
					        ha-md-list-item {
 | 
				
			||||||
          position: relative;
 | 
					          position: relative;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										51
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										51
									
								
								package.json
									
									
									
									
									
								
							@@ -25,15 +25,15 @@
 | 
				
			|||||||
  "license": "Apache-2.0",
 | 
					  "license": "Apache-2.0",
 | 
				
			||||||
  "type": "module",
 | 
					  "type": "module",
 | 
				
			||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@babel/runtime": "7.25.4",
 | 
					    "@babel/runtime": "7.25.6",
 | 
				
			||||||
    "@braintree/sanitize-url": "7.1.0",
 | 
					    "@braintree/sanitize-url": "7.1.0",
 | 
				
			||||||
    "@codemirror/autocomplete": "6.18.0",
 | 
					    "@codemirror/autocomplete": "6.18.1",
 | 
				
			||||||
    "@codemirror/commands": "6.6.0",
 | 
					    "@codemirror/commands": "6.6.2",
 | 
				
			||||||
    "@codemirror/language": "6.10.2",
 | 
					    "@codemirror/language": "6.10.3",
 | 
				
			||||||
    "@codemirror/legacy-modes": "6.4.1",
 | 
					    "@codemirror/legacy-modes": "6.4.1",
 | 
				
			||||||
    "@codemirror/search": "6.5.6",
 | 
					    "@codemirror/search": "6.5.6",
 | 
				
			||||||
    "@codemirror/state": "6.4.1",
 | 
					    "@codemirror/state": "6.4.1",
 | 
				
			||||||
    "@codemirror/view": "6.32.0",
 | 
					    "@codemirror/view": "6.33.0",
 | 
				
			||||||
    "@egjs/hammerjs": "2.0.17",
 | 
					    "@egjs/hammerjs": "2.0.17",
 | 
				
			||||||
    "@formatjs/intl-datetimeformat": "6.12.5",
 | 
					    "@formatjs/intl-datetimeformat": "6.12.5",
 | 
				
			||||||
    "@formatjs/intl-displaynames": "6.6.8",
 | 
					    "@formatjs/intl-displaynames": "6.6.8",
 | 
				
			||||||
@@ -80,7 +80,7 @@
 | 
				
			|||||||
    "@material/mwc-top-app-bar": "0.27.0",
 | 
					    "@material/mwc-top-app-bar": "0.27.0",
 | 
				
			||||||
    "@material/mwc-top-app-bar-fixed": "0.27.0",
 | 
					    "@material/mwc-top-app-bar-fixed": "0.27.0",
 | 
				
			||||||
    "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
 | 
					    "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
 | 
				
			||||||
    "@material/web": "2.1.0",
 | 
					    "@material/web": "2.2.0",
 | 
				
			||||||
    "@mdi/js": "7.4.47",
 | 
					    "@mdi/js": "7.4.47",
 | 
				
			||||||
    "@mdi/svg": "7.4.47",
 | 
					    "@mdi/svg": "7.4.47",
 | 
				
			||||||
    "@polymer/paper-item": "3.0.1",
 | 
					    "@polymer/paper-item": "3.0.1",
 | 
				
			||||||
@@ -88,8 +88,8 @@
 | 
				
			|||||||
    "@polymer/paper-tabs": "3.1.0",
 | 
					    "@polymer/paper-tabs": "3.1.0",
 | 
				
			||||||
    "@polymer/polymer": "3.5.1",
 | 
					    "@polymer/polymer": "3.5.1",
 | 
				
			||||||
    "@thomasloven/round-slider": "0.6.0",
 | 
					    "@thomasloven/round-slider": "0.6.0",
 | 
				
			||||||
    "@vaadin/combo-box": "24.4.6",
 | 
					    "@vaadin/combo-box": "24.4.9",
 | 
				
			||||||
    "@vaadin/vaadin-themable-mixin": "24.4.6",
 | 
					    "@vaadin/vaadin-themable-mixin": "24.4.9",
 | 
				
			||||||
    "@vibrant/color": "3.2.1-alpha.1",
 | 
					    "@vibrant/color": "3.2.1-alpha.1",
 | 
				
			||||||
    "@vibrant/core": "3.2.1-alpha.1",
 | 
					    "@vibrant/core": "3.2.1-alpha.1",
 | 
				
			||||||
    "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
 | 
					    "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
 | 
				
			||||||
@@ -102,10 +102,11 @@
 | 
				
			|||||||
    "comlink": "4.4.1",
 | 
					    "comlink": "4.4.1",
 | 
				
			||||||
    "core-js": "3.38.1",
 | 
					    "core-js": "3.38.1",
 | 
				
			||||||
    "cropperjs": "1.6.2",
 | 
					    "cropperjs": "1.6.2",
 | 
				
			||||||
    "date-fns": "3.6.0",
 | 
					    "date-fns": "4.1.0",
 | 
				
			||||||
    "date-fns-tz": "3.1.3",
 | 
					    "date-fns-tz": "3.1.3",
 | 
				
			||||||
    "deep-clone-simple": "1.1.1",
 | 
					    "deep-clone-simple": "1.1.1",
 | 
				
			||||||
    "deep-freeze": "0.0.1",
 | 
					    "deep-freeze": "0.0.1",
 | 
				
			||||||
 | 
					    "dialog-polyfill": "0.5.6",
 | 
				
			||||||
    "element-internals-polyfill": "1.3.11",
 | 
					    "element-internals-polyfill": "1.3.11",
 | 
				
			||||||
    "fuse.js": "7.0.0",
 | 
					    "fuse.js": "7.0.0",
 | 
				
			||||||
    "google-timezones-json": "1.2.0",
 | 
					    "google-timezones-json": "1.2.0",
 | 
				
			||||||
@@ -118,7 +119,7 @@
 | 
				
			|||||||
    "leaflet-draw": "1.0.4",
 | 
					    "leaflet-draw": "1.0.4",
 | 
				
			||||||
    "lit": "2.8.0",
 | 
					    "lit": "2.8.0",
 | 
				
			||||||
    "luxon": "3.5.0",
 | 
					    "luxon": "3.5.0",
 | 
				
			||||||
    "marked": "14.0.0",
 | 
					    "marked": "14.1.2",
 | 
				
			||||||
    "memoize-one": "6.0.0",
 | 
					    "memoize-one": "6.0.0",
 | 
				
			||||||
    "node-vibrant": "3.2.1-alpha.1",
 | 
					    "node-vibrant": "3.2.1-alpha.1",
 | 
				
			||||||
    "proxy-polyfill": "0.3.2",
 | 
					    "proxy-polyfill": "0.3.2",
 | 
				
			||||||
@@ -127,13 +128,13 @@
 | 
				
			|||||||
    "qrcode": "1.5.4",
 | 
					    "qrcode": "1.5.4",
 | 
				
			||||||
    "roboto-fontface": "0.10.0",
 | 
					    "roboto-fontface": "0.10.0",
 | 
				
			||||||
    "rrule": "2.8.1",
 | 
					    "rrule": "2.8.1",
 | 
				
			||||||
    "sortablejs": "1.15.2",
 | 
					    "sortablejs": "1.15.3",
 | 
				
			||||||
    "stacktrace-js": "2.0.2",
 | 
					    "stacktrace-js": "2.0.2",
 | 
				
			||||||
    "superstruct": "2.0.2",
 | 
					    "superstruct": "2.0.2",
 | 
				
			||||||
    "tinykeys": "3.0.0",
 | 
					    "tinykeys": "3.0.0",
 | 
				
			||||||
    "tsparticles-engine": "2.12.0",
 | 
					    "tsparticles-engine": "2.12.0",
 | 
				
			||||||
    "tsparticles-preset-links": "2.12.0",
 | 
					    "tsparticles-preset-links": "2.12.0",
 | 
				
			||||||
    "ua-parser-js": "1.0.38",
 | 
					    "ua-parser-js": "1.0.39",
 | 
				
			||||||
    "unfetch": "5.0.0",
 | 
					    "unfetch": "5.0.0",
 | 
				
			||||||
    "vis-data": "7.1.9",
 | 
					    "vis-data": "7.1.9",
 | 
				
			||||||
    "vis-network": "9.1.9",
 | 
					    "vis-network": "9.1.9",
 | 
				
			||||||
@@ -155,7 +156,7 @@
 | 
				
			|||||||
    "@babel/plugin-transform-runtime": "7.25.4",
 | 
					    "@babel/plugin-transform-runtime": "7.25.4",
 | 
				
			||||||
    "@babel/preset-env": "7.25.4",
 | 
					    "@babel/preset-env": "7.25.4",
 | 
				
			||||||
    "@babel/preset-typescript": "7.24.7",
 | 
					    "@babel/preset-typescript": "7.24.7",
 | 
				
			||||||
    "@bundle-stats/plugin-webpack-filter": "4.14.2",
 | 
					    "@bundle-stats/plugin-webpack-filter": "4.15.1",
 | 
				
			||||||
    "@koa/cors": "5.0.0",
 | 
					    "@koa/cors": "5.0.0",
 | 
				
			||||||
    "@lokalise/node-api": "12.7.0",
 | 
					    "@lokalise/node-api": "12.7.0",
 | 
				
			||||||
    "@octokit/auth-oauth-device": "7.1.1",
 | 
					    "@octokit/auth-oauth-device": "7.1.1",
 | 
				
			||||||
@@ -189,7 +190,7 @@
 | 
				
			|||||||
    "@typescript-eslint/parser": "7.18.0",
 | 
					    "@typescript-eslint/parser": "7.18.0",
 | 
				
			||||||
    "@web/dev-server": "0.1.38",
 | 
					    "@web/dev-server": "0.1.38",
 | 
				
			||||||
    "@web/dev-server-rollup": "0.4.1",
 | 
					    "@web/dev-server-rollup": "0.4.1",
 | 
				
			||||||
    "babel-loader": "9.1.3",
 | 
					    "babel-loader": "9.2.1",
 | 
				
			||||||
    "babel-plugin-template-html-minifier": "4.1.0",
 | 
					    "babel-plugin-template-html-minifier": "4.1.0",
 | 
				
			||||||
    "browserslist-useragent-regexp": "4.1.3",
 | 
					    "browserslist-useragent-regexp": "4.1.3",
 | 
				
			||||||
    "chai": "5.1.1",
 | 
					    "chai": "5.1.1",
 | 
				
			||||||
@@ -198,11 +199,11 @@
 | 
				
			|||||||
    "eslint-config-airbnb-base": "15.0.0",
 | 
					    "eslint-config-airbnb-base": "15.0.0",
 | 
				
			||||||
    "eslint-config-airbnb-typescript": "18.0.0",
 | 
					    "eslint-config-airbnb-typescript": "18.0.0",
 | 
				
			||||||
    "eslint-config-prettier": "9.1.0",
 | 
					    "eslint-config-prettier": "9.1.0",
 | 
				
			||||||
    "eslint-import-resolver-webpack": "0.13.8",
 | 
					    "eslint-import-resolver-webpack": "0.13.9",
 | 
				
			||||||
    "eslint-plugin-import": "2.29.1",
 | 
					    "eslint-plugin-import": "2.30.0",
 | 
				
			||||||
    "eslint-plugin-lit": "1.14.0",
 | 
					    "eslint-plugin-lit": "1.15.0",
 | 
				
			||||||
    "eslint-plugin-lit-a11y": "4.1.4",
 | 
					    "eslint-plugin-lit-a11y": "4.1.4",
 | 
				
			||||||
    "eslint-plugin-unused-imports": "4.1.3",
 | 
					    "eslint-plugin-unused-imports": "4.1.4",
 | 
				
			||||||
    "eslint-plugin-wc": "2.1.1",
 | 
					    "eslint-plugin-wc": "2.1.1",
 | 
				
			||||||
    "fancy-log": "2.0.0",
 | 
					    "fancy-log": "2.0.0",
 | 
				
			||||||
    "fs-extra": "11.2.0",
 | 
					    "fs-extra": "11.2.0",
 | 
				
			||||||
@@ -213,10 +214,10 @@
 | 
				
			|||||||
    "gulp-rename": "2.0.0",
 | 
					    "gulp-rename": "2.0.0",
 | 
				
			||||||
    "gulp-zopfli-green": "6.0.2",
 | 
					    "gulp-zopfli-green": "6.0.2",
 | 
				
			||||||
    "html-minifier-terser": "7.2.0",
 | 
					    "html-minifier-terser": "7.2.0",
 | 
				
			||||||
    "husky": "9.1.5",
 | 
					    "husky": "9.1.6",
 | 
				
			||||||
    "instant-mocha": "1.5.2",
 | 
					    "instant-mocha": "1.5.2",
 | 
				
			||||||
    "jszip": "3.10.1",
 | 
					    "jszip": "3.10.1",
 | 
				
			||||||
    "lint-staged": "15.2.9",
 | 
					    "lint-staged": "15.2.10",
 | 
				
			||||||
    "lit-analyzer": "2.0.3",
 | 
					    "lit-analyzer": "2.0.3",
 | 
				
			||||||
    "lodash.merge": "4.6.2",
 | 
					    "lodash.merge": "4.6.2",
 | 
				
			||||||
    "lodash.template": "4.5.0",
 | 
					    "lodash.template": "4.5.0",
 | 
				
			||||||
@@ -232,16 +233,16 @@
 | 
				
			|||||||
    "rollup-plugin-terser": "7.0.2",
 | 
					    "rollup-plugin-terser": "7.0.2",
 | 
				
			||||||
    "rollup-plugin-visualizer": "5.12.0",
 | 
					    "rollup-plugin-visualizer": "5.12.0",
 | 
				
			||||||
    "serve-handler": "6.1.5",
 | 
					    "serve-handler": "6.1.5",
 | 
				
			||||||
    "sinon": "18.0.0",
 | 
					    "sinon": "19.0.2",
 | 
				
			||||||
    "systemjs": "6.15.1",
 | 
					    "systemjs": "6.15.1",
 | 
				
			||||||
    "tar": "7.4.3",
 | 
					    "tar": "7.4.3",
 | 
				
			||||||
    "terser-webpack-plugin": "5.3.10",
 | 
					    "terser-webpack-plugin": "5.3.10",
 | 
				
			||||||
    "transform-async-modules-webpack-plugin": "1.1.1",
 | 
					    "transform-async-modules-webpack-plugin": "1.1.1",
 | 
				
			||||||
    "ts-lit-plugin": "2.0.2",
 | 
					    "ts-lit-plugin": "2.0.2",
 | 
				
			||||||
    "typescript": "5.5.4",
 | 
					    "typescript": "5.6.2",
 | 
				
			||||||
    "webpack": "5.94.0",
 | 
					    "webpack": "5.94.0",
 | 
				
			||||||
    "webpack-cli": "5.1.4",
 | 
					    "webpack-cli": "5.1.4",
 | 
				
			||||||
    "webpack-dev-server": "5.0.4",
 | 
					    "webpack-dev-server": "5.1.0",
 | 
				
			||||||
    "webpack-manifest-plugin": "5.0.0",
 | 
					    "webpack-manifest-plugin": "5.0.0",
 | 
				
			||||||
    "webpack-stats-plugin": "1.1.3",
 | 
					    "webpack-stats-plugin": "1.1.3",
 | 
				
			||||||
    "webpackbar": "6.0.1",
 | 
					    "webpackbar": "6.0.1",
 | 
				
			||||||
@@ -255,8 +256,8 @@
 | 
				
			|||||||
    "clean-css": "5.3.3",
 | 
					    "clean-css": "5.3.3",
 | 
				
			||||||
    "@lit/reactive-element": "1.6.3",
 | 
					    "@lit/reactive-element": "1.6.3",
 | 
				
			||||||
    "@fullcalendar/daygrid": "6.1.15",
 | 
					    "@fullcalendar/daygrid": "6.1.15",
 | 
				
			||||||
    "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch",
 | 
					    "sortablejs@1.15.3": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
 | 
				
			||||||
    "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
 | 
					    "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "packageManager": "yarn@4.4.0"
 | 
					  "packageManager": "yarn@4.5.0"
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[project]
 | 
					[project]
 | 
				
			||||||
name         = "home-assistant-frontend"
 | 
					name         = "home-assistant-frontend"
 | 
				
			||||||
version      = "20240809.0"
 | 
					version      = "20240909.1"
 | 
				
			||||||
license      = {text = "Apache-2.0"}
 | 
					license      = {text = "Apache-2.0"}
 | 
				
			||||||
description  = "The Home Assistant frontend"
 | 
					description  = "The Home Assistant frontend"
 | 
				
			||||||
readme       = "README.md"
 | 
					readme       = "README.md"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -234,7 +234,12 @@ export const SENSOR_ENTITIES = [
 | 
				
			|||||||
  "weather",
 | 
					  "weather",
 | 
				
			||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
 | 
					export const ASSIST_ENTITIES = [
 | 
				
			||||||
 | 
					  "assist_satellite",
 | 
				
			||||||
 | 
					  "conversation",
 | 
				
			||||||
 | 
					  "stt",
 | 
				
			||||||
 | 
					  "tts",
 | 
				
			||||||
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
/** Domains that render an input element instead of a text value when displayed in a row.
 | 
					/** Domains that render an input element instead of a text value when displayed in a row.
 | 
				
			||||||
 *  Those rows should then not show a cursor pointer when hovered (which would normally
 | 
					 *  Those rows should then not show a cursor pointer when hovered (which would normally
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -71,8 +71,7 @@ export const computeStateDisplayFromEntityAttributes = (
 | 
				
			|||||||
    if (
 | 
					    if (
 | 
				
			||||||
      attributes.device_class === "duration" &&
 | 
					      attributes.device_class === "duration" &&
 | 
				
			||||||
      attributes.unit_of_measurement &&
 | 
					      attributes.unit_of_measurement &&
 | 
				
			||||||
      UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] &&
 | 
					      UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement]
 | 
				
			||||||
      entity?.display_precision === undefined
 | 
					 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      try {
 | 
					      try {
 | 
				
			||||||
        return formatDuration(state, attributes.unit_of_measurement);
 | 
					        return formatDuration(state, attributes.unit_of_measurement);
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								src/components/chart/click_is_touch.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								src/components/chart/click_is_touch.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
				
			|||||||
 | 
					import type { ChartEvent } from "chart.js";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const clickIsTouch = (event: ChartEvent): boolean =>
 | 
				
			||||||
 | 
					  !(event.native instanceof MouseEvent) ||
 | 
				
			||||||
 | 
					  (event.native instanceof PointerEvent &&
 | 
				
			||||||
 | 
					    event.native.pointerType !== "mouse");
 | 
				
			||||||
@@ -16,6 +16,7 @@ import {
 | 
				
			|||||||
  HaChartBase,
 | 
					  HaChartBase,
 | 
				
			||||||
  MIN_TIME_BETWEEN_UPDATES,
 | 
					  MIN_TIME_BETWEEN_UPDATES,
 | 
				
			||||||
} from "./ha-chart-base";
 | 
					} from "./ha-chart-base";
 | 
				
			||||||
 | 
					import { clickIsTouch } from "./click_is_touch";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const safeParseFloat = (value) => {
 | 
					const safeParseFloat = (value) => {
 | 
				
			||||||
  const parsed = parseFloat(value);
 | 
					  const parsed = parseFloat(value);
 | 
				
			||||||
@@ -220,12 +221,7 @@ export class StateHistoryChartLine extends LitElement {
 | 
				
			|||||||
        // @ts-expect-error
 | 
					        // @ts-expect-error
 | 
				
			||||||
        locale: numberFormatToLocale(this.hass.locale),
 | 
					        locale: numberFormatToLocale(this.hass.locale),
 | 
				
			||||||
        onClick: (e: any) => {
 | 
					        onClick: (e: any) => {
 | 
				
			||||||
          if (
 | 
					          if (!this.clickForMoreInfo || clickIsTouch(e)) {
 | 
				
			||||||
            !this.clickForMoreInfo ||
 | 
					 | 
				
			||||||
            !(e.native instanceof MouseEvent) ||
 | 
					 | 
				
			||||||
            (e.native instanceof PointerEvent &&
 | 
					 | 
				
			||||||
              e.native.pointerType !== "mouse")
 | 
					 | 
				
			||||||
          ) {
 | 
					 | 
				
			||||||
            return;
 | 
					            return;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,6 +16,7 @@ import {
 | 
				
			|||||||
} from "./ha-chart-base";
 | 
					} from "./ha-chart-base";
 | 
				
			||||||
import type { TimeLineData } from "./timeline-chart/const";
 | 
					import type { TimeLineData } from "./timeline-chart/const";
 | 
				
			||||||
import { computeTimelineColor } from "./timeline-chart/timeline-color";
 | 
					import { computeTimelineColor } from "./timeline-chart/timeline-color";
 | 
				
			||||||
 | 
					import { clickIsTouch } from "./click_is_touch";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("state-history-chart-timeline")
 | 
					@customElement("state-history-chart-timeline")
 | 
				
			||||||
export class StateHistoryChartTimeline extends LitElement {
 | 
					export class StateHistoryChartTimeline extends LitElement {
 | 
				
			||||||
@@ -224,11 +225,7 @@ export class StateHistoryChartTimeline extends LitElement {
 | 
				
			|||||||
      // @ts-expect-error
 | 
					      // @ts-expect-error
 | 
				
			||||||
      locale: numberFormatToLocale(this.hass.locale),
 | 
					      locale: numberFormatToLocale(this.hass.locale),
 | 
				
			||||||
      onClick: (e: any) => {
 | 
					      onClick: (e: any) => {
 | 
				
			||||||
        if (
 | 
					        if (!this.clickForMoreInfo || clickIsTouch(e)) {
 | 
				
			||||||
          !this.clickForMoreInfo ||
 | 
					 | 
				
			||||||
          !(e.native instanceof MouseEvent) ||
 | 
					 | 
				
			||||||
          (e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -39,6 +39,7 @@ import type {
 | 
				
			|||||||
  ChartDatasetExtra,
 | 
					  ChartDatasetExtra,
 | 
				
			||||||
  HaChartBase,
 | 
					  HaChartBase,
 | 
				
			||||||
} from "./ha-chart-base";
 | 
					} from "./ha-chart-base";
 | 
				
			||||||
 | 
					import { clickIsTouch } from "./click_is_touch";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
 | 
					export const supportedStatTypeMap: Record<StatisticType, StatisticType> = {
 | 
				
			||||||
  mean: "mean",
 | 
					  mean: "mean",
 | 
				
			||||||
@@ -278,11 +279,7 @@ export class StatisticsChart extends LitElement {
 | 
				
			|||||||
      // @ts-expect-error
 | 
					      // @ts-expect-error
 | 
				
			||||||
      locale: numberFormatToLocale(this.hass.locale),
 | 
					      locale: numberFormatToLocale(this.hass.locale),
 | 
				
			||||||
      onClick: (e: any) => {
 | 
					      onClick: (e: any) => {
 | 
				
			||||||
        if (
 | 
					        if (!this.clickForMoreInfo || clickIsTouch(e)) {
 | 
				
			||||||
          !this.clickForMoreInfo ||
 | 
					 | 
				
			||||||
          !(e.native instanceof MouseEvent) ||
 | 
					 | 
				
			||||||
          (e.native instanceof PointerEvent && e.native.pointerType !== "mouse")
 | 
					 | 
				
			||||||
        ) {
 | 
					 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -25,7 +25,6 @@ import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			|||||||
import { stringCompare } from "../../common/string/compare";
 | 
					import { stringCompare } from "../../common/string/compare";
 | 
				
			||||||
import { debounce } from "../../common/util/debounce";
 | 
					import { debounce } from "../../common/util/debounce";
 | 
				
			||||||
import { groupBy } from "../../common/util/group-by";
 | 
					import { groupBy } from "../../common/util/group-by";
 | 
				
			||||||
import { nextRender } from "../../common/util/render-status";
 | 
					 | 
				
			||||||
import { haStyleScrollbar } from "../../resources/styles";
 | 
					import { haStyleScrollbar } from "../../resources/styles";
 | 
				
			||||||
import { loadVirtualizer } from "../../resources/virtualizer";
 | 
					import { loadVirtualizer } from "../../resources/virtualizer";
 | 
				
			||||||
import { HomeAssistant } from "../../types";
 | 
					import { HomeAssistant } from "../../types";
 | 
				
			||||||
@@ -35,6 +34,7 @@ import "../ha-svg-icon";
 | 
				
			|||||||
import "../search-input";
 | 
					import "../search-input";
 | 
				
			||||||
import { filterData, sortData } from "./sort-filter";
 | 
					import { filterData, sortData } from "./sort-filter";
 | 
				
			||||||
import { LocalizeFunc } from "../../common/translations/localize";
 | 
					import { LocalizeFunc } from "../../common/translations/localize";
 | 
				
			||||||
 | 
					import { nextRender } from "../../common/util/render-status";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface RowClickedEvent {
 | 
					export interface RowClickedEvent {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
@@ -169,8 +169,6 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @query("slot[name='header']") private _header!: HTMLSlotElement;
 | 
					  @query("slot[name='header']") private _header!: HTMLSlotElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _items: DataTableRowData[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _collapsedGroups: string[] = [];
 | 
					  @state() private _collapsedGroups: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _checkableRowsCount?: number;
 | 
					  private _checkableRowsCount?: number;
 | 
				
			||||||
@@ -179,7 +177,9 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _sortColumns: SortableColumnContainer = {};
 | 
					  private _sortColumns: SortableColumnContainer = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private curRequest = 0;
 | 
					  private _curRequest = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _lastUpdate = 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // @ts-ignore
 | 
					  // @ts-ignore
 | 
				
			||||||
  @restoreScroll(".scroller") private _savedScrollPos?: number;
 | 
					  @restoreScroll(".scroller") private _savedScrollPos?: number;
 | 
				
			||||||
@@ -206,9 +206,9 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  public connectedCallback() {
 | 
					  public connectedCallback() {
 | 
				
			||||||
    super.connectedCallback();
 | 
					    super.connectedCallback();
 | 
				
			||||||
    if (this._items.length) {
 | 
					    if (this._filteredData.length) {
 | 
				
			||||||
      // Force update of location of rows
 | 
					      // Force update of location of rows
 | 
				
			||||||
      this._items = [...this._items];
 | 
					      this._filteredData = [...this._filteredData];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -291,16 +291,13 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
      properties.has("columns") ||
 | 
					      properties.has("columns") ||
 | 
				
			||||||
      properties.has("_filter") ||
 | 
					      properties.has("_filter") ||
 | 
				
			||||||
      properties.has("sortColumn") ||
 | 
					      properties.has("sortColumn") ||
 | 
				
			||||||
      properties.has("sortDirection") ||
 | 
					      properties.has("sortDirection")
 | 
				
			||||||
      properties.has("groupColumn") ||
 | 
					 | 
				
			||||||
      properties.has("groupOrder") ||
 | 
					 | 
				
			||||||
      properties.has("_collapsedGroups")
 | 
					 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      this._sortFilterData();
 | 
					      this._sortFilterData();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (properties.has("selectable") || properties.has("hiddenColumns")) {
 | 
					    if (properties.has("selectable") || properties.has("hiddenColumns")) {
 | 
				
			||||||
      this._items = [...this._items];
 | 
					      this._filteredData = [...this._filteredData];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -467,7 +464,15 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
                  scroller
 | 
					                  scroller
 | 
				
			||||||
                  class="mdc-data-table__content scroller ha-scrollbar"
 | 
					                  class="mdc-data-table__content scroller ha-scrollbar"
 | 
				
			||||||
                  @scroll=${this._saveScrollPos}
 | 
					                  @scroll=${this._saveScrollPos}
 | 
				
			||||||
                  .items=${this._items}
 | 
					                  .items=${this._groupData(
 | 
				
			||||||
 | 
					                    this._filteredData,
 | 
				
			||||||
 | 
					                    localize,
 | 
				
			||||||
 | 
					                    this.appendRow,
 | 
				
			||||||
 | 
					                    this.hasFab,
 | 
				
			||||||
 | 
					                    this.groupColumn,
 | 
				
			||||||
 | 
					                    this.groupOrder,
 | 
				
			||||||
 | 
					                    this._collapsedGroups
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
                  .keyFunction=${this._keyFunction}
 | 
					                  .keyFunction=${this._keyFunction}
 | 
				
			||||||
                  .renderItem=${renderRow}
 | 
					                  .renderItem=${renderRow}
 | 
				
			||||||
                ></lit-virtualizer>
 | 
					                ></lit-virtualizer>
 | 
				
			||||||
@@ -602,8 +607,13 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private async _sortFilterData() {
 | 
					  private async _sortFilterData() {
 | 
				
			||||||
    const startTime = new Date().getTime();
 | 
					    const startTime = new Date().getTime();
 | 
				
			||||||
    this.curRequest++;
 | 
					    const timeBetweenUpdate = startTime - this._lastUpdate;
 | 
				
			||||||
    const curRequest = this.curRequest;
 | 
					    const timeBetweenRequest = startTime - this._curRequest;
 | 
				
			||||||
 | 
					    this._curRequest = startTime;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const forceUpdate =
 | 
				
			||||||
 | 
					      !this._lastUpdate ||
 | 
				
			||||||
 | 
					      (timeBetweenUpdate > 500 && timeBetweenRequest < 500);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let filteredData = this.data;
 | 
					    let filteredData = this.data;
 | 
				
			||||||
    if (this._filter) {
 | 
					    if (this._filter) {
 | 
				
			||||||
@@ -614,6 +624,10 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!forceUpdate && this._curRequest !== startTime) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const prom = this.sortColumn
 | 
					    const prom = this.sortColumn
 | 
				
			||||||
      ? sortData(
 | 
					      ? sortData(
 | 
				
			||||||
          filteredData,
 | 
					          filteredData,
 | 
				
			||||||
@@ -634,91 +648,103 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
        setTimeout(resolve, 100 - elapsed);
 | 
					        setTimeout(resolve, 100 - elapsed);
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (this.curRequest !== curRequest) {
 | 
					
 | 
				
			||||||
 | 
					    if (!forceUpdate && this._curRequest !== startTime) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const localize = this.localizeFunc || this.hass.localize;
 | 
					    this._lastUpdate = startTime;
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.appendRow || this.hasFab || this.groupColumn) {
 | 
					 | 
				
			||||||
      let items = [...data];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.groupColumn) {
 | 
					 | 
				
			||||||
        const grouped = groupBy(items, (item) => item[this.groupColumn!]);
 | 
					 | 
				
			||||||
        if (grouped.undefined) {
 | 
					 | 
				
			||||||
          // make sure ungrouped items are at the bottom
 | 
					 | 
				
			||||||
          grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
 | 
					 | 
				
			||||||
          delete grouped.undefined;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        const sorted: {
 | 
					 | 
				
			||||||
          [key: string]: DataTableRowData[];
 | 
					 | 
				
			||||||
        } = Object.keys(grouped)
 | 
					 | 
				
			||||||
          .sort((a, b) => {
 | 
					 | 
				
			||||||
            const orderA = this.groupOrder?.indexOf(a) ?? -1;
 | 
					 | 
				
			||||||
            const orderB = this.groupOrder?.indexOf(b) ?? -1;
 | 
					 | 
				
			||||||
            if (orderA !== orderB) {
 | 
					 | 
				
			||||||
              if (orderA === -1) {
 | 
					 | 
				
			||||||
                return 1;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
              if (orderB === -1) {
 | 
					 | 
				
			||||||
                return -1;
 | 
					 | 
				
			||||||
              }
 | 
					 | 
				
			||||||
              return orderA - orderB;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            return stringCompare(
 | 
					 | 
				
			||||||
              ["", "-", "—"].includes(a) ? "zzz" : a,
 | 
					 | 
				
			||||||
              ["", "-", "—"].includes(b) ? "zzz" : b,
 | 
					 | 
				
			||||||
              this.hass.locale.language
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
          .reduce((obj, key) => {
 | 
					 | 
				
			||||||
            obj[key] = grouped[key];
 | 
					 | 
				
			||||||
            return obj;
 | 
					 | 
				
			||||||
          }, {});
 | 
					 | 
				
			||||||
        const groupedItems: DataTableRowData[] = [];
 | 
					 | 
				
			||||||
        Object.entries(sorted).forEach(([groupName, rows]) => {
 | 
					 | 
				
			||||||
          groupedItems.push({
 | 
					 | 
				
			||||||
            append: true,
 | 
					 | 
				
			||||||
            content: html`<div
 | 
					 | 
				
			||||||
              class="mdc-data-table__cell group-header"
 | 
					 | 
				
			||||||
              role="cell"
 | 
					 | 
				
			||||||
              .group=${groupName}
 | 
					 | 
				
			||||||
              @click=${this._collapseGroup}
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
              <ha-icon-button
 | 
					 | 
				
			||||||
                .path=${mdiChevronUp}
 | 
					 | 
				
			||||||
                class=${this._collapsedGroups.includes(groupName)
 | 
					 | 
				
			||||||
                  ? "collapsed"
 | 
					 | 
				
			||||||
                  : ""}
 | 
					 | 
				
			||||||
              >
 | 
					 | 
				
			||||||
              </ha-icon-button>
 | 
					 | 
				
			||||||
              ${groupName === UNDEFINED_GROUP_KEY
 | 
					 | 
				
			||||||
                ? localize("ui.components.data-table.ungrouped")
 | 
					 | 
				
			||||||
                : groupName || ""}
 | 
					 | 
				
			||||||
            </div>`,
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
          if (!this._collapsedGroups.includes(groupName)) {
 | 
					 | 
				
			||||||
            groupedItems.push(...rows);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
        items = groupedItems;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.appendRow) {
 | 
					 | 
				
			||||||
        items.push({ append: true, content: this.appendRow });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (this.hasFab) {
 | 
					 | 
				
			||||||
        items.push({ empty: true });
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this._items = items;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this._items = data;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this._filteredData = data;
 | 
					    this._filteredData = data;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _groupData = memoizeOne(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					      data: DataTableRowData[],
 | 
				
			||||||
 | 
					      localize: LocalizeFunc,
 | 
				
			||||||
 | 
					      appendRow,
 | 
				
			||||||
 | 
					      hasFab: boolean,
 | 
				
			||||||
 | 
					      groupColumn: string | undefined,
 | 
				
			||||||
 | 
					      groupOrder: string[] | undefined,
 | 
				
			||||||
 | 
					      collapsedGroups: string[]
 | 
				
			||||||
 | 
					    ) => {
 | 
				
			||||||
 | 
					      if (appendRow || hasFab || groupColumn) {
 | 
				
			||||||
 | 
					        let items = [...data];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (groupColumn) {
 | 
				
			||||||
 | 
					          const grouped = groupBy(items, (item) => item[groupColumn]);
 | 
				
			||||||
 | 
					          if (grouped.undefined) {
 | 
				
			||||||
 | 
					            // make sure ungrouped items are at the bottom
 | 
				
			||||||
 | 
					            grouped[UNDEFINED_GROUP_KEY] = grouped.undefined;
 | 
				
			||||||
 | 
					            delete grouped.undefined;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          const sorted: {
 | 
				
			||||||
 | 
					            [key: string]: DataTableRowData[];
 | 
				
			||||||
 | 
					          } = Object.keys(grouped)
 | 
				
			||||||
 | 
					            .sort((a, b) => {
 | 
				
			||||||
 | 
					              const orderA = groupOrder?.indexOf(a) ?? -1;
 | 
				
			||||||
 | 
					              const orderB = groupOrder?.indexOf(b) ?? -1;
 | 
				
			||||||
 | 
					              if (orderA !== orderB) {
 | 
				
			||||||
 | 
					                if (orderA === -1) {
 | 
				
			||||||
 | 
					                  return 1;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                if (orderB === -1) {
 | 
				
			||||||
 | 
					                  return -1;
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return orderA - orderB;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              return stringCompare(
 | 
				
			||||||
 | 
					                ["", "-", "—"].includes(a) ? "zzz" : a,
 | 
				
			||||||
 | 
					                ["", "-", "—"].includes(b) ? "zzz" : b,
 | 
				
			||||||
 | 
					                this.hass.locale.language
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .reduce((obj, key) => {
 | 
				
			||||||
 | 
					              obj[key] = grouped[key];
 | 
				
			||||||
 | 
					              return obj;
 | 
				
			||||||
 | 
					            }, {});
 | 
				
			||||||
 | 
					          const groupedItems: DataTableRowData[] = [];
 | 
				
			||||||
 | 
					          Object.entries(sorted).forEach(([groupName, rows]) => {
 | 
				
			||||||
 | 
					            groupedItems.push({
 | 
				
			||||||
 | 
					              append: true,
 | 
				
			||||||
 | 
					              content: html`<div
 | 
				
			||||||
 | 
					                class="mdc-data-table__cell group-header"
 | 
				
			||||||
 | 
					                role="cell"
 | 
				
			||||||
 | 
					                .group=${groupName}
 | 
				
			||||||
 | 
					                @click=${this._collapseGroup}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <ha-icon-button
 | 
				
			||||||
 | 
					                  .path=${mdiChevronUp}
 | 
				
			||||||
 | 
					                  class=${collapsedGroups.includes(groupName)
 | 
				
			||||||
 | 
					                    ? "collapsed"
 | 
				
			||||||
 | 
					                    : ""}
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					                </ha-icon-button>
 | 
				
			||||||
 | 
					                ${groupName === UNDEFINED_GROUP_KEY
 | 
				
			||||||
 | 
					                  ? localize("ui.components.data-table.ungrouped")
 | 
				
			||||||
 | 
					                  : groupName || ""}
 | 
				
			||||||
 | 
					              </div>`,
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					            if (!collapsedGroups.includes(groupName)) {
 | 
				
			||||||
 | 
					              groupedItems.push(...rows);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          items = groupedItems;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (appendRow) {
 | 
				
			||||||
 | 
					          items.push({ append: true, content: appendRow });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (hasFab) {
 | 
				
			||||||
 | 
					          items.push({ empty: true });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return items;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return data;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _memFilterData = memoizeOne(
 | 
					  private _memFilterData = memoizeOne(
 | 
				
			||||||
    (
 | 
					    (
 | 
				
			||||||
      data: DataTableRowData[],
 | 
					      data: DataTableRowData[],
 | 
				
			||||||
@@ -802,8 +828,8 @@ export class HaDataTable extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _checkedRowsChanged() {
 | 
					  private _checkedRowsChanged() {
 | 
				
			||||||
    // force scroller to update, change it's items
 | 
					    // force scroller to update, change it's items
 | 
				
			||||||
    if (this._items.length) {
 | 
					    if (this._filteredData.length) {
 | 
				
			||||||
      this._items = [...this._items];
 | 
					      this._filteredData = [...this._filteredData];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    fireEvent(this, "selection-changed", {
 | 
					    fireEvent(this, "selection-changed", {
 | 
				
			||||||
      value: this._checkedRows,
 | 
					      value: this._checkedRows,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import { mdiTextureBox } from "@mdi/js";
 | 
					import { mdiTextureBox } from "@mdi/js";
 | 
				
			||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
					import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
				
			||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
					import { HassEntity } from "home-assistant-js-websocket";
 | 
				
			||||||
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
 | 
					import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
import { styleMap } from "lit/directives/style-map";
 | 
					import { styleMap } from "lit/directives/style-map";
 | 
				
			||||||
@@ -20,12 +20,7 @@ import {
 | 
				
			|||||||
  getDeviceEntityDisplayLookup,
 | 
					  getDeviceEntityDisplayLookup,
 | 
				
			||||||
} from "../data/device_registry";
 | 
					} from "../data/device_registry";
 | 
				
			||||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
					import { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
				
			||||||
import {
 | 
					import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
 | 
				
			||||||
  FloorRegistryEntry,
 | 
					 | 
				
			||||||
  getFloorAreaLookup,
 | 
					 | 
				
			||||||
  subscribeFloorRegistry,
 | 
					 | 
				
			||||||
} from "../data/floor_registry";
 | 
					 | 
				
			||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
 | 
					 | 
				
			||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
 | 
					import { HomeAssistant, ValueChangedEvent } from "../types";
 | 
				
			||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
					import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
				
			||||||
import "./ha-combo-box";
 | 
					import "./ha-combo-box";
 | 
				
			||||||
@@ -50,7 +45,7 @@ interface FloorAreaEntry {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-area-floor-picker")
 | 
					@customElement("ha-area-floor-picker")
 | 
				
			||||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
 | 
					export class HaAreaFloorPicker extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property() public label?: string;
 | 
					  @property() public label?: string;
 | 
				
			||||||
@@ -111,22 +106,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property({ type: Boolean }) public required = false;
 | 
					  @property({ type: Boolean }) public required = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _opened?: boolean;
 | 
					  @state() private _opened?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("ha-combo-box", true) public comboBox!: HaComboBox;
 | 
					  @query("ha-combo-box", true) public comboBox!: HaComboBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _init = false;
 | 
					  private _init = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
					 | 
				
			||||||
    return [
 | 
					 | 
				
			||||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
					 | 
				
			||||||
        this._floors = floors;
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public async open() {
 | 
					  public async open() {
 | 
				
			||||||
    await this.updateComplete;
 | 
					    await this.updateComplete;
 | 
				
			||||||
    await this.comboBox?.open();
 | 
					    await this.comboBox?.open();
 | 
				
			||||||
@@ -431,12 +416,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  protected updated(changedProps: PropertyValues) {
 | 
					  protected updated(changedProps: PropertyValues) {
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
      (!this._init && this.hass && this._floors) ||
 | 
					      (!this._init && this.hass) ||
 | 
				
			||||||
      (this._init && changedProps.has("_opened") && this._opened)
 | 
					      (this._init && changedProps.has("_opened") && this._opened)
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      this._init = true;
 | 
					      this._init = true;
 | 
				
			||||||
      const areas = this._getAreas(
 | 
					      const areas = this._getAreas(
 | 
				
			||||||
        this._floors!,
 | 
					        Object.values(this.hass.floors),
 | 
				
			||||||
        Object.values(this.hass.areas),
 | 
					        Object.values(this.hass.areas),
 | 
				
			||||||
        Object.values(this.hass.devices),
 | 
					        Object.values(this.hass.devices),
 | 
				
			||||||
        Object.values(this.hass.entities),
 | 
					        Object.values(this.hass.entities),
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										155
									
								
								src/components/ha-badge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										155
									
								
								src/components/ha-badge.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,155 @@
 | 
				
			|||||||
 | 
					import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
 | 
					import { classMap } from "lit/directives/class-map";
 | 
				
			||||||
 | 
					import { ifDefined } from "lit/directives/if-defined";
 | 
				
			||||||
 | 
					import "./ha-ripple";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					type BadgeType = "badge" | "button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("ha-badge")
 | 
				
			||||||
 | 
					export class HaBadge extends LitElement {
 | 
				
			||||||
 | 
					  @property() public type: BadgeType = "badge";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public label?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ type: Boolean, attribute: "icon-only" }) iconOnly = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected render() {
 | 
				
			||||||
 | 
					    const label = this.label;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <div
 | 
				
			||||||
 | 
					        class="badge ${classMap({
 | 
				
			||||||
 | 
					          "icon-only": this.iconOnly,
 | 
				
			||||||
 | 
					        })}"
 | 
				
			||||||
 | 
					        role=${ifDefined(this.type === "button" ? "button" : undefined)}
 | 
				
			||||||
 | 
					        tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        <ha-ripple .disabled=${this.type !== "button"}></ha-ripple>
 | 
				
			||||||
 | 
					        <slot name="icon"></slot>
 | 
				
			||||||
 | 
					        ${this.iconOnly
 | 
				
			||||||
 | 
					          ? nothing
 | 
				
			||||||
 | 
					          : html`<span class="info">
 | 
				
			||||||
 | 
					              ${label ? html`<span class="label">${label}</span>` : nothing}
 | 
				
			||||||
 | 
					              <span class="content"><slot></slot></span>
 | 
				
			||||||
 | 
					            </span>`}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static get styles(): CSSResultGroup {
 | 
				
			||||||
 | 
					    return css`
 | 
				
			||||||
 | 
					      :host {
 | 
				
			||||||
 | 
					        --badge-color: var(--secondary-text-color);
 | 
				
			||||||
 | 
					        -webkit-tap-highlight-color: transparent;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .badge {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        --ha-ripple-color: var(--badge-color);
 | 
				
			||||||
 | 
					        --ha-ripple-hover-opacity: 0.04;
 | 
				
			||||||
 | 
					        --ha-ripple-pressed-opacity: 0.12;
 | 
				
			||||||
 | 
					        transition:
 | 
				
			||||||
 | 
					          box-shadow 180ms ease-in-out,
 | 
				
			||||||
 | 
					          border-color 180ms ease-in-out;
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-direction: row;
 | 
				
			||||||
 | 
					        align-items: center;
 | 
				
			||||||
 | 
					        justify-content: center;
 | 
				
			||||||
 | 
					        gap: 8px;
 | 
				
			||||||
 | 
					        height: var(--ha-badge-size, 36px);
 | 
				
			||||||
 | 
					        min-width: var(--ha-badge-size, 36px);
 | 
				
			||||||
 | 
					        padding: 0px 12px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        width: auto;
 | 
				
			||||||
 | 
					        border-radius: var(
 | 
				
			||||||
 | 
					          --ha-badge-border-radius,
 | 
				
			||||||
 | 
					          calc(var(--ha-badge-size, 36px) / 2)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        background: var(
 | 
				
			||||||
 | 
					          --ha-card-background,
 | 
				
			||||||
 | 
					          var(--card-background-color, white)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        -webkit-backdrop-filter: var(--ha-card-backdrop-filter, none);
 | 
				
			||||||
 | 
					        backdrop-filter: var(--ha-card-backdrop-filter, none);
 | 
				
			||||||
 | 
					        border-width: var(--ha-card-border-width, 1px);
 | 
				
			||||||
 | 
					        box-shadow: var(--ha-card-box-shadow, none);
 | 
				
			||||||
 | 
					        border-style: solid;
 | 
				
			||||||
 | 
					        border-color: var(
 | 
				
			||||||
 | 
					          --ha-card-border-color,
 | 
				
			||||||
 | 
					          var(--divider-color, #e0e0e0)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .badge:focus-visible {
 | 
				
			||||||
 | 
					        --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent);
 | 
				
			||||||
 | 
					        --shadow-focus: 0 0 0 1px var(--badge-color);
 | 
				
			||||||
 | 
					        border-color: var(--badge-color);
 | 
				
			||||||
 | 
					        box-shadow: var(--shadow-default), var(--shadow-focus);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      [role="button"] {
 | 
				
			||||||
 | 
					        cursor: pointer;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      [role="button"]:focus {
 | 
				
			||||||
 | 
					        outline: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .info {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        flex-direction: column;
 | 
				
			||||||
 | 
					        align-items: flex-start;
 | 
				
			||||||
 | 
					        padding-inline-start: initial;
 | 
				
			||||||
 | 
					        text-align: center;
 | 
				
			||||||
 | 
					        font-family: Roboto;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .label {
 | 
				
			||||||
 | 
					        font-size: 10px;
 | 
				
			||||||
 | 
					        font-style: normal;
 | 
				
			||||||
 | 
					        font-weight: 500;
 | 
				
			||||||
 | 
					        line-height: 10px;
 | 
				
			||||||
 | 
					        letter-spacing: 0.1px;
 | 
				
			||||||
 | 
					        color: var(--secondary-text-color);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .content {
 | 
				
			||||||
 | 
					        font-size: 12px;
 | 
				
			||||||
 | 
					        font-style: normal;
 | 
				
			||||||
 | 
					        font-weight: 500;
 | 
				
			||||||
 | 
					        line-height: 16px;
 | 
				
			||||||
 | 
					        letter-spacing: 0.1px;
 | 
				
			||||||
 | 
					        color: var(--primary-text-color);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      ::slotted([slot="icon"]) {
 | 
				
			||||||
 | 
					        --mdc-icon-size: 18px;
 | 
				
			||||||
 | 
					        color: var(--badge-color);
 | 
				
			||||||
 | 
					        line-height: 0;
 | 
				
			||||||
 | 
					        margin-left: -4px;
 | 
				
			||||||
 | 
					        margin-right: 0;
 | 
				
			||||||
 | 
					        margin-inline-start: -4px;
 | 
				
			||||||
 | 
					        margin-inline-end: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      ::slotted(img[slot="icon"]) {
 | 
				
			||||||
 | 
					        width: 30px;
 | 
				
			||||||
 | 
					        height: 30px;
 | 
				
			||||||
 | 
					        border-radius: 50%;
 | 
				
			||||||
 | 
					        object-fit: cover;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					        margin-left: -10px;
 | 
				
			||||||
 | 
					        margin-right: 0;
 | 
				
			||||||
 | 
					        margin-inline-start: -10px;
 | 
				
			||||||
 | 
					        margin-inline-end: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .badge.icon-only {
 | 
				
			||||||
 | 
					        padding: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .badge.icon-only ::slotted([slot="icon"]) {
 | 
				
			||||||
 | 
					        margin-left: 0;
 | 
				
			||||||
 | 
					        margin-right: 0;
 | 
				
			||||||
 | 
					        margin-inline-start: 0;
 | 
				
			||||||
 | 
					        margin-inline-end: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					    "ha-badge": HaBadge;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -45,15 +45,35 @@ export class HaConversationAgentPicker extends LitElement {
 | 
				
			|||||||
    if (!this._agents) {
 | 
					    if (!this._agents) {
 | 
				
			||||||
      return nothing;
 | 
					      return nothing;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const value =
 | 
					    let value = this.value;
 | 
				
			||||||
      this.value ??
 | 
					    if (!value && this.required) {
 | 
				
			||||||
      (this.required &&
 | 
					      // Select Home Assistant conversation agent if it supports the language
 | 
				
			||||||
      (!this.language ||
 | 
					      for (const agent of this._agents) {
 | 
				
			||||||
        this._agents
 | 
					        if (
 | 
				
			||||||
          .find((agent) => agent.id === "homeassistant")
 | 
					          agent.id === "conversation.home_assistant" &&
 | 
				
			||||||
          ?.supported_languages.includes(this.language))
 | 
					          agent.supported_languages.includes(this.language!)
 | 
				
			||||||
        ? "homeassistant"
 | 
					        ) {
 | 
				
			||||||
        : NONE);
 | 
					          value = agent.id;
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!value) {
 | 
				
			||||||
 | 
					        // Select the first agent that supports the language
 | 
				
			||||||
 | 
					        for (const agent of this._agents) {
 | 
				
			||||||
 | 
					          if (
 | 
				
			||||||
 | 
					            agent.supported_languages === "*" &&
 | 
				
			||||||
 | 
					            agent.supported_languages.includes(this.language!)
 | 
				
			||||||
 | 
					          ) {
 | 
				
			||||||
 | 
					            value = agent.id;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!value) {
 | 
				
			||||||
 | 
					      value = NONE;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-select
 | 
					      <ha-select
 | 
				
			||||||
        .label=${this.label ||
 | 
					        .label=${this.label ||
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,8 +68,8 @@ export class HaExpansionPanel extends LitElement {
 | 
				
			|||||||
                ></ha-svg-icon>
 | 
					                ></ha-svg-icon>
 | 
				
			||||||
              `
 | 
					              `
 | 
				
			||||||
            : ""}
 | 
					            : ""}
 | 
				
			||||||
 | 
					          <slot name="icons"></slot>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <slot name="icons"></slot>
 | 
					 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div
 | 
					      <div
 | 
				
			||||||
        class="container ${classMap({ expanded: this.expanded })}"
 | 
					        class="container ${classMap({ expanded: this.expanded })}"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,5 @@
 | 
				
			|||||||
import "@material/mwc-menu/mwc-menu-surface";
 | 
					import "@material/mwc-menu/mwc-menu-surface";
 | 
				
			||||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
 | 
					import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
 | 
				
			||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  CSSResultGroup,
 | 
					  CSSResultGroup,
 | 
				
			||||||
  LitElement,
 | 
					  LitElement,
 | 
				
			||||||
@@ -15,13 +14,8 @@ import { repeat } from "lit/directives/repeat";
 | 
				
			|||||||
import memoizeOne from "memoize-one";
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
import { fireEvent } from "../common/dom/fire_event";
 | 
					import { fireEvent } from "../common/dom/fire_event";
 | 
				
			||||||
import { computeRTL } from "../common/util/compute_rtl";
 | 
					import { computeRTL } from "../common/util/compute_rtl";
 | 
				
			||||||
import {
 | 
					import { getFloorAreaLookup } from "../data/floor_registry";
 | 
				
			||||||
  FloorRegistryEntry,
 | 
					 | 
				
			||||||
  getFloorAreaLookup,
 | 
					 | 
				
			||||||
  subscribeFloorRegistry,
 | 
					 | 
				
			||||||
} from "../data/floor_registry";
 | 
					 | 
				
			||||||
import { RelatedResult, findRelated } from "../data/search";
 | 
					import { RelatedResult, findRelated } from "../data/search";
 | 
				
			||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
 | 
					 | 
				
			||||||
import { haStyleScrollbar } from "../resources/styles";
 | 
					import { haStyleScrollbar } from "../resources/styles";
 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
import "./ha-check-list-item";
 | 
					import "./ha-check-list-item";
 | 
				
			||||||
@@ -31,7 +25,7 @@ import "./ha-svg-icon";
 | 
				
			|||||||
import "./ha-tree-indicator";
 | 
					import "./ha-tree-indicator";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-filter-floor-areas")
 | 
					@customElement("ha-filter-floor-areas")
 | 
				
			||||||
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
					export class HaFilterFloorAreas extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public value?: {
 | 
					  @property({ attribute: false }) public value?: {
 | 
				
			||||||
@@ -47,8 +41,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _shouldRender = false;
 | 
					  @state() private _shouldRender = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public willUpdate(properties: PropertyValues) {
 | 
					  public willUpdate(properties: PropertyValues) {
 | 
				
			||||||
    super.willUpdate(properties);
 | 
					    super.willUpdate(properties);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -60,7 +52,7 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected render() {
 | 
					  protected render() {
 | 
				
			||||||
    const areas = this._areas(this.hass.areas, this._floors);
 | 
					    const areas = this._areas(this.hass.areas, this.hass.floors);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-expansion-panel
 | 
					      <ha-expansion-panel
 | 
				
			||||||
@@ -189,14 +181,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
    this._findRelated();
 | 
					    this._findRelated();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
					 | 
				
			||||||
    return [
 | 
					 | 
				
			||||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
					 | 
				
			||||||
        this._floors = floors;
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected updated(changed) {
 | 
					  protected updated(changed) {
 | 
				
			||||||
    if (changed.has("expanded") && this.expanded) {
 | 
					    if (changed.has("expanded") && this.expanded) {
 | 
				
			||||||
      setTimeout(() => {
 | 
					      setTimeout(() => {
 | 
				
			||||||
@@ -220,9 +204,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _areas = memoizeOne(
 | 
					  private _areas = memoizeOne(
 | 
				
			||||||
    (areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
 | 
					    (areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
 | 
				
			||||||
      const areas = Object.values(areaReg);
 | 
					      const areas = Object.values(areaReg);
 | 
				
			||||||
 | 
					      const floors = Object.values(floorReg);
 | 
				
			||||||
      const floorAreaLookup = getFloorAreaLookup(areas);
 | 
					      const floorAreaLookup = getFloorAreaLookup(areas);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const unassisgnedAreas = areas.filter(
 | 
					      const unassisgnedAreas = areas.filter(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
					import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
				
			||||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
					import { HassEntity } from "home-assistant-js-websocket";
 | 
				
			||||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
 | 
					import { LitElement, PropertyValues, TemplateResult, html } from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
import { classMap } from "lit/directives/class-map";
 | 
					import { classMap } from "lit/directives/class-map";
 | 
				
			||||||
@@ -24,10 +24,8 @@ import {
 | 
				
			|||||||
  FloorRegistryEntry,
 | 
					  FloorRegistryEntry,
 | 
				
			||||||
  createFloorRegistryEntry,
 | 
					  createFloorRegistryEntry,
 | 
				
			||||||
  getFloorAreaLookup,
 | 
					  getFloorAreaLookup,
 | 
				
			||||||
  subscribeFloorRegistry,
 | 
					 | 
				
			||||||
} from "../data/floor_registry";
 | 
					} from "../data/floor_registry";
 | 
				
			||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
 | 
					import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
 | 
				
			||||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
 | 
					 | 
				
			||||||
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
 | 
					import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
 | 
				
			||||||
import { HomeAssistant, ValueChangedEvent } from "../types";
 | 
					import { HomeAssistant, ValueChangedEvent } from "../types";
 | 
				
			||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
					import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
				
			||||||
@@ -53,7 +51,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
 | 
				
			|||||||
  </ha-list-item>`;
 | 
					  </ha-list-item>`;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-floor-picker")
 | 
					@customElement("ha-floor-picker")
 | 
				
			||||||
export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
					export class HaFloorPicker extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property() public label?: string;
 | 
					  @property() public label?: string;
 | 
				
			||||||
@@ -111,8 +109,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _opened?: boolean;
 | 
					  @state() private _opened?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @query("ha-combo-box", true) public comboBox!: HaComboBox;
 | 
					  @query("ha-combo-box", true) public comboBox!: HaComboBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _suggestion?: string;
 | 
					  private _suggestion?: string;
 | 
				
			||||||
@@ -129,14 +125,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
    await this.comboBox?.focus();
 | 
					    await this.comboBox?.focus();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
					 | 
				
			||||||
    return [
 | 
					 | 
				
			||||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
					 | 
				
			||||||
        this._floors = floors;
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _getFloors = memoizeOne(
 | 
					  private _getFloors = memoizeOne(
 | 
				
			||||||
    (
 | 
					    (
 | 
				
			||||||
      floors: FloorRegistryEntry[],
 | 
					      floors: FloorRegistryEntry[],
 | 
				
			||||||
@@ -320,12 +308,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  protected updated(changedProps: PropertyValues) {
 | 
					  protected updated(changedProps: PropertyValues) {
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
      (!this._init && this.hass && this._floors) ||
 | 
					      (!this._init && this.hass) ||
 | 
				
			||||||
      (this._init && changedProps.has("_opened") && this._opened)
 | 
					      (this._init && changedProps.has("_opened") && this._opened)
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      this._init = true;
 | 
					      this._init = true;
 | 
				
			||||||
      const floors = this._getFloors(
 | 
					      const floors = this._getFloors(
 | 
				
			||||||
        this._floors!,
 | 
					        Object.values(this.hass.floors),
 | 
				
			||||||
        Object.values(this.hass.areas),
 | 
					        Object.values(this.hass.areas),
 | 
				
			||||||
        Object.values(this.hass.devices),
 | 
					        Object.values(this.hass.devices),
 | 
				
			||||||
        Object.values(this.hass.entities),
 | 
					        Object.values(this.hass.entities),
 | 
				
			||||||
@@ -360,8 +348,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          ? this.hass.localize("ui.components.floor-picker.floor")
 | 
					          ? this.hass.localize("ui.components.floor-picker.floor")
 | 
				
			||||||
          : this.label}
 | 
					          : this.label}
 | 
				
			||||||
        .placeholder=${this.placeholder
 | 
					        .placeholder=${this.placeholder
 | 
				
			||||||
          ? this._floors?.find((floor) => floor.floor_id === this.placeholder)
 | 
					          ? this.hass.floors[this.placeholder]?.name
 | 
				
			||||||
              ?.name
 | 
					 | 
				
			||||||
          : undefined}
 | 
					          : undefined}
 | 
				
			||||||
        .renderer=${rowRenderer}
 | 
					        .renderer=${rowRenderer}
 | 
				
			||||||
        @filter-changed=${this._filterChanged}
 | 
					        @filter-changed=${this._filterChanged}
 | 
				
			||||||
@@ -460,7 +447,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
              floor_id: floor.floor_id,
 | 
					              floor_id: floor.floor_id,
 | 
				
			||||||
            });
 | 
					            });
 | 
				
			||||||
          });
 | 
					          });
 | 
				
			||||||
          const floors = [...this._floors!, floor];
 | 
					          const floors = [...Object.values(this.hass.floors), floor];
 | 
				
			||||||
          this.comboBox.filteredItems = this._getFloors(
 | 
					          this.comboBox.filteredItems = this._getFloors(
 | 
				
			||||||
            floors,
 | 
					            floors,
 | 
				
			||||||
            Object.values(this.hass.areas)!,
 | 
					            Object.values(this.hass.areas)!,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -95,10 +95,10 @@ export const computeInitialHaFormData = (
 | 
				
			|||||||
      } else if (
 | 
					      } else if (
 | 
				
			||||||
        "action" in selector ||
 | 
					        "action" in selector ||
 | 
				
			||||||
        "trigger" in selector ||
 | 
					        "trigger" in selector ||
 | 
				
			||||||
        "condition" in selector ||
 | 
					        "condition" in selector
 | 
				
			||||||
        "media" in selector ||
 | 
					 | 
				
			||||||
        "target" in selector
 | 
					 | 
				
			||||||
      ) {
 | 
					      ) {
 | 
				
			||||||
 | 
					        data[field.name] = [];
 | 
				
			||||||
 | 
					      } else if ("media" in selector || "target" in selector) {
 | 
				
			||||||
        data[field.name] = {};
 | 
					        data[field.name] = {};
 | 
				
			||||||
      } else {
 | 
					      } else {
 | 
				
			||||||
        throw new Error(
 | 
					        throw new Error(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,13 +21,45 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public computeLabel?: (
 | 
					  @property({ attribute: false }) public computeLabel?: (
 | 
				
			||||||
    schema: HaFormSchema,
 | 
					    schema: HaFormSchema,
 | 
				
			||||||
    data?: HaFormDataContainer
 | 
					    data?: HaFormDataContainer,
 | 
				
			||||||
 | 
					    options?: { path?: string[] }
 | 
				
			||||||
  ) => string;
 | 
					  ) => string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public computeHelper?: (
 | 
					  @property({ attribute: false }) public computeHelper?: (
 | 
				
			||||||
    schema: HaFormSchema
 | 
					    schema: HaFormSchema,
 | 
				
			||||||
 | 
					    options?: { path?: string[] }
 | 
				
			||||||
  ) => string;
 | 
					  ) => string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _renderDescription() {
 | 
				
			||||||
 | 
					    const description = this.computeHelper?.(this.schema);
 | 
				
			||||||
 | 
					    return description ? html`<p>${description}</p>` : nothing;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _computeLabel = (
 | 
				
			||||||
 | 
					    schema: HaFormSchema,
 | 
				
			||||||
 | 
					    data?: HaFormDataContainer,
 | 
				
			||||||
 | 
					    options?: { path?: string[] }
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    if (!this.computeLabel) return this.computeLabel;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.computeLabel(schema, data, {
 | 
				
			||||||
 | 
					      ...options,
 | 
				
			||||||
 | 
					      path: [...(options?.path || []), this.schema.name],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _computeHelper = (
 | 
				
			||||||
 | 
					    schema: HaFormSchema,
 | 
				
			||||||
 | 
					    options?: { path?: string[] }
 | 
				
			||||||
 | 
					  ) => {
 | 
				
			||||||
 | 
					    if (!this.computeHelper) return this.computeHelper;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return this.computeHelper(schema, {
 | 
				
			||||||
 | 
					      ...options,
 | 
				
			||||||
 | 
					      path: [...(options?.path || []), this.schema.name],
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected render() {
 | 
					  protected render() {
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
 | 
					      <ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}>
 | 
				
			||||||
@@ -43,16 +75,17 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
 | 
				
			|||||||
                  <ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
 | 
					                  <ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon>
 | 
				
			||||||
                `
 | 
					                `
 | 
				
			||||||
              : nothing}
 | 
					              : nothing}
 | 
				
			||||||
          ${this.schema.title}
 | 
					          ${this.schema.title || this.computeLabel?.(this.schema)}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <div class="content">
 | 
					        <div class="content">
 | 
				
			||||||
 | 
					          ${this._renderDescription()}
 | 
				
			||||||
          <ha-form
 | 
					          <ha-form
 | 
				
			||||||
            .hass=${this.hass}
 | 
					            .hass=${this.hass}
 | 
				
			||||||
            .data=${this.data}
 | 
					            .data=${this.data}
 | 
				
			||||||
            .schema=${this.schema.schema}
 | 
					            .schema=${this.schema.schema}
 | 
				
			||||||
            .disabled=${this.disabled}
 | 
					            .disabled=${this.disabled}
 | 
				
			||||||
            .computeLabel=${this.computeLabel}
 | 
					            .computeLabel=${this._computeLabel}
 | 
				
			||||||
            .computeHelper=${this.computeHelper}
 | 
					            .computeHelper=${this._computeHelper}
 | 
				
			||||||
          ></ha-form>
 | 
					          ></ha-form>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
      </ha-expansion-panel>
 | 
					      </ha-expansion-panel>
 | 
				
			||||||
@@ -71,6 +104,9 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
 | 
				
			|||||||
      .content {
 | 
					      .content {
 | 
				
			||||||
        padding: 12px;
 | 
					        padding: 12px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      .content p {
 | 
				
			||||||
 | 
					        margin: 0 0 24px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      ha-expansion-panel {
 | 
					      ha-expansion-panel {
 | 
				
			||||||
        display: block;
 | 
					        display: block;
 | 
				
			||||||
        --expansion-panel-content-padding: 0;
 | 
					        --expansion-panel-content-padding: 0;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,7 +31,7 @@ const LOAD_ELEMENTS = {
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getValue = (obj, item) =>
 | 
					const getValue = (obj, item) =>
 | 
				
			||||||
  obj ? (!item.name ? obj : obj[item.name]) : null;
 | 
					  obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
 | 
					const getError = (obj, item) => (obj && item.name ? obj[item.name] : null);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -204,9 +204,10 @@ export class HaForm extends LitElement implements HaFormElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      if (ev.target === this) return;
 | 
					      if (ev.target === this) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const newValue = !schema.name
 | 
					      const newValue =
 | 
				
			||||||
        ? ev.detail.value
 | 
					        !schema.name || ("flatten" in schema && schema.flatten)
 | 
				
			||||||
        : { [schema.name]: ev.detail.value };
 | 
					          ? ev.detail.value
 | 
				
			||||||
 | 
					          : { [schema.name]: ev.detail.value };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      this.data = {
 | 
					      this.data = {
 | 
				
			||||||
        ...this.data,
 | 
					        ...this.data,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,15 +31,15 @@ export interface HaFormBaseSchema {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export interface HaFormGridSchema extends HaFormBaseSchema {
 | 
					export interface HaFormGridSchema extends HaFormBaseSchema {
 | 
				
			||||||
  type: "grid";
 | 
					  type: "grid";
 | 
				
			||||||
  name: string;
 | 
					  flatten?: boolean;
 | 
				
			||||||
  column_min_width?: string;
 | 
					  column_min_width?: string;
 | 
				
			||||||
  schema: readonly HaFormSchema[];
 | 
					  schema: readonly HaFormSchema[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface HaFormExpandableSchema extends HaFormBaseSchema {
 | 
					export interface HaFormExpandableSchema extends HaFormBaseSchema {
 | 
				
			||||||
  type: "expandable";
 | 
					  type: "expandable";
 | 
				
			||||||
  name: "";
 | 
					  flatten?: boolean;
 | 
				
			||||||
  title: string;
 | 
					  title?: string;
 | 
				
			||||||
  icon?: string;
 | 
					  icon?: string;
 | 
				
			||||||
  iconPath?: string;
 | 
					  iconPath?: string;
 | 
				
			||||||
  expanded?: boolean;
 | 
					  expanded?: boolean;
 | 
				
			||||||
@@ -100,7 +100,7 @@ export type SchemaUnion<
 | 
				
			|||||||
  SchemaArray extends readonly HaFormSchema[],
 | 
					  SchemaArray extends readonly HaFormSchema[],
 | 
				
			||||||
  Schema = SchemaArray[number],
 | 
					  Schema = SchemaArray[number],
 | 
				
			||||||
> = Schema extends HaFormGridSchema | HaFormExpandableSchema
 | 
					> = Schema extends HaFormGridSchema | HaFormExpandableSchema
 | 
				
			||||||
  ? SchemaUnion<Schema["schema"]>
 | 
					  ? SchemaUnion<Schema["schema"]> | Schema
 | 
				
			||||||
  : Schema;
 | 
					  : Schema;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface HaFormDataContainer {
 | 
					export interface HaFormDataContainer {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -18,9 +18,9 @@ export class HaFormfield extends FormfieldBase {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return html` <div class="mdc-form-field ${classMap(classes)}">
 | 
					    return html` <div class="mdc-form-field ${classMap(classes)}">
 | 
				
			||||||
      <slot></slot>
 | 
					      <slot></slot>
 | 
				
			||||||
      <label class="mdc-label" @click=${this._labelClick}
 | 
					      <label class="mdc-label" @click=${this._labelClick}>
 | 
				
			||||||
        ><slot name="label">${this.label}</slot></label
 | 
					        <slot name="label">${this.label}</slot>
 | 
				
			||||||
      >
 | 
					      </label>
 | 
				
			||||||
    </div>`;
 | 
					    </div>`;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -57,13 +57,13 @@ export class HaFormfield extends FormfieldBase {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      .mdc-form-field {
 | 
					      .mdc-form-field {
 | 
				
			||||||
        align-items: var(--ha-formfield-align-items, center);
 | 
					        align-items: var(--ha-formfield-align-items, center);
 | 
				
			||||||
 | 
					        gap: 4px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .mdc-form-field > label {
 | 
					      .mdc-form-field > label {
 | 
				
			||||||
        direction: var(--direction);
 | 
					        direction: var(--direction);
 | 
				
			||||||
        margin-inline-start: 0;
 | 
					        margin-inline-start: 0;
 | 
				
			||||||
        margin-inline-end: auto;
 | 
					        margin-inline-end: auto;
 | 
				
			||||||
        padding-inline-start: 4px;
 | 
					        padding: 0;
 | 
				
			||||||
        padding-inline-end: 0;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      :host([disabled]) label {
 | 
					      :host([disabled]) label {
 | 
				
			||||||
        color: var(--disabled-text-color);
 | 
					        color: var(--disabled-text-color);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,24 +1,24 @@
 | 
				
			|||||||
import { LitElement, css, html, nothing } from "lit";
 | 
					import { LitElement, css, html, nothing } from "lit";
 | 
				
			||||||
import { customElement, property, state } from "lit/decorators";
 | 
					import { customElement, property, state } from "lit/decorators";
 | 
				
			||||||
import "./ha-icon-button";
 | 
					 | 
				
			||||||
import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
 | 
					import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider";
 | 
				
			||||||
 | 
					import "./ha-icon-button";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
import { mdiRestore } from "@mdi/js";
 | 
					import { mdiRestore } from "@mdi/js";
 | 
				
			||||||
 | 
					import { classMap } from "lit/directives/class-map";
 | 
				
			||||||
import { styleMap } from "lit/directives/style-map";
 | 
					import { styleMap } from "lit/directives/style-map";
 | 
				
			||||||
import { fireEvent } from "../common/dom/fire_event";
 | 
					import { fireEvent } from "../common/dom/fire_event";
 | 
				
			||||||
import { HomeAssistant } from "../types";
 | 
					 | 
				
			||||||
import { conditionalClamp } from "../common/number/clamp";
 | 
					import { conditionalClamp } from "../common/number/clamp";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
type GridSizeValue = {
 | 
					  CardGridSize,
 | 
				
			||||||
  rows?: number | "auto";
 | 
					  DEFAULT_GRID_SIZE,
 | 
				
			||||||
  columns?: number;
 | 
					} from "../panels/lovelace/common/compute-card-grid-size";
 | 
				
			||||||
};
 | 
					import { HomeAssistant } from "../types";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-grid-size-picker")
 | 
					@customElement("ha-grid-size-picker")
 | 
				
			||||||
export class HaGridSizeEditor extends LitElement {
 | 
					export class HaGridSizeEditor extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public value?: GridSizeValue;
 | 
					  @property({ attribute: false }) public value?: CardGridSize;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public rows = 8;
 | 
					  @property({ attribute: false }) public rows = 8;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public isDefault?: boolean;
 | 
					  @property({ attribute: false }) public isDefault?: boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() public _localValue?: GridSizeValue = undefined;
 | 
					  @state() public _localValue?: CardGridSize = { rows: 1, columns: 1 };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected willUpdate(changedProperties) {
 | 
					  protected willUpdate(changedProperties) {
 | 
				
			||||||
    if (changedProperties.has("value")) {
 | 
					    if (changedProperties.has("value")) {
 | 
				
			||||||
@@ -49,6 +49,7 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
      this.rowMin !== undefined && this.rowMin === this.rowMax;
 | 
					      this.rowMin !== undefined && this.rowMin === this.rowMax;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const autoHeight = this._localValue?.rows === "auto";
 | 
					    const autoHeight = this._localValue?.rows === "auto";
 | 
				
			||||||
 | 
					    const fullWidth = this._localValue?.columns === "full";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const rowMin = this.rowMin ?? 1;
 | 
					    const rowMin = this.rowMin ?? 1;
 | 
				
			||||||
    const rowMax = this.rowMax ?? this.rows;
 | 
					    const rowMax = this.rowMax ?? this.rows;
 | 
				
			||||||
@@ -67,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
          .min=${columnMin}
 | 
					          .min=${columnMin}
 | 
				
			||||||
          .max=${columnMax}
 | 
					          .max=${columnMax}
 | 
				
			||||||
          .range=${this.columns}
 | 
					          .range=${this.columns}
 | 
				
			||||||
          .value=${columnValue}
 | 
					          .value=${fullWidth ? this.columns : columnValue}
 | 
				
			||||||
          @value-changed=${this._valueChanged}
 | 
					          @value-changed=${this._valueChanged}
 | 
				
			||||||
          @slider-moved=${this._sliderMoved}
 | 
					          @slider-moved=${this._sliderMoved}
 | 
				
			||||||
          .disabled=${disabledColumns}
 | 
					          .disabled=${disabledColumns}
 | 
				
			||||||
@@ -104,12 +105,12 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
            `
 | 
					            `
 | 
				
			||||||
          : nothing}
 | 
					          : nothing}
 | 
				
			||||||
        <div
 | 
					        <div
 | 
				
			||||||
          class="preview"
 | 
					          class="preview ${classMap({ "full-width": fullWidth })}"
 | 
				
			||||||
          style=${styleMap({
 | 
					          style=${styleMap({
 | 
				
			||||||
            "--total-rows": this.rows,
 | 
					            "--total-rows": this.rows,
 | 
				
			||||||
            "--total-columns": this.columns,
 | 
					            "--total-columns": this.columns,
 | 
				
			||||||
            "--rows": rowValue,
 | 
					            "--rows": rowValue,
 | 
				
			||||||
            "--columns": columnValue,
 | 
					            "--columns": fullWidth ? this.columns : columnValue,
 | 
				
			||||||
          })}
 | 
					          })}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          <div>
 | 
					          <div>
 | 
				
			||||||
@@ -140,12 +141,21 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
    const cell = ev.currentTarget as HTMLElement;
 | 
					    const cell = ev.currentTarget as HTMLElement;
 | 
				
			||||||
    const rows = Number(cell.getAttribute("data-row"));
 | 
					    const rows = Number(cell.getAttribute("data-row"));
 | 
				
			||||||
    const columns = Number(cell.getAttribute("data-column"));
 | 
					    const columns = Number(cell.getAttribute("data-column"));
 | 
				
			||||||
    const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax);
 | 
					    const clampedRow: CardGridSize["rows"] = conditionalClamp(
 | 
				
			||||||
    const clampedColumn = conditionalClamp(
 | 
					      rows,
 | 
				
			||||||
 | 
					      this.rowMin,
 | 
				
			||||||
 | 
					      this.rowMax
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    let clampedColumn: CardGridSize["columns"] = conditionalClamp(
 | 
				
			||||||
      columns,
 | 
					      columns,
 | 
				
			||||||
      this.columnMin,
 | 
					      this.columnMin,
 | 
				
			||||||
      this.columnMax
 | 
					      this.columnMax
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const currentSize = this.value ?? DEFAULT_GRID_SIZE;
 | 
				
			||||||
 | 
					    if (currentSize.columns === "full" && clampedColumn === this.columns) {
 | 
				
			||||||
 | 
					      clampedColumn = "full";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
    fireEvent(this, "value-changed", {
 | 
					    fireEvent(this, "value-changed", {
 | 
				
			||||||
      value: { rows: clampedRow, columns: clampedColumn },
 | 
					      value: { rows: clampedRow, columns: clampedColumn },
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
@@ -153,12 +163,23 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _valueChanged(ev) {
 | 
					  private _valueChanged(ev) {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    const key = ev.currentTarget.id;
 | 
					    const key = ev.currentTarget.id as "rows" | "columns";
 | 
				
			||||||
    const newValue = {
 | 
					    const currentSize = this.value ?? DEFAULT_GRID_SIZE;
 | 
				
			||||||
      ...this.value,
 | 
					    let value = ev.detail.value as CardGridSize[typeof key];
 | 
				
			||||||
      [key]: ev.detail.value,
 | 
					
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      key === "columns" &&
 | 
				
			||||||
 | 
					      currentSize.columns === "full" &&
 | 
				
			||||||
 | 
					      value === this.columns
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      value = "full";
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const newSize = {
 | 
				
			||||||
 | 
					      ...currentSize,
 | 
				
			||||||
 | 
					      [key]: value,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
    fireEvent(this, "value-changed", { value: newValue });
 | 
					    fireEvent(this, "value-changed", { value: newSize });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _reset(ev) {
 | 
					  private _reset(ev) {
 | 
				
			||||||
@@ -173,11 +194,14 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _sliderMoved(ev) {
 | 
					  private _sliderMoved(ev) {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    const key = ev.currentTarget.id;
 | 
					    const key = ev.currentTarget.id as "rows" | "columns";
 | 
				
			||||||
    const value = ev.detail.value;
 | 
					    const currentSize = this.value ?? DEFAULT_GRID_SIZE;
 | 
				
			||||||
 | 
					    const value = ev.detail.value as CardGridSize[typeof key] | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (value === undefined) return;
 | 
					    if (value === undefined) return;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._localValue = {
 | 
					    this._localValue = {
 | 
				
			||||||
      ...this.value,
 | 
					      ...currentSize,
 | 
				
			||||||
      [key]: ev.detail.value,
 | 
					      [key]: ev.detail.value,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -189,7 +213,7 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
        grid-template-areas:
 | 
					        grid-template-areas:
 | 
				
			||||||
          "reset column-slider"
 | 
					          "reset column-slider"
 | 
				
			||||||
          "row-slider preview";
 | 
					          "row-slider preview";
 | 
				
			||||||
        grid-template-rows: auto 1fr;
 | 
					        grid-template-rows: auto auto;
 | 
				
			||||||
        grid-template-columns: auto 1fr;
 | 
					        grid-template-columns: auto 1fr;
 | 
				
			||||||
        gap: 8px;
 | 
					        gap: 8px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -205,17 +229,12 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
      .preview {
 | 
					      .preview {
 | 
				
			||||||
        position: relative;
 | 
					        position: relative;
 | 
				
			||||||
        grid-area: preview;
 | 
					        grid-area: preview;
 | 
				
			||||||
        aspect-ratio: 1 / 1.2;
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .preview > div {
 | 
					      .preview > div {
 | 
				
			||||||
        position: absolute;
 | 
					        position: relative;
 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
        height: 100%;
 | 
					 | 
				
			||||||
        top: 0;
 | 
					 | 
				
			||||||
        left: 0;
 | 
					 | 
				
			||||||
        display: grid;
 | 
					        display: grid;
 | 
				
			||||||
        grid-template-columns: repeat(var(--total-columns), 1fr);
 | 
					        grid-template-columns: repeat(var(--total-columns), 1fr);
 | 
				
			||||||
        grid-template-rows: repeat(var(--total-rows), 1fr);
 | 
					        grid-template-rows: repeat(var(--total-rows), 25px);
 | 
				
			||||||
        gap: 4px;
 | 
					        gap: 4px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .preview .cell {
 | 
					      .preview .cell {
 | 
				
			||||||
@@ -226,15 +245,23 @@ export class HaGridSizeEditor extends LitElement {
 | 
				
			|||||||
        opacity: 0.2;
 | 
					        opacity: 0.2;
 | 
				
			||||||
        cursor: pointer;
 | 
					        cursor: pointer;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .selected {
 | 
					      .preview .selected {
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
        pointer-events: none;
 | 
					        pointer-events: none;
 | 
				
			||||||
 | 
					        top: 0;
 | 
				
			||||||
 | 
					        left: 0;
 | 
				
			||||||
 | 
					        height: 100%;
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .selected .cell {
 | 
					      .selected .cell {
 | 
				
			||||||
        background-color: var(--primary-color);
 | 
					        background-color: var(--primary-color);
 | 
				
			||||||
        grid-column: 1 / span var(--columns, 0);
 | 
					        grid-column: 1 / span min(var(--columns, 0), var(--total-columns));
 | 
				
			||||||
        grid-row: 1 / span var(--rows, 0);
 | 
					        grid-row: 1 / span min(var(--rows, 0), var(--total-rows));
 | 
				
			||||||
        opacity: 0.5;
 | 
					        opacity: 0.5;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      .preview.full-width .selected .cell {
 | 
				
			||||||
 | 
					        grid-column: 1 / -1;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    `,
 | 
					    `,
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -96,7 +96,25 @@ class HaMarkdownElement extends ReactiveElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
          haAlertNode.append(
 | 
					          haAlertNode.append(
 | 
				
			||||||
            ...Array.from(node.childNodes)
 | 
					            ...Array.from(node.childNodes)
 | 
				
			||||||
              .map((child) => Array.from(child.childNodes))
 | 
					              .map((child) => {
 | 
				
			||||||
 | 
					                const arr = Array.from(child.childNodes);
 | 
				
			||||||
 | 
					                if (!this.breaks && arr.length) {
 | 
				
			||||||
 | 
					                  // When we are not breaking, the first line of the blockquote is not considered,
 | 
				
			||||||
 | 
					                  // so we need to adjust the first child text content
 | 
				
			||||||
 | 
					                  const firstChild = arr[0];
 | 
				
			||||||
 | 
					                  if (
 | 
				
			||||||
 | 
					                    firstChild.nodeType === Node.TEXT_NODE &&
 | 
				
			||||||
 | 
					                    firstChild.textContent === gitHubAlertMatch.input &&
 | 
				
			||||||
 | 
					                    firstChild.textContent?.includes("\n")
 | 
				
			||||||
 | 
					                  ) {
 | 
				
			||||||
 | 
					                    firstChild.textContent = firstChild.textContent
 | 
				
			||||||
 | 
					                      .split("\n")
 | 
				
			||||||
 | 
					                      .slice(1)
 | 
				
			||||||
 | 
					                      .join("\n");
 | 
				
			||||||
 | 
					                  }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					                return arr;
 | 
				
			||||||
 | 
					              })
 | 
				
			||||||
              .reduce((acc, val) => acc.concat(val), [])
 | 
					              .reduce((acc, val) => acc.concat(val), [])
 | 
				
			||||||
              .filter(
 | 
					              .filter(
 | 
				
			||||||
                (childNode) =>
 | 
					                (childNode) =>
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button";
 | 
				
			|||||||
import "./ha-menu";
 | 
					import "./ha-menu";
 | 
				
			||||||
import type { HaMenu } from "./ha-menu";
 | 
					import type { HaMenu } from "./ha-menu";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-button-menu-new")
 | 
					@customElement("ha-md-button-menu")
 | 
				
			||||||
export class HaButtonMenuNew extends LitElement {
 | 
					export class HaMdButtonMenu extends LitElement {
 | 
				
			||||||
  protected readonly [FOCUS_TARGET];
 | 
					  protected readonly [FOCUS_TARGET];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ type: Boolean }) public disabled = false;
 | 
					  @property({ type: Boolean }) public disabled = false;
 | 
				
			||||||
@@ -84,6 +84,6 @@ export class HaButtonMenuNew extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
    "ha-button-menu-new": HaButtonMenuNew;
 | 
					    "ha-md-button-menu": HaMdButtonMenu;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
							
								
								
									
										210
									
								
								src/components/ha-md-dialog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										210
									
								
								src/components/ha-md-dialog.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,210 @@
 | 
				
			|||||||
 | 
					import { MdDialog } from "@material/web/dialog/dialog";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  type DialogAnimation,
 | 
				
			||||||
 | 
					  DIALOG_DEFAULT_CLOSE_ANIMATION,
 | 
				
			||||||
 | 
					  DIALOG_DEFAULT_OPEN_ANIMATION,
 | 
				
			||||||
 | 
					} from "@material/web/dialog/internal/animations";
 | 
				
			||||||
 | 
					import { css } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					/**
 | 
				
			||||||
 | 
					 * Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
 | 
				
			||||||
 | 
					 *
 | 
				
			||||||
 | 
					 */
 | 
				
			||||||
 | 
					@customElement("ha-md-dialog")
 | 
				
			||||||
 | 
					export class HaMdDialog extends MdDialog {
 | 
				
			||||||
 | 
					  /**
 | 
				
			||||||
 | 
					   * When true the dialog will not close when the user presses the esc key or press out of the dialog.
 | 
				
			||||||
 | 
					   */
 | 
				
			||||||
 | 
					  @property({ attribute: "disable-cancel-action", type: Boolean })
 | 
				
			||||||
 | 
					  public disableCancelAction = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _polyfillDialogRegistered = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  constructor() {
 | 
				
			||||||
 | 
					    super();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.addEventListener("cancel", this._handleCancel);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (typeof HTMLDialogElement !== "function") {
 | 
				
			||||||
 | 
					      this.addEventListener("open", this._handleOpen);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!DIALOG_POLYFILL) {
 | 
				
			||||||
 | 
					        DIALOG_POLYFILL = import("dialog-polyfill");
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // if browser doesn't support animate API disable open/close animations
 | 
				
			||||||
 | 
					    if (this.animate === undefined) {
 | 
				
			||||||
 | 
					      this.quick = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  // prevent open in older browsers and wait for polyfill to load
 | 
				
			||||||
 | 
					  private async _handleOpen(openEvent: Event) {
 | 
				
			||||||
 | 
					    openEvent.preventDefault();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (this._polyfillDialogRegistered) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this._polyfillDialogRegistered = true;
 | 
				
			||||||
 | 
					    this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
 | 
				
			||||||
 | 
					    const dialog = this.shadowRoot?.querySelector(
 | 
				
			||||||
 | 
					      "dialog"
 | 
				
			||||||
 | 
					    ) as HTMLDialogElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const dialogPolyfill = await DIALOG_POLYFILL;
 | 
				
			||||||
 | 
					    dialogPolyfill.default.registerDialog(dialog);
 | 
				
			||||||
 | 
					    this.removeEventListener("open", this._handleOpen);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.show();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async _loadPolyfillStylesheet(href) {
 | 
				
			||||||
 | 
					    const link = document.createElement("link");
 | 
				
			||||||
 | 
					    link.rel = "stylesheet";
 | 
				
			||||||
 | 
					    link.href = href;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return new Promise<void>((resolve, reject) => {
 | 
				
			||||||
 | 
					      link.onload = () => resolve();
 | 
				
			||||||
 | 
					      link.onerror = () =>
 | 
				
			||||||
 | 
					        reject(new Error(`Stylesheet failed to load: ${href}`));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      this.shadowRoot?.appendChild(link);
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  _handleCancel(closeEvent: Event) {
 | 
				
			||||||
 | 
					    if (this.disableCancelAction) {
 | 
				
			||||||
 | 
					      closeEvent.preventDefault();
 | 
				
			||||||
 | 
					      const dialogElement = this.shadowRoot?.querySelector("dialog");
 | 
				
			||||||
 | 
					      if (this.animate !== undefined) {
 | 
				
			||||||
 | 
					        dialogElement?.animate(
 | 
				
			||||||
 | 
					          [
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              transform: "rotate(-1deg)",
 | 
				
			||||||
 | 
					              "animation-timing-function": "ease-in",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              transform: "rotate(1.5deg)",
 | 
				
			||||||
 | 
					              "animation-timing-function": "ease-out",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              transform: "rotate(0deg)",
 | 
				
			||||||
 | 
					              "animation-timing-function": "ease-in",
 | 
				
			||||||
 | 
					            },
 | 
				
			||||||
 | 
					          ],
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            duration: 200,
 | 
				
			||||||
 | 
					            iterations: 2,
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static override styles = [
 | 
				
			||||||
 | 
					    ...super.styles,
 | 
				
			||||||
 | 
					    css`
 | 
				
			||||||
 | 
					      :host {
 | 
				
			||||||
 | 
					        --md-dialog-container-color: var(--card-background-color);
 | 
				
			||||||
 | 
					        --md-dialog-headline-color: var(--primary-text-color);
 | 
				
			||||||
 | 
					        --md-dialog-supporting-text-color: var(--primary-text-color);
 | 
				
			||||||
 | 
					        --md-sys-color-scrim: #000000;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        --md-dialog-headline-weight: 400;
 | 
				
			||||||
 | 
					        --md-dialog-headline-size: 1.574rem;
 | 
				
			||||||
 | 
					        --md-dialog-supporting-text-size: 1rem;
 | 
				
			||||||
 | 
					        --md-dialog-supporting-text-line-height: 1.5rem;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      :host([type="alert"]) {
 | 
				
			||||||
 | 
					        max-width: 320px;
 | 
				
			||||||
 | 
					        min-width: 320px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      :host(:not([type="alert"])) {
 | 
				
			||||||
 | 
					        @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
				
			||||||
 | 
					          min-width: calc(
 | 
				
			||||||
 | 
					            100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          max-width: calc(
 | 
				
			||||||
 | 
					            100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					          min-height: 100%;
 | 
				
			||||||
 | 
					          max-height: 100%;
 | 
				
			||||||
 | 
					          --md-dialog-container-shape: 0;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      :host ::slotted(ha-dialog-header) {
 | 
				
			||||||
 | 
					        display: contents;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .scrim {
 | 
				
			||||||
 | 
					        z-index: 10; // overlay navigation
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    `,
 | 
				
			||||||
 | 
					  ];
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					// by default the dialog open/close animation will be from/to the top
 | 
				
			||||||
 | 
					// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used:
 | 
				
			||||||
 | 
					const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
 | 
				
			||||||
 | 
					  ...DIALOG_DEFAULT_OPEN_ANIMATION,
 | 
				
			||||||
 | 
					  dialog: [
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      // Dialog slide up
 | 
				
			||||||
 | 
					      [{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
 | 
				
			||||||
 | 
					      { duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  container: [
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      // Container fade in
 | 
				
			||||||
 | 
					      [{ opacity: 0 }, { opacity: 1 }],
 | 
				
			||||||
 | 
					      { duration: 50, easing: "linear", pseudoElement: "::before" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
 | 
				
			||||||
 | 
					  ...DIALOG_DEFAULT_CLOSE_ANIMATION,
 | 
				
			||||||
 | 
					  dialog: [
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      // Dialog slide down
 | 
				
			||||||
 | 
					      [{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
 | 
				
			||||||
 | 
					      { duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					  container: [
 | 
				
			||||||
 | 
					    [
 | 
				
			||||||
 | 
					      // Container fade out
 | 
				
			||||||
 | 
					      [{ opacity: "1" }, { opacity: "0" }],
 | 
				
			||||||
 | 
					      { delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
 | 
				
			||||||
 | 
					    ],
 | 
				
			||||||
 | 
					  ],
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getMobileOpenFromBottomAnimation = () => {
 | 
				
			||||||
 | 
					  const matches = window.matchMedia(
 | 
				
			||||||
 | 
					    "all and (max-width: 450px), all and (max-height: 500px)"
 | 
				
			||||||
 | 
					  ).matches;
 | 
				
			||||||
 | 
					  return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getMobileCloseToBottomAnimation = () => {
 | 
				
			||||||
 | 
					  const matches = window.matchMedia(
 | 
				
			||||||
 | 
					    "all and (max-width: 450px), all and (max-height: 500px)"
 | 
				
			||||||
 | 
					  ).matches;
 | 
				
			||||||
 | 
					  return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					    "ha-md-dialog": HaMdDialog;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
 | 
				
			|||||||
import { css } from "lit";
 | 
					import { css } from "lit";
 | 
				
			||||||
import { customElement } from "lit/decorators";
 | 
					import { customElement } from "lit/decorators";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-list-item-new")
 | 
					@customElement("ha-md-list-item")
 | 
				
			||||||
export class HaListItemNew extends MdListItem {
 | 
					export class HaMdListItem extends MdListItem {
 | 
				
			||||||
  static override styles = [
 | 
					  static override styles = [
 | 
				
			||||||
    ...super.styles,
 | 
					    ...super.styles,
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
@@ -21,6 +21,6 @@ export class HaListItemNew extends MdListItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
    "ha-list-item-new": HaListItemNew;
 | 
					    "ha-md-list-item": HaMdListItem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
 | 
				
			|||||||
import { css } from "lit";
 | 
					import { css } from "lit";
 | 
				
			||||||
import { customElement } from "lit/decorators";
 | 
					import { customElement } from "lit/decorators";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-list-new")
 | 
					@customElement("ha-md-list")
 | 
				
			||||||
export class HaListNew extends MdList {
 | 
					export class HaMdList extends MdList {
 | 
				
			||||||
  static override styles = [
 | 
					  static override styles = [
 | 
				
			||||||
    ...super.styles,
 | 
					    ...super.styles,
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
@@ -16,6 +16,6 @@ export class HaListNew extends MdList {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
    "ha-list-new": HaListNew;
 | 
					    "ha-md-list": HaMdList;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
 | 
				
			|||||||
import { css } from "lit";
 | 
					import { css } from "lit";
 | 
				
			||||||
import { customElement, property } from "lit/decorators";
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-menu-item")
 | 
					@customElement("ha-md-menu-item")
 | 
				
			||||||
export class HaMenuItem extends MdMenuItem {
 | 
					export class HaMdMenuItem extends MdMenuItem {
 | 
				
			||||||
  @property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
 | 
					  @property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static override styles = [
 | 
					  static override styles = [
 | 
				
			||||||
@@ -41,6 +41,6 @@ export class HaMenuItem extends MdMenuItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
    "ha-menu-item": HaMenuItem;
 | 
					    "ha-md-menu-item": HaMdMenuItem;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -6,7 +6,7 @@ import {
 | 
				
			|||||||
} from "@material/web/menu/internal/controllers/shared";
 | 
					} from "@material/web/menu/internal/controllers/shared";
 | 
				
			||||||
import { css } from "lit";
 | 
					import { css } from "lit";
 | 
				
			||||||
import { customElement } from "lit/decorators";
 | 
					import { customElement } from "lit/decorators";
 | 
				
			||||||
import type { HaMenuItem } from "./ha-menu-item";
 | 
					import type { HaMdMenuItem } from "./ha-md-menu-item";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-menu")
 | 
					@customElement("ha-menu")
 | 
				
			||||||
export class HaMenu extends MdMenu {
 | 
					export class HaMenu extends MdMenu {
 | 
				
			||||||
@@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
 | 
				
			|||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    (ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
 | 
					    (ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static override styles = [
 | 
					  static override styles = [
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,19 +1,15 @@
 | 
				
			|||||||
import { css, CSSResultGroup, html, LitElement } from "lit";
 | 
					import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
 | 
				
			||||||
import { customElement, property } from "lit/decorators";
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
import { BooleanSelector } from "../../data/selector";
 | 
					 | 
				
			||||||
import { HomeAssistant } from "../../types";
 | 
					import { HomeAssistant } from "../../types";
 | 
				
			||||||
import "../ha-checkbox";
 | 
					 | 
				
			||||||
import "../ha-formfield";
 | 
					import "../ha-formfield";
 | 
				
			||||||
import "../ha-input-helper-text";
 | 
					 | 
				
			||||||
import "../ha-switch";
 | 
					import "../ha-switch";
 | 
				
			||||||
 | 
					import "../ha-input-helper-text";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-selector-boolean")
 | 
					@customElement("ha-selector-boolean")
 | 
				
			||||||
export class HaBooleanSelector extends LitElement {
 | 
					export class HaBooleanSelector extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public selector!: BooleanSelector;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ type: Boolean }) public value = false;
 | 
					  @property({ type: Boolean }) public value = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property() public placeholder?: any;
 | 
					  @property() public placeholder?: any;
 | 
				
			||||||
@@ -25,28 +21,20 @@ export class HaBooleanSelector extends LitElement {
 | 
				
			|||||||
  @property({ type: Boolean }) public disabled = false;
 | 
					  @property({ type: Boolean }) public disabled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected render() {
 | 
					  protected render() {
 | 
				
			||||||
    const checkbox = this.selector.boolean?.mode === "checkbox";
 | 
					 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-formfield .alignEnd=${!checkbox} spaceBetween .label=${this.label}>
 | 
					      <ha-formfield alignEnd spaceBetween .label=${this.label}>
 | 
				
			||||||
        ${checkbox
 | 
					        <ha-switch
 | 
				
			||||||
          ? html`
 | 
					          .checked=${this.value ?? this.placeholder === true}
 | 
				
			||||||
              <ha-checkbox
 | 
					          @change=${this._handleChange}
 | 
				
			||||||
                .checked=${this.value ?? this.placeholder === true}
 | 
					          .disabled=${this.disabled}
 | 
				
			||||||
                @change=${this._handleChange}
 | 
					        ></ha-switch>
 | 
				
			||||||
                .disabled=${this.disabled}
 | 
					        <span slot="label">
 | 
				
			||||||
              ></ha-checkbox>
 | 
					          <p class="primary">${this.label}</p>
 | 
				
			||||||
            `
 | 
					          ${this.helper
 | 
				
			||||||
          : html`
 | 
					            ? html`<p class="secondary">${this.helper}</p>`
 | 
				
			||||||
              <ha-switch
 | 
					            : nothing}
 | 
				
			||||||
                .checked=${this.value ?? this.placeholder === true}
 | 
					        </span>
 | 
				
			||||||
                @change=${this._handleChange}
 | 
					 | 
				
			||||||
                .disabled=${this.disabled}
 | 
					 | 
				
			||||||
              ></ha-switch>
 | 
					 | 
				
			||||||
            `}
 | 
					 | 
				
			||||||
      </ha-formfield>
 | 
					      </ha-formfield>
 | 
				
			||||||
      ${this.helper
 | 
					 | 
				
			||||||
        ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
 | 
					 | 
				
			||||||
        : ""}
 | 
					 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -62,10 +50,21 @@ export class HaBooleanSelector extends LitElement {
 | 
				
			|||||||
    return css`
 | 
					    return css`
 | 
				
			||||||
      ha-formfield {
 | 
					      ha-formfield {
 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
        height: 56px;
 | 
					        min-height: 56px;
 | 
				
			||||||
        align-items: center;
 | 
					        align-items: center;
 | 
				
			||||||
        --mdc-typography-body2-font-size: 1em;
 | 
					        --mdc-typography-body2-font-size: 1em;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					      p {
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .secondary {
 | 
				
			||||||
 | 
					        direction: var(--direction);
 | 
				
			||||||
 | 
					        padding-top: 4px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        color: var(--secondary-text-color);
 | 
				
			||||||
 | 
					        font-size: 0.875rem;
 | 
				
			||||||
 | 
					        font-weight: var(--mdc-typography-body2-font-weight, 400);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
 | 
				
			|||||||
        .label=${this.label || ""}
 | 
					        .label=${this.label || ""}
 | 
				
			||||||
        .required=${this.required}
 | 
					        .required=${this.required}
 | 
				
			||||||
        .helper=${this.helper}
 | 
					        .helper=${this.helper}
 | 
				
			||||||
        .disalbled=${this.disabled}
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
        @change=${this._valueChanged}
 | 
					        @change=${this._valueChanged}
 | 
				
			||||||
      ></ha-textfield>
 | 
					      ></ha-textfield>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -162,8 +162,14 @@ export class HaLocationSelector extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _computeLabel = (
 | 
					  private _computeLabel = (
 | 
				
			||||||
    entry: SchemaUnion<ReturnType<typeof this._schema>>
 | 
					    entry: SchemaUnion<ReturnType<typeof this._schema>>
 | 
				
			||||||
  ): string =>
 | 
					  ): string => {
 | 
				
			||||||
    this.hass.localize(`ui.components.selectors.location.${entry.name}`);
 | 
					    if (entry.name) {
 | 
				
			||||||
 | 
					      return this.hass.localize(
 | 
				
			||||||
 | 
					        `ui.components.selectors.location.${entry.name}`
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return "";
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static styles = css`
 | 
					  static styles = css`
 | 
				
			||||||
    ha-locations-editor {
 | 
					    ha-locations-editor {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,11 @@
 | 
				
			|||||||
import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit";
 | 
					import {
 | 
				
			||||||
 | 
					  css,
 | 
				
			||||||
 | 
					  CSSResultGroup,
 | 
				
			||||||
 | 
					  html,
 | 
				
			||||||
 | 
					  LitElement,
 | 
				
			||||||
 | 
					  nothing,
 | 
				
			||||||
 | 
					  PropertyValues,
 | 
				
			||||||
 | 
					} from "lit";
 | 
				
			||||||
import { customElement, property } from "lit/decorators";
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
import { classMap } from "lit/directives/class-map";
 | 
					import { classMap } from "lit/directives/class-map";
 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
@@ -60,12 +67,12 @@ export class HaNumberSelector extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
 | 
					      ${this.label && !isBox
 | 
				
			||||||
 | 
					        ? html`${this.label}${this.required ? "*" : ""}`
 | 
				
			||||||
 | 
					        : nothing}
 | 
				
			||||||
      <div class="input">
 | 
					      <div class="input">
 | 
				
			||||||
        ${!isBox
 | 
					        ${!isBox
 | 
				
			||||||
          ? html`
 | 
					          ? html`
 | 
				
			||||||
              ${this.label
 | 
					 | 
				
			||||||
                ? html`${this.label}${this.required ? "*" : ""}`
 | 
					 | 
				
			||||||
                : ""}
 | 
					 | 
				
			||||||
              <ha-slider
 | 
					              <ha-slider
 | 
				
			||||||
                labeled
 | 
					                labeled
 | 
				
			||||||
                .min=${this.selector.number!.min}
 | 
					                .min=${this.selector.number!.min}
 | 
				
			||||||
@@ -75,10 +82,11 @@ export class HaNumberSelector extends LitElement {
 | 
				
			|||||||
                .disabled=${this.disabled}
 | 
					                .disabled=${this.disabled}
 | 
				
			||||||
                .required=${this.required}
 | 
					                .required=${this.required}
 | 
				
			||||||
                @change=${this._handleSliderChange}
 | 
					                @change=${this._handleSliderChange}
 | 
				
			||||||
 | 
					                .ticks=${this.selector.number?.slider_ticks}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
              </ha-slider>
 | 
					              </ha-slider>
 | 
				
			||||||
            `
 | 
					            `
 | 
				
			||||||
          : ""}
 | 
					          : nothing}
 | 
				
			||||||
        <ha-textfield
 | 
					        <ha-textfield
 | 
				
			||||||
          .inputMode=${this.selector.number?.step === "any" ||
 | 
					          .inputMode=${this.selector.number?.step === "any" ||
 | 
				
			||||||
          (this.selector.number?.step ?? 1) % 1 !== 0
 | 
					          (this.selector.number?.step ?? 1) % 1 !== 0
 | 
				
			||||||
@@ -105,7 +113,7 @@ export class HaNumberSelector extends LitElement {
 | 
				
			|||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      ${!isBox && this.helper
 | 
					      ${!isBox && this.helper
 | 
				
			||||||
        ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
 | 
					        ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>`
 | 
				
			||||||
        : ""}
 | 
					        : nothing}
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -141,6 +149,9 @@ export class HaNumberSelector extends LitElement {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      ha-slider {
 | 
					      ha-slider {
 | 
				
			||||||
        flex: 1;
 | 
					        flex: 1;
 | 
				
			||||||
 | 
					        margin-right: 16px;
 | 
				
			||||||
 | 
					        margin-inline-end: 16px;
 | 
				
			||||||
 | 
					        margin-inline-start: 0;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      ha-textfield {
 | 
					      ha-textfield {
 | 
				
			||||||
        --ha-textfield-input-width: 40px;
 | 
					        --ha-textfield-input-width: 40px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -82,6 +82,7 @@ export class HaTextSelector extends LitElement {
 | 
				
			|||||||
        .disabled=${this.disabled}
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
        .type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
 | 
					        .type=${this._unmaskedPassword ? "text" : this.selector.text?.type}
 | 
				
			||||||
        @input=${this._handleChange}
 | 
					        @input=${this._handleChange}
 | 
				
			||||||
 | 
					        @change=${this._handleChange}
 | 
				
			||||||
        .label=${this.label || ""}
 | 
					        .label=${this.label || ""}
 | 
				
			||||||
        .prefix=${this.selector.text?.prefix}
 | 
					        .prefix=${this.selector.text?.prefix}
 | 
				
			||||||
        .suffix=${this.selector.text?.type === "password"
 | 
					        .suffix=${this.selector.text?.type === "password"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -30,7 +30,7 @@ export class HaTimeSelector extends LitElement {
 | 
				
			|||||||
        clearable
 | 
					        clearable
 | 
				
			||||||
        .helper=${this.helper}
 | 
					        .helper=${this.helper}
 | 
				
			||||||
        .label=${this.label}
 | 
					        .label=${this.label}
 | 
				
			||||||
        enable-second
 | 
					        .enableSecond=${!this.selector.time?.no_second}
 | 
				
			||||||
      ></ha-time-input>
 | 
					      ></ha-time-input>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,6 +44,7 @@ import "./ha-service-picker";
 | 
				
			|||||||
import "./ha-settings-row";
 | 
					import "./ha-settings-row";
 | 
				
			||||||
import "./ha-yaml-editor";
 | 
					import "./ha-yaml-editor";
 | 
				
			||||||
import type { HaYamlEditor } from "./ha-yaml-editor";
 | 
					import type { HaYamlEditor } from "./ha-yaml-editor";
 | 
				
			||||||
 | 
					import "./ha-service-section-icon";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const attributeFilter = (values: any[], attribute: any) => {
 | 
					const attributeFilter = (values: any[], attribute: any) => {
 | 
				
			||||||
  if (typeof attribute === "object") {
 | 
					  if (typeof attribute === "object") {
 | 
				
			||||||
@@ -239,12 +240,24 @@ export class HaServiceControl extends LitElement {
 | 
				
			|||||||
        ...value,
 | 
					        ...value,
 | 
				
			||||||
        selector: value.selector as Selector | undefined,
 | 
					        selector: value.selector as Selector | undefined,
 | 
				
			||||||
      }));
 | 
					      }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const hasSelector: string[] = [];
 | 
				
			||||||
 | 
					      fields.forEach((field) => {
 | 
				
			||||||
 | 
					        if ((field as any).fields) {
 | 
				
			||||||
 | 
					          Object.entries((field as any).fields).forEach(([key, subField]) => {
 | 
				
			||||||
 | 
					            if ((subField as any).selector) {
 | 
				
			||||||
 | 
					              hasSelector.push(key);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        } else if (field.selector) {
 | 
				
			||||||
 | 
					          hasSelector.push(field.key);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        ...serviceDomains[domain][serviceName],
 | 
					        ...serviceDomains[domain][serviceName],
 | 
				
			||||||
        fields,
 | 
					        fields,
 | 
				
			||||||
        hasSelector: fields.length
 | 
					        hasSelector,
 | 
				
			||||||
          ? fields.filter((field) => field.selector).map((field) => field.key)
 | 
					 | 
				
			||||||
          : [],
 | 
					 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
@@ -496,12 +509,18 @@ export class HaServiceControl extends LitElement {
 | 
				
			|||||||
                ) ||
 | 
					                ) ||
 | 
				
			||||||
                dataField.name ||
 | 
					                dataField.name ||
 | 
				
			||||||
                dataField.key}
 | 
					                dataField.key}
 | 
				
			||||||
              >
 | 
					                .secondary=${this._getSectionDescription(
 | 
				
			||||||
                ${this._renderSectionDescription(
 | 
					 | 
				
			||||||
                  dataField,
 | 
					                  dataField,
 | 
				
			||||||
                  domain,
 | 
					                  domain,
 | 
				
			||||||
                  serviceName
 | 
					                  serviceName
 | 
				
			||||||
                )}
 | 
					                )}
 | 
				
			||||||
 | 
					              >
 | 
				
			||||||
 | 
					                <ha-service-section-icon
 | 
				
			||||||
 | 
					                  slot="icons"
 | 
				
			||||||
 | 
					                  .hass=${this.hass}
 | 
				
			||||||
 | 
					                  .service=${this._value!.action}
 | 
				
			||||||
 | 
					                  .section=${dataField.key}
 | 
				
			||||||
 | 
					                ></ha-service-section-icon>
 | 
				
			||||||
                ${Object.entries(dataField.fields).map(([key, field]) =>
 | 
					                ${Object.entries(dataField.fields).map(([key, field]) =>
 | 
				
			||||||
                  this._renderField(
 | 
					                  this._renderField(
 | 
				
			||||||
                    { key, ...field },
 | 
					                    { key, ...field },
 | 
				
			||||||
@@ -522,20 +541,14 @@ export class HaServiceControl extends LitElement {
 | 
				
			|||||||
        )} `;
 | 
					        )} `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _renderSectionDescription(
 | 
					  private _getSectionDescription(
 | 
				
			||||||
    dataField: ExtHassService["fields"][number],
 | 
					    dataField: ExtHassService["fields"][number],
 | 
				
			||||||
    domain: string | undefined,
 | 
					    domain: string | undefined,
 | 
				
			||||||
    serviceName: string | undefined
 | 
					    serviceName: string | undefined
 | 
				
			||||||
  ) {
 | 
					  ) {
 | 
				
			||||||
    const description = this.hass!.localize(
 | 
					    return this.hass!.localize(
 | 
				
			||||||
      `component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
 | 
					      `component.${domain}.services.${serviceName}.sections.${dataField.key}.description`
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!description) {
 | 
					 | 
				
			||||||
      return nothing;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html`<p>${description}</p>`;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _renderField = (
 | 
					  private _renderField = (
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										53
									
								
								src/components/ha-service-section-icon.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								src/components/ha-service-section-icon.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,53 @@
 | 
				
			|||||||
 | 
					import { html, LitElement, nothing } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
 | 
					import { until } from "lit/directives/until";
 | 
				
			||||||
 | 
					import { HomeAssistant } from "../types";
 | 
				
			||||||
 | 
					import "./ha-icon";
 | 
				
			||||||
 | 
					import "./ha-svg-icon";
 | 
				
			||||||
 | 
					import { serviceSectionIcon } from "../data/icons";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("ha-service-section-icon")
 | 
				
			||||||
 | 
					export class HaServiceSectionIcon extends LitElement {
 | 
				
			||||||
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public service?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public section?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public icon?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected render() {
 | 
				
			||||||
 | 
					    if (this.icon) {
 | 
				
			||||||
 | 
					      return html`<ha-icon .icon=${this.icon}></ha-icon>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.service || !this.section) {
 | 
				
			||||||
 | 
					      return nothing;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.hass) {
 | 
				
			||||||
 | 
					      return this._renderFallback();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const icon = serviceSectionIcon(this.hass, this.service, this.section).then(
 | 
				
			||||||
 | 
					      (icn) => {
 | 
				
			||||||
 | 
					        if (icn) {
 | 
				
			||||||
 | 
					          return html`<ha-icon .icon=${icn}></ha-icon>`;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return this._renderFallback();
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html`${until(icon)}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _renderFallback() {
 | 
				
			||||||
 | 
					    return nothing;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					    "ha-service-section-icon": HaServiceSectionIcon;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -20,6 +20,7 @@ export class HaSlider extends MdSlider {
 | 
				
			|||||||
        --md-sys-color-on-surface: var(--primary-text-color);
 | 
					        --md-sys-color-on-surface: var(--primary-text-color);
 | 
				
			||||||
        --md-slider-handle-width: 14px;
 | 
					        --md-slider-handle-width: 14px;
 | 
				
			||||||
        --md-slider-handle-height: 14px;
 | 
					        --md-slider-handle-height: 14px;
 | 
				
			||||||
 | 
					        --md-slider-state-layer-size: 24px;
 | 
				
			||||||
        min-width: 100px;
 | 
					        min-width: 100px;
 | 
				
			||||||
        min-inline-size: 100px;
 | 
					        min-inline-size: 100px;
 | 
				
			||||||
        width: 200px;
 | 
					        width: 200px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,11 +16,10 @@ import { HomeAssistant } from "../types";
 | 
				
			|||||||
import "./ha-list-item";
 | 
					import "./ha-list-item";
 | 
				
			||||||
import "./ha-select";
 | 
					import "./ha-select";
 | 
				
			||||||
import type { HaSelect } from "./ha-select";
 | 
					import type { HaSelect } from "./ha-select";
 | 
				
			||||||
 | 
					import { computeDomain } from "../common/entity/compute_domain";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NONE = "__NONE_OPTION__";
 | 
					const NONE = "__NONE_OPTION__";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NAME_MAP = { cloud: "Home Assistant Cloud" };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ha-stt-picker")
 | 
					@customElement("ha-stt-picker")
 | 
				
			||||||
export class HaSTTPicker extends LitElement {
 | 
					export class HaSTTPicker extends LitElement {
 | 
				
			||||||
  @property() public value?: string;
 | 
					  @property() public value?: string;
 | 
				
			||||||
@@ -41,13 +40,32 @@ export class HaSTTPicker extends LitElement {
 | 
				
			|||||||
    if (!this._engines) {
 | 
					    if (!this._engines) {
 | 
				
			||||||
      return nothing;
 | 
					      return nothing;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const value =
 | 
					
 | 
				
			||||||
      this.value ??
 | 
					    let value = this.value;
 | 
				
			||||||
      (this.required
 | 
					    if (!value && this.required) {
 | 
				
			||||||
        ? this._engines.find(
 | 
					      for (const entity of Object.values(this.hass.entities)) {
 | 
				
			||||||
            (engine) => engine.supported_languages?.length !== 0
 | 
					        if (
 | 
				
			||||||
          )
 | 
					          entity.platform === "cloud" &&
 | 
				
			||||||
        : NONE);
 | 
					          computeDomain(entity.entity_id) === "stt"
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          value = entity.entity_id;
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!value) {
 | 
				
			||||||
 | 
					        for (const sttEngine of this._engines) {
 | 
				
			||||||
 | 
					          if (sttEngine?.supported_languages?.length !== 0) {
 | 
				
			||||||
 | 
					            value = sttEngine.engine_id;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!value) {
 | 
				
			||||||
 | 
					      value = NONE;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-select
 | 
					      <ha-select
 | 
				
			||||||
        .label=${this.label ||
 | 
					        .label=${this.label ||
 | 
				
			||||||
@@ -66,12 +84,15 @@ export class HaSTTPicker extends LitElement {
 | 
				
			|||||||
            </ha-list-item>`
 | 
					            </ha-list-item>`
 | 
				
			||||||
          : nothing}
 | 
					          : nothing}
 | 
				
			||||||
        ${this._engines.map((engine) => {
 | 
					        ${this._engines.map((engine) => {
 | 
				
			||||||
          let label = engine.engine_id;
 | 
					          if (engine.deprecated && engine.engine_id !== value) {
 | 
				
			||||||
 | 
					            return nothing;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          let label: string;
 | 
				
			||||||
          if (engine.engine_id.includes(".")) {
 | 
					          if (engine.engine_id.includes(".")) {
 | 
				
			||||||
            const stateObj = this.hass!.states[engine.engine_id];
 | 
					            const stateObj = this.hass!.states[engine.engine_id];
 | 
				
			||||||
            label = stateObj ? computeStateName(stateObj) : engine.engine_id;
 | 
					            label = stateObj ? computeStateName(stateObj) : engine.engine_id;
 | 
				
			||||||
          } else if (engine.engine_id in NAME_MAP) {
 | 
					          } else {
 | 
				
			||||||
            label = NAME_MAP[engine.engine_id];
 | 
					            label = engine.name || engine.engine_id;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          return html`<ha-list-item
 | 
					          return html`<ha-list-item
 | 
				
			||||||
            .value=${engine.engine_id}
 | 
					            .value=${engine.engine_id}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,10 +35,6 @@ import {
 | 
				
			|||||||
  computeDeviceName,
 | 
					  computeDeviceName,
 | 
				
			||||||
} from "../data/device_registry";
 | 
					} from "../data/device_registry";
 | 
				
			||||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
					import { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  FloorRegistryEntry,
 | 
					 | 
				
			||||||
  subscribeFloorRegistry,
 | 
					 | 
				
			||||||
} from "../data/floor_registry";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  LabelRegistryEntry,
 | 
					  LabelRegistryEntry,
 | 
				
			||||||
  subscribeLabelRegistry,
 | 
					  subscribeLabelRegistry,
 | 
				
			||||||
@@ -103,17 +99,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @query(".add-container", true) private _addContainer?: HTMLDivElement;
 | 
					  @query(".add-container", true) private _addContainer?: HTMLDivElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _labels?: LabelRegistryEntry[];
 | 
					  @state() private _labels?: LabelRegistryEntry[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _opened = false;
 | 
					  private _opened = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
					  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
					 | 
				
			||||||
        this._floors = floors;
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
      subscribeLabelRegistry(this.hass.connection, (labels) => {
 | 
					      subscribeLabelRegistry(this.hass.connection, (labels) => {
 | 
				
			||||||
        this._labels = labels;
 | 
					        this._labels = labels;
 | 
				
			||||||
      }),
 | 
					      }),
 | 
				
			||||||
@@ -132,9 +123,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
      <div class="mdc-chip-set items">
 | 
					      <div class="mdc-chip-set items">
 | 
				
			||||||
        ${this.value?.floor_id
 | 
					        ${this.value?.floor_id
 | 
				
			||||||
          ? ensureArray(this.value.floor_id).map((floor_id) => {
 | 
					          ? ensureArray(this.value.floor_id).map((floor_id) => {
 | 
				
			||||||
              const floor = this._floors?.find(
 | 
					              const floor = this.hass.floors[floor_id];
 | 
				
			||||||
                (flr) => flr.floor_id === floor_id
 | 
					 | 
				
			||||||
              );
 | 
					 | 
				
			||||||
              return this._renderChip(
 | 
					              return this._renderChip(
 | 
				
			||||||
                "floor_id",
 | 
					                "floor_id",
 | 
				
			||||||
                floor_id,
 | 
					                floor_id,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -109,7 +109,7 @@ export class HaTextField extends TextFieldBase {
 | 
				
			|||||||
        color: var(--secondary-text-color);
 | 
					        color: var(--secondary-text-color);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      .mdc-text-field__icon {
 | 
					      .mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
 | 
				
			||||||
        color: var(--secondary-text-color);
 | 
					        color: var(--secondary-text-color);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,14 +16,10 @@ import { HomeAssistant } from "../types";
 | 
				
			|||||||
import "./ha-list-item";
 | 
					import "./ha-list-item";
 | 
				
			||||||
import "./ha-select";
 | 
					import "./ha-select";
 | 
				
			||||||
import type { HaSelect } from "./ha-select";
 | 
					import type { HaSelect } from "./ha-select";
 | 
				
			||||||
 | 
					import { computeDomain } from "../common/entity/compute_domain";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NONE = "__NONE_OPTION__";
 | 
					const NONE = "__NONE_OPTION__";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const NAME_MAP = {
 | 
					 | 
				
			||||||
  cloud: "Home Assistant Cloud",
 | 
					 | 
				
			||||||
  google_translate: "Google Translate",
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ha-tts-picker")
 | 
					@customElement("ha-tts-picker")
 | 
				
			||||||
export class HaTTSPicker extends LitElement {
 | 
					export class HaTTSPicker extends LitElement {
 | 
				
			||||||
  @property() public value?: string;
 | 
					  @property() public value?: string;
 | 
				
			||||||
@@ -44,13 +40,32 @@ export class HaTTSPicker extends LitElement {
 | 
				
			|||||||
    if (!this._engines) {
 | 
					    if (!this._engines) {
 | 
				
			||||||
      return nothing;
 | 
					      return nothing;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    const value =
 | 
					
 | 
				
			||||||
      this.value ??
 | 
					    let value = this.value;
 | 
				
			||||||
      (this.required
 | 
					    if (!value && this.required) {
 | 
				
			||||||
        ? this._engines.find(
 | 
					      for (const entity of Object.values(this.hass.entities)) {
 | 
				
			||||||
            (engine) => engine.supported_languages?.length !== 0
 | 
					        if (
 | 
				
			||||||
          )
 | 
					          entity.platform === "cloud" &&
 | 
				
			||||||
        : NONE);
 | 
					          computeDomain(entity.entity_id) === "tts"
 | 
				
			||||||
 | 
					        ) {
 | 
				
			||||||
 | 
					          value = entity.entity_id;
 | 
				
			||||||
 | 
					          break;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (!value) {
 | 
				
			||||||
 | 
					        for (const ttsEngine of this._engines) {
 | 
				
			||||||
 | 
					          if (ttsEngine?.supported_languages?.length !== 0) {
 | 
				
			||||||
 | 
					            value = ttsEngine.engine_id;
 | 
				
			||||||
 | 
					            break;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!value) {
 | 
				
			||||||
 | 
					      value = NONE;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-select
 | 
					      <ha-select
 | 
				
			||||||
        .label=${this.label ||
 | 
					        .label=${this.label ||
 | 
				
			||||||
@@ -69,12 +84,15 @@ export class HaTTSPicker extends LitElement {
 | 
				
			|||||||
            </ha-list-item>`
 | 
					            </ha-list-item>`
 | 
				
			||||||
          : nothing}
 | 
					          : nothing}
 | 
				
			||||||
        ${this._engines.map((engine) => {
 | 
					        ${this._engines.map((engine) => {
 | 
				
			||||||
          let label = engine.engine_id;
 | 
					          if (engine.deprecated && engine.engine_id !== value) {
 | 
				
			||||||
 | 
					            return nothing;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          let label: string;
 | 
				
			||||||
          if (engine.engine_id.includes(".")) {
 | 
					          if (engine.engine_id.includes(".")) {
 | 
				
			||||||
            const stateObj = this.hass!.states[engine.engine_id];
 | 
					            const stateObj = this.hass!.states[engine.engine_id];
 | 
				
			||||||
            label = stateObj ? computeStateName(stateObj) : engine.engine_id;
 | 
					            label = stateObj ? computeStateName(stateObj) : engine.engine_id;
 | 
				
			||||||
          } else if (engine.engine_id in NAME_MAP) {
 | 
					          } else {
 | 
				
			||||||
            label = NAME_MAP[engine.engine_id];
 | 
					            label = engine.name || engine.engine_id;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
          return html`<ha-list-item
 | 
					          return html`<ha-list-item
 | 
				
			||||||
            .value=${engine.engine_id}
 | 
					            .value=${engine.engine_id}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -109,7 +109,7 @@ class HaWebRtcPlayer extends LitElement {
 | 
				
			|||||||
    let candidates = ""; // Build an Offer SDP string with ice candidates
 | 
					    let candidates = ""; // Build an Offer SDP string with ice candidates
 | 
				
			||||||
    const iceResolver = new Promise<void>((resolve) => {
 | 
					    const iceResolver = new Promise<void>((resolve) => {
 | 
				
			||||||
      peerConnection.addEventListener("icecandidate", async (event) => {
 | 
					      peerConnection.addEventListener("icecandidate", async (event) => {
 | 
				
			||||||
        if (!event.candidate) {
 | 
					        if (!event.candidate?.candidate) {
 | 
				
			||||||
          resolve(); // Gathering complete
 | 
					          resolve(); // Gathering complete
 | 
				
			||||||
          return;
 | 
					          return;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
 | 
				
			|||||||
import { customElement, property } from "lit/decorators";
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
import { ensureArray } from "../../common/array/ensure-array";
 | 
					import { ensureArray } from "../../common/array/ensure-array";
 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
import { Condition, Trigger } from "../../data/automation";
 | 
					import { Condition, Trigger, flattenTriggers } from "../../data/automation";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  Action,
 | 
					  Action,
 | 
				
			||||||
  ChooseAction,
 | 
					  ChooseAction,
 | 
				
			||||||
@@ -572,8 +572,8 @@ export class HatScriptGraph extends LitElement {
 | 
				
			|||||||
    const paths = Object.keys(this.trackedNodes);
 | 
					    const paths = Object.keys(this.trackedNodes);
 | 
				
			||||||
    const trigger_nodes =
 | 
					    const trigger_nodes =
 | 
				
			||||||
      "trigger" in this.trace.config
 | 
					      "trigger" in this.trace.config
 | 
				
			||||||
        ? ensureArray(this.trace.config.trigger).map((trigger, i) =>
 | 
					        ? flattenTriggers(ensureArray(this.trace.config.trigger)).map(
 | 
				
			||||||
            this.render_trigger(trigger, i)
 | 
					            (trigger, i) => this.render_trigger(trigger, i)
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        : undefined;
 | 
					        : undefined;
 | 
				
			||||||
    try {
 | 
					    try {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import {
 | 
				
			|||||||
  HassEntityBase,
 | 
					  HassEntityBase,
 | 
				
			||||||
} from "home-assistant-js-websocket";
 | 
					} from "home-assistant-js-websocket";
 | 
				
			||||||
import { navigate } from "../common/navigate";
 | 
					import { navigate } from "../common/navigate";
 | 
				
			||||||
 | 
					import { ensureArray } from "../common/array/ensure-array";
 | 
				
			||||||
import { Context, HomeAssistant } from "../types";
 | 
					import { Context, HomeAssistant } from "../types";
 | 
				
			||||||
import { BlueprintInput } from "./blueprint";
 | 
					import { BlueprintInput } from "./blueprint";
 | 
				
			||||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
 | 
					import { DeviceCondition, DeviceTrigger } from "./device_automation";
 | 
				
			||||||
@@ -62,6 +63,10 @@ export interface ContextConstraint {
 | 
				
			|||||||
  user_id?: string | string[];
 | 
					  user_id?: string | string[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface TriggerList {
 | 
				
			||||||
 | 
					  triggers: Trigger | Trigger[] | undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface BaseTrigger {
 | 
					export interface BaseTrigger {
 | 
				
			||||||
  alias?: string;
 | 
					  alias?: string;
 | 
				
			||||||
  platform: string;
 | 
					  platform: string;
 | 
				
			||||||
@@ -373,6 +378,27 @@ export const normalizeAutomationConfig = <
 | 
				
			|||||||
  return config;
 | 
					  return config;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const flattenTriggers = (
 | 
				
			||||||
 | 
					  triggers: undefined | (Trigger | TriggerList)[]
 | 
				
			||||||
 | 
					): Trigger[] => {
 | 
				
			||||||
 | 
					  if (!triggers) {
 | 
				
			||||||
 | 
					    return [];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const flatTriggers: Trigger[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  triggers.forEach((t) => {
 | 
				
			||||||
 | 
					    if ("triggers" in t) {
 | 
				
			||||||
 | 
					      if (t.triggers) {
 | 
				
			||||||
 | 
					        flatTriggers.push(...ensureArray(t.triggers));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      flatTriggers.push(t);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					  return flatTriggers;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
 | 
					export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
 | 
				
			||||||
  initialAutomationEditorData = data;
 | 
					  initialAutomationEditorData = data;
 | 
				
			||||||
  navigate("/config/automation/edit/new");
 | 
					  navigate("/config/automation/edit/new");
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,9 +68,18 @@ export const describeTrigger = (
 | 
				
			|||||||
  hass: HomeAssistant,
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
  entityRegistry: EntityRegistryEntry[],
 | 
					  entityRegistry: EntityRegistryEntry[],
 | 
				
			||||||
  ignoreAlias = false
 | 
					  ignoreAlias = false
 | 
				
			||||||
) => {
 | 
					): string => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
 | 
					    const description = tryDescribeTrigger(
 | 
				
			||||||
 | 
					      trigger,
 | 
				
			||||||
 | 
					      hass,
 | 
				
			||||||
 | 
					      entityRegistry,
 | 
				
			||||||
 | 
					      ignoreAlias
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (typeof description !== "string") {
 | 
				
			||||||
 | 
					      throw new Error(String(description));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return description;
 | 
				
			||||||
  } catch (error: any) {
 | 
					  } catch (error: any) {
 | 
				
			||||||
    // eslint-disable-next-line no-console
 | 
					    // eslint-disable-next-line no-console
 | 
				
			||||||
    console.error(error);
 | 
					    console.error(error);
 | 
				
			||||||
@@ -700,9 +709,18 @@ export const describeCondition = (
 | 
				
			|||||||
  hass: HomeAssistant,
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
  entityRegistry: EntityRegistryEntry[],
 | 
					  entityRegistry: EntityRegistryEntry[],
 | 
				
			||||||
  ignoreAlias = false
 | 
					  ignoreAlias = false
 | 
				
			||||||
) => {
 | 
					): string => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
 | 
					    const description = tryDescribeCondition(
 | 
				
			||||||
 | 
					      condition,
 | 
				
			||||||
 | 
					      hass,
 | 
				
			||||||
 | 
					      entityRegistry,
 | 
				
			||||||
 | 
					      ignoreAlias
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (typeof description !== "string") {
 | 
				
			||||||
 | 
					      throw new Error(String(description));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return description;
 | 
				
			||||||
  } catch (error: any) {
 | 
					  } catch (error: any) {
 | 
				
			||||||
    // eslint-disable-next-line no-console
 | 
					    // eslint-disable-next-line no-console
 | 
				
			||||||
    console.error(error);
 | 
					    console.error(error);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,6 +1,6 @@
 | 
				
			|||||||
import type { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
					import type { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
import type { IntegrationManifest, IntegrationType } from "./integration";
 | 
					import type { IntegrationType } from "./integration";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ConfigEntry {
 | 
					export interface ConfigEntry {
 | 
				
			||||||
  entry_id: string;
 | 
					  entry_id: string;
 | 
				
			||||||
@@ -149,20 +149,19 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) =>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const sortConfigEntries = (
 | 
					export const sortConfigEntries = (
 | 
				
			||||||
  configEntries: ConfigEntry[],
 | 
					  configEntries: ConfigEntry[],
 | 
				
			||||||
  manifestLookup: { [domain: string]: IntegrationManifest }
 | 
					  primaryConfigEntry: string | null
 | 
				
			||||||
): ConfigEntry[] => {
 | 
					): ConfigEntry[] => {
 | 
				
			||||||
  const sortedConfigEntries = [...configEntries];
 | 
					  if (!primaryConfigEntry) {
 | 
				
			||||||
 | 
					    return configEntries;
 | 
				
			||||||
  const getScore = (entry: ConfigEntry) => {
 | 
					  }
 | 
				
			||||||
    const manifest = manifestLookup[entry.domain] as
 | 
					  const primaryEntry = configEntries.find(
 | 
				
			||||||
      | IntegrationManifest
 | 
					    (e) => e.entry_id === primaryConfigEntry
 | 
				
			||||||
      | undefined;
 | 
					  );
 | 
				
			||||||
    const isHelper = manifest?.integration_type === "helper";
 | 
					  if (!primaryEntry) {
 | 
				
			||||||
    return isHelper ? -1 : 1;
 | 
					    return configEntries;
 | 
				
			||||||
  };
 | 
					  }
 | 
				
			||||||
 | 
					  const otherEntries = configEntries.filter(
 | 
				
			||||||
  const configEntriesCompare = (a: ConfigEntry, b: ConfigEntry) =>
 | 
					    (e) => e.entry_id !== primaryConfigEntry
 | 
				
			||||||
    getScore(b) - getScore(a);
 | 
					  );
 | 
				
			||||||
 | 
					  return [primaryEntry, ...otherEntries];
 | 
				
			||||||
  return sortedConfigEntries.sort(configEntriesCompare);
 | 
					 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,10 +1,20 @@
 | 
				
			|||||||
export interface DataTableFilters {
 | 
					export interface DataTableFilters {
 | 
				
			||||||
  [key: string]: {
 | 
					  [key: string]: {
 | 
				
			||||||
    value: string[] | { key: string[] } | undefined;
 | 
					    value: DataTableFiltersValue;
 | 
				
			||||||
    items: Set<string> | undefined;
 | 
					    items: Set<string> | undefined;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface DataTableFiltersValues {
 | 
				
			||||||
 | 
					  [key: string]: DataTableFiltersValue;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface DataTableFiltersItems {
 | 
				
			||||||
 | 
					  [key: string]: Set<string> | undefined;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const serializeFilters = (value: DataTableFilters) => {
 | 
					export const serializeFilters = (value: DataTableFilters) => {
 | 
				
			||||||
  const serializedValue = {};
 | 
					  const serializedValue = {};
 | 
				
			||||||
  Object.entries(value).forEach(([key, val]) => {
 | 
					  Object.entries(value).forEach(([key, val]) => {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -33,6 +33,7 @@ export interface DeviceRegistryEntry extends RegistryEntry {
 | 
				
			|||||||
  entry_type: "service" | null;
 | 
					  entry_type: "service" | null;
 | 
				
			||||||
  disabled_by: "user" | "integration" | "config_entry" | null;
 | 
					  disabled_by: "user" | "integration" | "config_entry" | null;
 | 
				
			||||||
  configuration_url: string | null;
 | 
					  configuration_url: string | null;
 | 
				
			||||||
 | 
					  primary_config_entry: string | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface DeviceEntityDisplayLookup {
 | 
					export interface DeviceEntityDisplayLookup {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,6 +11,7 @@ import {
 | 
				
			|||||||
  isLastDayOfMonth,
 | 
					  isLastDayOfMonth,
 | 
				
			||||||
} from "date-fns";
 | 
					} from "date-fns";
 | 
				
			||||||
import { Collection, getCollection } from "home-assistant-js-websocket";
 | 
					import { Collection, getCollection } from "home-assistant-js-websocket";
 | 
				
			||||||
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  calcDate,
 | 
					  calcDate,
 | 
				
			||||||
  calcDateProperty,
 | 
					  calcDateProperty,
 | 
				
			||||||
@@ -791,3 +792,147 @@ export const getEnergyWaterUnit = (hass: HomeAssistant): string =>
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const energyStatisticHelpUrl =
 | 
					export const energyStatisticHelpUrl =
 | 
				
			||||||
  "/docs/energy/faq/#troubleshooting-missing-entities";
 | 
					  "/docs/energy/faq/#troubleshooting-missing-entities";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface EnergySumData {
 | 
				
			||||||
 | 
					  to_grid?: { [start: number]: number };
 | 
				
			||||||
 | 
					  from_grid?: { [start: number]: number };
 | 
				
			||||||
 | 
					  to_battery?: { [start: number]: number };
 | 
				
			||||||
 | 
					  from_battery?: { [start: number]: number };
 | 
				
			||||||
 | 
					  solar?: { [start: number]: number };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface EnergyConsumptionData {
 | 
				
			||||||
 | 
					  total: { [start: number]: number };
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getSummedData = memoizeOne(
 | 
				
			||||||
 | 
					  (
 | 
				
			||||||
 | 
					    data: EnergyData
 | 
				
			||||||
 | 
					  ): { summedData: EnergySumData; compareSummedData?: EnergySumData } => {
 | 
				
			||||||
 | 
					    const summedData = getSummedDataPartial(data);
 | 
				
			||||||
 | 
					    const compareSummedData = data.statsCompare
 | 
				
			||||||
 | 
					      ? getSummedDataPartial(data, true)
 | 
				
			||||||
 | 
					      : undefined;
 | 
				
			||||||
 | 
					    return { summedData, compareSummedData };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const getSummedDataPartial = (
 | 
				
			||||||
 | 
					  data: EnergyData,
 | 
				
			||||||
 | 
					  compare?: boolean
 | 
				
			||||||
 | 
					): EnergySumData => {
 | 
				
			||||||
 | 
					  const statIds: {
 | 
				
			||||||
 | 
					    to_grid?: string[];
 | 
				
			||||||
 | 
					    from_grid?: string[];
 | 
				
			||||||
 | 
					    solar?: string[];
 | 
				
			||||||
 | 
					    to_battery?: string[];
 | 
				
			||||||
 | 
					    from_battery?: string[];
 | 
				
			||||||
 | 
					  } = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  for (const source of data.prefs.energy_sources) {
 | 
				
			||||||
 | 
					    if (source.type === "solar") {
 | 
				
			||||||
 | 
					      if (statIds.solar) {
 | 
				
			||||||
 | 
					        statIds.solar.push(source.stat_energy_from);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        statIds.solar = [source.stat_energy_from];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (source.type === "battery") {
 | 
				
			||||||
 | 
					      if (statIds.to_battery) {
 | 
				
			||||||
 | 
					        statIds.to_battery.push(source.stat_energy_to);
 | 
				
			||||||
 | 
					        statIds.from_battery!.push(source.stat_energy_from);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        statIds.to_battery = [source.stat_energy_to];
 | 
				
			||||||
 | 
					        statIds.from_battery = [source.stat_energy_from];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (source.type !== "grid") {
 | 
				
			||||||
 | 
					      continue;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // grid source
 | 
				
			||||||
 | 
					    for (const flowFrom of source.flow_from) {
 | 
				
			||||||
 | 
					      if (statIds.from_grid) {
 | 
				
			||||||
 | 
					        statIds.from_grid.push(flowFrom.stat_energy_from);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        statIds.from_grid = [flowFrom.stat_energy_from];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    for (const flowTo of source.flow_to) {
 | 
				
			||||||
 | 
					      if (statIds.to_grid) {
 | 
				
			||||||
 | 
					        statIds.to_grid.push(flowTo.stat_energy_to);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        statIds.to_grid = [flowTo.stat_energy_to];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const summedData: EnergySumData = {};
 | 
				
			||||||
 | 
					  Object.entries(statIds).forEach(([key, subStatIds]) => {
 | 
				
			||||||
 | 
					    const totalStats: { [start: number]: number } = {};
 | 
				
			||||||
 | 
					    const sets: { [statId: string]: { [start: number]: number } } = {};
 | 
				
			||||||
 | 
					    subStatIds!.forEach((id) => {
 | 
				
			||||||
 | 
					      const stats = compare ? data.statsCompare[id] : data.stats[id];
 | 
				
			||||||
 | 
					      if (!stats) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const set = {};
 | 
				
			||||||
 | 
					      stats.forEach((stat) => {
 | 
				
			||||||
 | 
					        if (stat.change === null || stat.change === undefined) {
 | 
				
			||||||
 | 
					          return;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        const val = stat.change;
 | 
				
			||||||
 | 
					        // Get total of solar and to grid to calculate the solar energy used
 | 
				
			||||||
 | 
					        totalStats[stat.start] =
 | 
				
			||||||
 | 
					          stat.start in totalStats ? totalStats[stat.start] + val : val;
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      sets[id] = set;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    summedData[key] = totalStats;
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return summedData;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const computeConsumptionData = memoizeOne(
 | 
				
			||||||
 | 
					  (
 | 
				
			||||||
 | 
					    data: EnergySumData,
 | 
				
			||||||
 | 
					    compareData?: EnergySumData
 | 
				
			||||||
 | 
					  ): {
 | 
				
			||||||
 | 
					    consumption: EnergyConsumptionData;
 | 
				
			||||||
 | 
					    compareConsumption?: EnergyConsumptionData;
 | 
				
			||||||
 | 
					  } => {
 | 
				
			||||||
 | 
					    const consumption = computeConsumptionDataPartial(data);
 | 
				
			||||||
 | 
					    const compareConsumption = compareData
 | 
				
			||||||
 | 
					      ? computeConsumptionDataPartial(compareData)
 | 
				
			||||||
 | 
					      : undefined;
 | 
				
			||||||
 | 
					    return { consumption, compareConsumption };
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const computeConsumptionDataPartial = (
 | 
				
			||||||
 | 
					  data: EnergySumData
 | 
				
			||||||
 | 
					): EnergyConsumptionData => {
 | 
				
			||||||
 | 
					  const outData: EnergyConsumptionData = { total: {} };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  Object.keys(data).forEach((type) => {
 | 
				
			||||||
 | 
					    Object.keys(data[type]).forEach((start) => {
 | 
				
			||||||
 | 
					      if (outData.total[start] === undefined) {
 | 
				
			||||||
 | 
					        const consumption =
 | 
				
			||||||
 | 
					          (data.from_grid?.[start] || 0) +
 | 
				
			||||||
 | 
					          (data.solar?.[start] || 0) +
 | 
				
			||||||
 | 
					          (data.from_battery?.[start] || 0) -
 | 
				
			||||||
 | 
					          (data.to_grid?.[start] || 0) -
 | 
				
			||||||
 | 
					          (data.to_battery?.[start] || 0);
 | 
				
			||||||
 | 
					        outData.total[start] = consumption;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return outData;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,7 +1,4 @@
 | 
				
			|||||||
import { Connection, createCollection } from "home-assistant-js-websocket";
 | 
					 | 
				
			||||||
import { Store } from "home-assistant-js-websocket/dist/store";
 | 
					 | 
				
			||||||
import { stringCompare } from "../common/string/compare";
 | 
					import { stringCompare } from "../common/string/compare";
 | 
				
			||||||
import { debounce } from "../common/util/debounce";
 | 
					 | 
				
			||||||
import { HomeAssistant } from "../types";
 | 
					import { HomeAssistant } from "../types";
 | 
				
			||||||
import { AreaRegistryEntry } from "./area_registry";
 | 
					import { AreaRegistryEntry } from "./area_registry";
 | 
				
			||||||
import { RegistryEntry } from "./registry";
 | 
					import { RegistryEntry } from "./registry";
 | 
				
			||||||
@@ -27,48 +24,6 @@ export interface FloorRegistryEntryMutableParams {
 | 
				
			|||||||
  aliases?: string[];
 | 
					  aliases?: string[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const fetchFloorRegistry = (conn: Connection) =>
 | 
					 | 
				
			||||||
  conn
 | 
					 | 
				
			||||||
    .sendMessagePromise({
 | 
					 | 
				
			||||||
      type: "config/floor_registry/list",
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .then((floors) =>
 | 
					 | 
				
			||||||
      (floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
 | 
					 | 
				
			||||||
        if (ent1.level !== ent2.level) {
 | 
					 | 
				
			||||||
          return (ent1.level ?? 9999) - (ent2.level ?? 9999);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return stringCompare(ent1.name, ent2.name);
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const subscribeFloorRegistryUpdates = (
 | 
					 | 
				
			||||||
  conn: Connection,
 | 
					 | 
				
			||||||
  store: Store<FloorRegistryEntry[]>
 | 
					 | 
				
			||||||
) =>
 | 
					 | 
				
			||||||
  conn.subscribeEvents(
 | 
					 | 
				
			||||||
    debounce(
 | 
					 | 
				
			||||||
      () =>
 | 
					 | 
				
			||||||
        fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
 | 
					 | 
				
			||||||
          store.setState(areas, true)
 | 
					 | 
				
			||||||
        ),
 | 
					 | 
				
			||||||
      500,
 | 
					 | 
				
			||||||
      true
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
    "floor_registry_updated"
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const subscribeFloorRegistry = (
 | 
					 | 
				
			||||||
  conn: Connection,
 | 
					 | 
				
			||||||
  onChange: (floors: FloorRegistryEntry[]) => void
 | 
					 | 
				
			||||||
) =>
 | 
					 | 
				
			||||||
  createCollection<FloorRegistryEntry[]>(
 | 
					 | 
				
			||||||
    "_floorRegistry",
 | 
					 | 
				
			||||||
    fetchFloorRegistry,
 | 
					 | 
				
			||||||
    subscribeFloorRegistryUpdates,
 | 
					 | 
				
			||||||
    conn,
 | 
					 | 
				
			||||||
    onChange
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const createFloorRegistryEntry = (
 | 
					export const createFloorRegistryEntry = (
 | 
				
			||||||
  hass: HomeAssistant,
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
  values: FloorRegistryEntryMutableParams
 | 
					  values: FloorRegistryEntryMutableParams
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -62,7 +62,7 @@ export interface ComponentIcons {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface ServiceIcons {
 | 
					interface ServiceIcons {
 | 
				
			||||||
  [service: string]: string;
 | 
					  [service: string]: { service: string; sections?: { [name: string]: string } };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IconCategory = "entity" | "entity_component" | "services";
 | 
					export type IconCategory = "entity" | "entity_component" | "services";
 | 
				
			||||||
@@ -288,7 +288,8 @@ export const serviceIcon = async (
 | 
				
			|||||||
  const serviceName = computeObjectId(service);
 | 
					  const serviceName = computeObjectId(service);
 | 
				
			||||||
  const serviceIcons = await getServiceIcons(hass, domain);
 | 
					  const serviceIcons = await getServiceIcons(hass, domain);
 | 
				
			||||||
  if (serviceIcons) {
 | 
					  if (serviceIcons) {
 | 
				
			||||||
    icon = serviceIcons[serviceName] as string;
 | 
					    const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
 | 
				
			||||||
 | 
					    icon = srvceIcon?.service;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (!icon) {
 | 
					  if (!icon) {
 | 
				
			||||||
    icon = await domainIcon(hass, domain);
 | 
					    icon = await domainIcon(hass, domain);
 | 
				
			||||||
@@ -296,6 +297,21 @@ export const serviceIcon = async (
 | 
				
			|||||||
  return icon;
 | 
					  return icon;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const serviceSectionIcon = async (
 | 
				
			||||||
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
 | 
					  service: string,
 | 
				
			||||||
 | 
					  section: string
 | 
				
			||||||
 | 
					): Promise<string | undefined> => {
 | 
				
			||||||
 | 
					  const domain = computeDomain(service);
 | 
				
			||||||
 | 
					  const serviceName = computeObjectId(service);
 | 
				
			||||||
 | 
					  const serviceIcons = await getServiceIcons(hass, domain);
 | 
				
			||||||
 | 
					  if (serviceIcons) {
 | 
				
			||||||
 | 
					    const srvceIcon = serviceIcons[serviceName] as ServiceIcons[string];
 | 
				
			||||||
 | 
					    return srvceIcon?.sections?.[section];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return undefined;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const domainIcon = async (
 | 
					export const domainIcon = async (
 | 
				
			||||||
  hass: HomeAssistant,
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
  domain: string,
 | 
					  domain: string,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -13,7 +13,7 @@ export const ensureBadgeConfig = (
 | 
				
			|||||||
    return {
 | 
					    return {
 | 
				
			||||||
      type: "entity",
 | 
					      type: "entity",
 | 
				
			||||||
      entity: config,
 | 
					      entity: config,
 | 
				
			||||||
      display_type: "complete",
 | 
					      show_name: true,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if ("type" in config && config.type) {
 | 
					  if ("type" in config && config.type) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,6 +5,8 @@ import type { LovelaceStrategyConfig } from "./strategy";
 | 
				
			|||||||
export interface LovelaceBaseSectionConfig {
 | 
					export interface LovelaceBaseSectionConfig {
 | 
				
			||||||
  title?: string;
 | 
					  title?: string;
 | 
				
			||||||
  visibility?: Condition[];
 | 
					  visibility?: Condition[];
 | 
				
			||||||
 | 
					  column_span?: number;
 | 
				
			||||||
 | 
					  row_span?: number;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
 | 
					export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,9 @@ export interface LovelaceBaseViewConfig {
 | 
				
			|||||||
  visible?: boolean | ShowViewConfig[];
 | 
					  visible?: boolean | ShowViewConfig[];
 | 
				
			||||||
  subview?: boolean;
 | 
					  subview?: boolean;
 | 
				
			||||||
  back_path?: string;
 | 
					  back_path?: string;
 | 
				
			||||||
  max_columns?: number; // Only used for section view, it should move to a section view config type when the views will have dedicated editor.
 | 
					  // Only used for section view, it should move to a section view config type when the views will have dedicated editor.
 | 
				
			||||||
 | 
					  max_columns?: number;
 | 
				
			||||||
 | 
					  dense_section_placement?: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
 | 
					export interface LovelaceViewConfig extends LovelaceBaseViewConfig {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,6 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
				
			|||||||
import { navigate } from "../common/navigate";
 | 
					import { navigate } from "../common/navigate";
 | 
				
			||||||
import { HomeAssistant } from "../types";
 | 
					import { HomeAssistant } from "../types";
 | 
				
			||||||
import { subscribeDeviceRegistry } from "./device_registry";
 | 
					import { subscribeDeviceRegistry } from "./device_registry";
 | 
				
			||||||
 | 
					import { isComponentLoaded } from "../common/config/is_component_loaded";
 | 
				
			||||||
 | 
					import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export enum NetworkType {
 | 
					export enum NetworkType {
 | 
				
			||||||
  THREAD = "thread",
 | 
					  THREAD = "thread",
 | 
				
			||||||
@@ -51,10 +53,30 @@ export interface MatterCommissioningParameters {
 | 
				
			|||||||
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
 | 
					export const canCommissionMatterExternal = (hass: HomeAssistant) =>
 | 
				
			||||||
  hass.auth.external?.config.canCommissionMatter;
 | 
					  hass.auth.external?.config.canCommissionMatter;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const startExternalCommissioning = (hass: HomeAssistant) =>
 | 
					export const startExternalCommissioning = async (hass: HomeAssistant) => {
 | 
				
			||||||
  hass.auth.external!.fireMessage({
 | 
					  if (isComponentLoaded(hass, "thread")) {
 | 
				
			||||||
 | 
					    const datasets = await listThreadDataSets(hass);
 | 
				
			||||||
 | 
					    const preferredDataset = datasets.datasets.find(
 | 
				
			||||||
 | 
					      (dataset) => dataset.preferred
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					    if (preferredDataset) {
 | 
				
			||||||
 | 
					      return hass.auth.external!.fireMessage({
 | 
				
			||||||
 | 
					        type: "matter/commission",
 | 
				
			||||||
 | 
					        payload: {
 | 
				
			||||||
 | 
					          active_operational_dataset: (
 | 
				
			||||||
 | 
					            await getThreadDataSetTLV(hass, preferredDataset.dataset_id)
 | 
				
			||||||
 | 
					          ).tlv,
 | 
				
			||||||
 | 
					          border_agent_id: preferredDataset.preferred_border_agent_id,
 | 
				
			||||||
 | 
					          mac_extended_address: preferredDataset.preferred_extended_address,
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  return hass.auth.external!.fireMessage({
 | 
				
			||||||
    type: "matter/commission",
 | 
					    type: "matter/commission",
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const redirectOnNewMatterDevice = (
 | 
					export const redirectOnNewMatterDevice = (
 | 
				
			||||||
  hass: HomeAssistant,
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -50,7 +50,7 @@ export const describeAction = <T extends ActionType>(
 | 
				
			|||||||
  ignoreAlias = false
 | 
					  ignoreAlias = false
 | 
				
			||||||
): string => {
 | 
					): string => {
 | 
				
			||||||
  try {
 | 
					  try {
 | 
				
			||||||
    return tryDescribeAction(
 | 
					    const description = tryDescribeAction(
 | 
				
			||||||
      hass,
 | 
					      hass,
 | 
				
			||||||
      entityRegistry,
 | 
					      entityRegistry,
 | 
				
			||||||
      labelRegistry,
 | 
					      labelRegistry,
 | 
				
			||||||
@@ -59,6 +59,10 @@ export const describeAction = <T extends ActionType>(
 | 
				
			|||||||
      actionType,
 | 
					      actionType,
 | 
				
			||||||
      ignoreAlias
 | 
					      ignoreAlias
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					    if (typeof description !== "string") {
 | 
				
			||||||
 | 
					      throw new Error(String(description));
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    return description;
 | 
				
			||||||
  } catch (error: any) {
 | 
					  } catch (error: any) {
 | 
				
			||||||
    // eslint-disable-next-line no-console
 | 
					    // eslint-disable-next-line no-console
 | 
				
			||||||
    console.error(error);
 | 
					    console.error(error);
 | 
				
			||||||
@@ -127,6 +131,12 @@ const tryDescribeAction = <T extends ActionType>(
 | 
				
			|||||||
                targets.push(
 | 
					                targets.push(
 | 
				
			||||||
                  computeEntityRegistryName(hass, entityReg) || targetThing
 | 
					                  computeEntityRegistryName(hass, entityReg) || targetThing
 | 
				
			||||||
                );
 | 
					                );
 | 
				
			||||||
 | 
					              } else if (targetThing === "all") {
 | 
				
			||||||
 | 
					                targets.push(
 | 
				
			||||||
 | 
					                  hass.localize(
 | 
				
			||||||
 | 
					                    `${actionTranslationBaseKey}.service.description.target_every_entity`
 | 
				
			||||||
 | 
					                  )
 | 
				
			||||||
 | 
					                );
 | 
				
			||||||
              } else {
 | 
					              } else {
 | 
				
			||||||
                targets.push(
 | 
					                targets.push(
 | 
				
			||||||
                  hass.localize(
 | 
					                  hass.localize(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -101,9 +101,8 @@ export interface AttributeSelector {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface BooleanSelector {
 | 
					export interface BooleanSelector {
 | 
				
			||||||
  boolean: {
 | 
					  // eslint-disable-next-line @typescript-eslint/ban-types
 | 
				
			||||||
    mode?: "checkbox" | "switch";
 | 
					  boolean: {} | null;
 | 
				
			||||||
  } | null;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ColorRGBSelector {
 | 
					export interface ColorRGBSelector {
 | 
				
			||||||
@@ -324,6 +323,7 @@ export interface NumberSelector {
 | 
				
			|||||||
    step?: number | "any";
 | 
					    step?: number | "any";
 | 
				
			||||||
    mode?: "box" | "slider";
 | 
					    mode?: "box" | "slider";
 | 
				
			||||||
    unit_of_measurement?: string;
 | 
					    unit_of_measurement?: string;
 | 
				
			||||||
 | 
					    slider_ticks?: boolean;
 | 
				
			||||||
  } | null;
 | 
					  } | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -428,8 +428,7 @@ export interface ThemeSelector {
 | 
				
			|||||||
  theme: { include_default?: boolean } | null;
 | 
					  theme: { include_default?: boolean } | null;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
export interface TimeSelector {
 | 
					export interface TimeSelector {
 | 
				
			||||||
  // eslint-disable-next-line @typescript-eslint/ban-types
 | 
					  time: { no_second?: boolean } | null;
 | 
				
			||||||
  time: {} | null;
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface TriggerSelector {
 | 
					export interface TriggerSelector {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,8 @@ export interface SpeechMetadata {
 | 
				
			|||||||
export interface STTEngine {
 | 
					export interface STTEngine {
 | 
				
			||||||
  engine_id: string;
 | 
					  engine_id: string;
 | 
				
			||||||
  supported_languages?: string[];
 | 
					  supported_languages?: string[];
 | 
				
			||||||
 | 
					  name?: string;
 | 
				
			||||||
 | 
					  deprecated: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const listSTTEngines = (
 | 
					export const listSTTEngines = (
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,7 @@ import { Context, HomeAssistant } from "../types";
 | 
				
			|||||||
import {
 | 
					import {
 | 
				
			||||||
  BlueprintAutomationConfig,
 | 
					  BlueprintAutomationConfig,
 | 
				
			||||||
  ManualAutomationConfig,
 | 
					  ManualAutomationConfig,
 | 
				
			||||||
 | 
					  flattenTriggers,
 | 
				
			||||||
} from "./automation";
 | 
					} from "./automation";
 | 
				
			||||||
import { BlueprintScriptConfig, ScriptConfig } from "./script";
 | 
					import { BlueprintScriptConfig, ScriptConfig } from "./script";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -190,7 +191,11 @@ export const getDataFromPath = (
 | 
				
			|||||||
      if (!tempResult && raw === "sequence") {
 | 
					      if (!tempResult && raw === "sequence") {
 | 
				
			||||||
        continue;
 | 
					        continue;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      result = tempResult;
 | 
					      if (raw === "trigger") {
 | 
				
			||||||
 | 
					        result = flattenTriggers(tempResult);
 | 
				
			||||||
 | 
					      } else {
 | 
				
			||||||
 | 
					        result = tempResult;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
      continue;
 | 
					      continue;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ import { HomeAssistant } from "../types";
 | 
				
			|||||||
export interface TTSEngine {
 | 
					export interface TTSEngine {
 | 
				
			||||||
  engine_id: string;
 | 
					  engine_id: string;
 | 
				
			||||||
  supported_languages?: string[];
 | 
					  supported_languages?: string[];
 | 
				
			||||||
 | 
					  name?: string;
 | 
				
			||||||
 | 
					  deprecated: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface TTSVoice {
 | 
					export interface TTSVoice {
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										47
									
								
								src/data/ws-floor_registry.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										47
									
								
								src/data/ws-floor_registry.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,47 @@
 | 
				
			|||||||
 | 
					import { Connection, createCollection } from "home-assistant-js-websocket";
 | 
				
			||||||
 | 
					import { Store } from "home-assistant-js-websocket/dist/store";
 | 
				
			||||||
 | 
					import { stringCompare } from "../common/string/compare";
 | 
				
			||||||
 | 
					import { debounce } from "../common/util/debounce";
 | 
				
			||||||
 | 
					import { FloorRegistryEntry } from "./floor_registry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const fetchFloorRegistry = (conn: Connection) =>
 | 
				
			||||||
 | 
					  conn
 | 
				
			||||||
 | 
					    .sendMessagePromise({
 | 
				
			||||||
 | 
					      type: "config/floor_registry/list",
 | 
				
			||||||
 | 
					    })
 | 
				
			||||||
 | 
					    .then((floors) =>
 | 
				
			||||||
 | 
					      (floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
 | 
				
			||||||
 | 
					        if (ent1.level !== ent2.level) {
 | 
				
			||||||
 | 
					          return (ent1.level ?? 9999) - (ent2.level ?? 9999);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        return stringCompare(ent1.name, ent2.name);
 | 
				
			||||||
 | 
					      })
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					const subscribeFloorRegistryUpdates = (
 | 
				
			||||||
 | 
					  conn: Connection,
 | 
				
			||||||
 | 
					  store: Store<FloorRegistryEntry[]>
 | 
				
			||||||
 | 
					) =>
 | 
				
			||||||
 | 
					  conn.subscribeEvents(
 | 
				
			||||||
 | 
					    debounce(
 | 
				
			||||||
 | 
					      () =>
 | 
				
			||||||
 | 
					        fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
 | 
				
			||||||
 | 
					          store.setState(areas, true)
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      500,
 | 
				
			||||||
 | 
					      true
 | 
				
			||||||
 | 
					    ),
 | 
				
			||||||
 | 
					    "floor_registry_updated"
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const subscribeFloorRegistry = (
 | 
				
			||||||
 | 
					  conn: Connection,
 | 
				
			||||||
 | 
					  onChange: (floors: FloorRegistryEntry[]) => void
 | 
				
			||||||
 | 
					) =>
 | 
				
			||||||
 | 
					  createCollection<FloorRegistryEntry[]>(
 | 
				
			||||||
 | 
					    "_floorRegistry",
 | 
				
			||||||
 | 
					    fetchFloorRegistry,
 | 
				
			||||||
 | 
					    subscribeFloorRegistryUpdates,
 | 
				
			||||||
 | 
					    conn,
 | 
				
			||||||
 | 
					    onChange
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
@@ -252,6 +252,7 @@ export interface ZWaveJSNodeConfigParamMetadata {
 | 
				
			|||||||
  type: string;
 | 
					  type: string;
 | 
				
			||||||
  unit: string;
 | 
					  unit: string;
 | 
				
			||||||
  states: { [key: number]: string };
 | 
					  states: { [key: number]: string };
 | 
				
			||||||
 | 
					  default: any;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface ZWaveJSSetConfigParamData {
 | 
					export interface ZWaveJSSetConfigParamData {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -76,17 +76,36 @@ export const showConfigFlowDialog = (
 | 
				
			|||||||
        : "";
 | 
					        : "";
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    renderShowFormStepFieldLabel(hass, step, field) {
 | 
					    renderShowFormStepFieldLabel(hass, step, field, options) {
 | 
				
			||||||
      return hass.localize(
 | 
					      if (field.type === "expandable") {
 | 
				
			||||||
        `component.${step.handler}.config.step.${step.step_id}.data.${field.name}`
 | 
					        return hass.localize(
 | 
				
			||||||
 | 
					          `component.${step.handler}.config.step.${step.step_id}.sections.${field.name}.name`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return (
 | 
				
			||||||
 | 
					        hass.localize(
 | 
				
			||||||
 | 
					          `component.${step.handler}.config.step.${step.step_id}.${prefix}data.${field.name}`
 | 
				
			||||||
 | 
					        ) || field.name
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    renderShowFormStepFieldHelper(hass, step, field) {
 | 
					    renderShowFormStepFieldHelper(hass, step, field, options) {
 | 
				
			||||||
 | 
					      if (field.type === "expandable") {
 | 
				
			||||||
 | 
					        return hass.localize(
 | 
				
			||||||
 | 
					          `component.${step.translation_domain || step.handler}.config.step.${step.step_id}.sections.${field.name}.description`
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const description = hass.localize(
 | 
					      const description = hass.localize(
 | 
				
			||||||
        `component.${step.translation_domain || step.handler}.config.step.${step.step_id}.data_description.${field.name}`,
 | 
					        `component.${step.translation_domain || step.handler}.config.step.${step.step_id}.${prefix}data_description.${field.name}`,
 | 
				
			||||||
        step.description_placeholders
 | 
					        step.description_placeholders
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return description
 | 
					      return description
 | 
				
			||||||
        ? html`<ha-markdown breaks .content=${description}></ha-markdown>`
 | 
					        ? html`<ha-markdown breaks .content=${description}></ha-markdown>`
 | 
				
			||||||
        : "";
 | 
					        : "";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,13 +49,15 @@ export interface FlowConfig {
 | 
				
			|||||||
  renderShowFormStepFieldLabel(
 | 
					  renderShowFormStepFieldLabel(
 | 
				
			||||||
    hass: HomeAssistant,
 | 
					    hass: HomeAssistant,
 | 
				
			||||||
    step: DataEntryFlowStepForm,
 | 
					    step: DataEntryFlowStepForm,
 | 
				
			||||||
    field: HaFormSchema
 | 
					    field: HaFormSchema,
 | 
				
			||||||
 | 
					    options: { path?: string[]; [key: string]: any }
 | 
				
			||||||
  ): string;
 | 
					  ): string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  renderShowFormStepFieldHelper(
 | 
					  renderShowFormStepFieldHelper(
 | 
				
			||||||
    hass: HomeAssistant,
 | 
					    hass: HomeAssistant,
 | 
				
			||||||
    step: DataEntryFlowStepForm,
 | 
					    step: DataEntryFlowStepForm,
 | 
				
			||||||
    field: HaFormSchema
 | 
					    field: HaFormSchema,
 | 
				
			||||||
 | 
					    options: { path?: string[]; [key: string]: any }
 | 
				
			||||||
  ): TemplateResult | string;
 | 
					  ): TemplateResult | string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  renderShowFormStepFieldError(
 | 
					  renderShowFormStepFieldError(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -93,15 +93,33 @@ export const showOptionsFlowDialog = (
 | 
				
			|||||||
          : "";
 | 
					          : "";
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      renderShowFormStepFieldLabel(hass, step, field) {
 | 
					      renderShowFormStepFieldLabel(hass, step, field, options) {
 | 
				
			||||||
        return hass.localize(
 | 
					        if (field.type === "expandable") {
 | 
				
			||||||
          `component.${configEntry.domain}.options.step.${step.step_id}.data.${field.name}`
 | 
					          return hass.localize(
 | 
				
			||||||
 | 
					            `component.${configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.name`
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return (
 | 
				
			||||||
 | 
					          hass.localize(
 | 
				
			||||||
 | 
					            `component.${configEntry.domain}.options.step.${step.step_id}.${prefix}data.${field.name}`
 | 
				
			||||||
 | 
					          ) || field.name
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      renderShowFormStepFieldHelper(hass, step, field) {
 | 
					      renderShowFormStepFieldHelper(hass, step, field, options) {
 | 
				
			||||||
 | 
					        if (field.type === "expandable") {
 | 
				
			||||||
 | 
					          return hass.localize(
 | 
				
			||||||
 | 
					            `component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.sections.${field.name}.description`
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const prefix = options?.path?.[0] ? `sections.${options.path[0]}.` : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const description = hass.localize(
 | 
					        const description = hass.localize(
 | 
				
			||||||
          `component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.data_description.${field.name}`,
 | 
					          `component.${step.translation_domain || configEntry.domain}.options.step.${step.step_id}.${prefix}data_description.${field.name}`,
 | 
				
			||||||
          step.description_placeholders
 | 
					          step.description_placeholders
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        return description
 | 
					        return description
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -225,11 +225,24 @@ class StepFlowForm extends LitElement {
 | 
				
			|||||||
    this._stepData = ev.detail.value;
 | 
					    this._stepData = ev.detail.value;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _labelCallback = (field: HaFormSchema): string =>
 | 
					  private _labelCallback = (field: HaFormSchema, _data, options): string =>
 | 
				
			||||||
    this.flowConfig.renderShowFormStepFieldLabel(this.hass, this.step, field);
 | 
					    this.flowConfig.renderShowFormStepFieldLabel(
 | 
				
			||||||
 | 
					      this.hass,
 | 
				
			||||||
 | 
					      this.step,
 | 
				
			||||||
 | 
					      field,
 | 
				
			||||||
 | 
					      options
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _helperCallback = (field: HaFormSchema): string | TemplateResult =>
 | 
					  private _helperCallback = (
 | 
				
			||||||
    this.flowConfig.renderShowFormStepFieldHelper(this.hass, this.step, field);
 | 
					    field: HaFormSchema,
 | 
				
			||||||
 | 
					    options
 | 
				
			||||||
 | 
					  ): string | TemplateResult =>
 | 
				
			||||||
 | 
					    this.flowConfig.renderShowFormStepFieldHelper(
 | 
				
			||||||
 | 
					      this.hass,
 | 
				
			||||||
 | 
					      this.step,
 | 
				
			||||||
 | 
					      field,
 | 
				
			||||||
 | 
					      options
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _errorCallback = (error: string) =>
 | 
					  private _errorCallback = (error: string) =>
 | 
				
			||||||
    this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);
 | 
					    this.flowConfig.renderShowFormStepFieldError(this.hass, this.step, error);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,14 @@
 | 
				
			|||||||
import "@material/mwc-button/mwc-button";
 | 
					 | 
				
			||||||
import { mdiAlertOutline } from "@mdi/js";
 | 
					import { mdiAlertOutline } from "@mdi/js";
 | 
				
			||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
 | 
					import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
import { classMap } from "lit/directives/class-map";
 | 
					import { classMap } from "lit/directives/class-map";
 | 
				
			||||||
import { ifDefined } from "lit/directives/if-defined";
 | 
					import { ifDefined } from "lit/directives/if-defined";
 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
import "../../components/ha-dialog";
 | 
					import "../../components/ha-md-dialog";
 | 
				
			||||||
 | 
					import type { HaMdDialog } from "../../components/ha-md-dialog";
 | 
				
			||||||
 | 
					import "../../components/ha-dialog-header";
 | 
				
			||||||
import "../../components/ha-svg-icon";
 | 
					import "../../components/ha-svg-icon";
 | 
				
			||||||
import "../../components/ha-switch";
 | 
					import "../../components/ha-button";
 | 
				
			||||||
import { HaTextField } from "../../components/ha-textfield";
 | 
					import { HaTextField } from "../../components/ha-textfield";
 | 
				
			||||||
import { HomeAssistant } from "../../types";
 | 
					import { HomeAssistant } from "../../types";
 | 
				
			||||||
import { DialogBoxParams } from "./show-dialog-box";
 | 
					import { DialogBoxParams } from "./show-dialog-box";
 | 
				
			||||||
@@ -18,8 +19,12 @@ class DialogBox extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _params?: DialogBoxParams;
 | 
					  @state() private _params?: DialogBoxParams;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @state() private _closeState?: "canceled" | "confirmed";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("ha-textfield") private _textField?: HaTextField;
 | 
					  @query("ha-textfield") private _textField?: HaTextField;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @query("ha-md-dialog") private _dialog?: HaMdDialog;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async showDialog(params: DialogBoxParams): Promise<void> {
 | 
					  public async showDialog(params: DialogBoxParams): Promise<void> {
 | 
				
			||||||
    this._params = params;
 | 
					    this._params = params;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -42,33 +47,33 @@ class DialogBox extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const confirmPrompt = this._params.confirmation || this._params.prompt;
 | 
					    const confirmPrompt = this._params.confirmation || this._params.prompt;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const dialogTitle =
 | 
				
			||||||
 | 
					      this._params.title ||
 | 
				
			||||||
 | 
					      (this._params.confirmation &&
 | 
				
			||||||
 | 
					        this.hass.localize("ui.dialogs.generic.default_confirmation_title"));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-dialog
 | 
					      <ha-md-dialog
 | 
				
			||||||
        open
 | 
					        open
 | 
				
			||||||
        ?scrimClickAction=${confirmPrompt}
 | 
					        .disableCancelAction=${confirmPrompt || false}
 | 
				
			||||||
        ?escapeKeyAction=${confirmPrompt}
 | 
					 | 
				
			||||||
        @closed=${this._dialogClosed}
 | 
					        @closed=${this._dialogClosed}
 | 
				
			||||||
        defaultAction="ignore"
 | 
					        type="alert"
 | 
				
			||||||
        .heading=${html`${this._params.warning
 | 
					        aria-labelledby="dialog-box-title"
 | 
				
			||||||
          ? html`<ha-svg-icon
 | 
					        aria-describedby="dialog-box-description"
 | 
				
			||||||
              .path=${mdiAlertOutline}
 | 
					 | 
				
			||||||
              style="color: var(--warning-color)"
 | 
					 | 
				
			||||||
            ></ha-svg-icon> `
 | 
					 | 
				
			||||||
          : ""}${this._params.title
 | 
					 | 
				
			||||||
          ? this._params.title
 | 
					 | 
				
			||||||
          : this._params.confirmation &&
 | 
					 | 
				
			||||||
            this.hass.localize(
 | 
					 | 
				
			||||||
              "ui.dialogs.generic.default_confirmation_title"
 | 
					 | 
				
			||||||
            )}`}
 | 
					 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <div>
 | 
					        <div slot="headline">
 | 
				
			||||||
          ${this._params.text
 | 
					          <span .title=${dialogTitle} id="dialog-box-title">
 | 
				
			||||||
            ? html`
 | 
					            ${this._params.warning
 | 
				
			||||||
                <p class=${this._params.prompt ? "no-bottom-padding" : ""}>
 | 
					              ? html`<ha-svg-icon
 | 
				
			||||||
                  ${this._params.text}
 | 
					                  .path=${mdiAlertOutline}
 | 
				
			||||||
                </p>
 | 
					                  style="color: var(--warning-color)"
 | 
				
			||||||
              `
 | 
					                ></ha-svg-icon> `
 | 
				
			||||||
            : ""}
 | 
					              : nothing}
 | 
				
			||||||
 | 
					            ${dialogTitle}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div slot="content" id="dialog-box-description">
 | 
				
			||||||
 | 
					          ${this._params.text ? html` <p>${this._params.text}</p> ` : ""}
 | 
				
			||||||
          ${this._params.prompt
 | 
					          ${this._params.prompt
 | 
				
			||||||
            ? html`
 | 
					            ? html`
 | 
				
			||||||
                <ha-textfield
 | 
					                <ha-textfield
 | 
				
			||||||
@@ -87,58 +92,64 @@ class DialogBox extends LitElement {
 | 
				
			|||||||
              `
 | 
					              `
 | 
				
			||||||
            : ""}
 | 
					            : ""}
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        ${confirmPrompt &&
 | 
					        <div slot="actions">
 | 
				
			||||||
        html`
 | 
					          ${confirmPrompt &&
 | 
				
			||||||
          <mwc-button
 | 
					          html`
 | 
				
			||||||
            @click=${this._dismiss}
 | 
					            <ha-button
 | 
				
			||||||
            slot="secondaryAction"
 | 
					              @click=${this._dismiss}
 | 
				
			||||||
 | 
					              ?dialogInitialFocus=${!this._params.prompt &&
 | 
				
			||||||
 | 
					              this._params.destructive}
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              ${this._params.dismissText
 | 
				
			||||||
 | 
					                ? this._params.dismissText
 | 
				
			||||||
 | 
					                : this.hass.localize("ui.dialogs.generic.cancel")}
 | 
				
			||||||
 | 
					            </ha-button>
 | 
				
			||||||
 | 
					          `}
 | 
				
			||||||
 | 
					          <ha-button
 | 
				
			||||||
 | 
					            @click=${this._confirm}
 | 
				
			||||||
            ?dialogInitialFocus=${!this._params.prompt &&
 | 
					            ?dialogInitialFocus=${!this._params.prompt &&
 | 
				
			||||||
            this._params.destructive}
 | 
					            !this._params.destructive}
 | 
				
			||||||
 | 
					            class=${classMap({
 | 
				
			||||||
 | 
					              destructive: this._params.destructive || false,
 | 
				
			||||||
 | 
					            })}
 | 
				
			||||||
          >
 | 
					          >
 | 
				
			||||||
            ${this._params.dismissText
 | 
					            ${this._params.confirmText
 | 
				
			||||||
              ? this._params.dismissText
 | 
					              ? this._params.confirmText
 | 
				
			||||||
              : this.hass.localize("ui.dialogs.generic.cancel")}
 | 
					              : this.hass.localize("ui.dialogs.generic.ok")}
 | 
				
			||||||
          </mwc-button>
 | 
					          </ha-button>
 | 
				
			||||||
        `}
 | 
					        </div>
 | 
				
			||||||
        <mwc-button
 | 
					      </ha-md-dialog>
 | 
				
			||||||
          @click=${this._confirm}
 | 
					 | 
				
			||||||
          ?dialogInitialFocus=${!this._params.prompt &&
 | 
					 | 
				
			||||||
          !this._params.destructive}
 | 
					 | 
				
			||||||
          slot="primaryAction"
 | 
					 | 
				
			||||||
          class=${classMap({
 | 
					 | 
				
			||||||
            destructive: this._params.destructive || false,
 | 
					 | 
				
			||||||
          })}
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
          ${this._params.confirmText
 | 
					 | 
				
			||||||
            ? this._params.confirmText
 | 
					 | 
				
			||||||
            : this.hass.localize("ui.dialogs.generic.ok")}
 | 
					 | 
				
			||||||
        </mwc-button>
 | 
					 | 
				
			||||||
      </ha-dialog>
 | 
					 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _dismiss(): void {
 | 
					  private _cancel(): void {
 | 
				
			||||||
    if (this._params?.cancel) {
 | 
					    if (this._params?.cancel) {
 | 
				
			||||||
      this._params.cancel();
 | 
					      this._params.cancel();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this._close();
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _dismiss(): void {
 | 
				
			||||||
 | 
					    this._cancel();
 | 
				
			||||||
 | 
					    this._closeState = "canceled";
 | 
				
			||||||
 | 
					    this._closeDialog();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _confirm(): void {
 | 
					  private _confirm(): void {
 | 
				
			||||||
    if (this._params!.confirm) {
 | 
					    if (this._params!.confirm) {
 | 
				
			||||||
      this._params!.confirm(this._textField?.value);
 | 
					      this._params!.confirm(this._textField?.value);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this._close();
 | 
					    this._closeState = "confirmed";
 | 
				
			||||||
 | 
					    this._closeDialog();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _dialogClosed(ev) {
 | 
					  private _closeDialog() {
 | 
				
			||||||
    if (ev.detail.action === "ignore") {
 | 
					    this._dialog?.close();
 | 
				
			||||||
      return;
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _dialogClosed() {
 | 
				
			||||||
 | 
					    if (!this._closeState) {
 | 
				
			||||||
 | 
					      this._cancel();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this._dismiss();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _close(): void {
 | 
					 | 
				
			||||||
    if (!this._params) {
 | 
					    if (!this._params) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -168,10 +179,6 @@ class DialogBox extends LitElement {
 | 
				
			|||||||
      .destructive {
 | 
					      .destructive {
 | 
				
			||||||
        --mdc-theme-primary: var(--error-color);
 | 
					        --mdc-theme-primary: var(--error-color);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      ha-dialog {
 | 
					 | 
				
			||||||
        /* Place above other dialogs */
 | 
					 | 
				
			||||||
        --dialog-z-index: 104;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      @media all and (min-width: 600px) {
 | 
					      @media all and (min-width: 600px) {
 | 
				
			||||||
        ha-dialog {
 | 
					        ha-dialog {
 | 
				
			||||||
          --mdc-dialog-min-width: 400px;
 | 
					          --mdc-dialog-min-width: 400px;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,15 +1,18 @@
 | 
				
			|||||||
import { mdiClose } from "@mdi/js";
 | 
					import { mdiClose } from "@mdi/js";
 | 
				
			||||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
 | 
					import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
 | 
				
			||||||
import { customElement, property, state } from "lit/decorators";
 | 
					import { customElement, property, state, query } from "lit/decorators";
 | 
				
			||||||
import { classMap } from "lit/directives/class-map";
 | 
					import { classMap } from "lit/directives/class-map";
 | 
				
			||||||
import { fireEvent } from "../../../../common/dom/fire_event";
 | 
					import { fireEvent } from "../../../../common/dom/fire_event";
 | 
				
			||||||
import "../../../../components/ha-button";
 | 
					import "../../../../components/ha-button";
 | 
				
			||||||
import "../../../../components/ha-dialog";
 | 
					import {
 | 
				
			||||||
 | 
					  getMobileOpenFromBottomAnimation,
 | 
				
			||||||
 | 
					  getMobileCloseToBottomAnimation,
 | 
				
			||||||
 | 
					} from "../../../../components/ha-md-dialog";
 | 
				
			||||||
 | 
					import type { HaMdDialog } from "../../../../components/ha-md-dialog";
 | 
				
			||||||
import "../../../../components/ha-dialog-header";
 | 
					import "../../../../components/ha-dialog-header";
 | 
				
			||||||
import "../../../../components/ha-icon-button-toggle";
 | 
					import "../../../../components/ha-icon-button-toggle";
 | 
				
			||||||
import type { EntityRegistryEntry } from "../../../../data/entity_registry";
 | 
					import type { EntityRegistryEntry } from "../../../../data/entity_registry";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  formatTempColor,
 | 
					 | 
				
			||||||
  LightColor,
 | 
					  LightColor,
 | 
				
			||||||
  LightColorMode,
 | 
					  LightColorMode,
 | 
				
			||||||
  LightEntity,
 | 
					  LightEntity,
 | 
				
			||||||
@@ -38,15 +41,7 @@ class DialogLightColorFavorite extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _modes: LightPickerMode[] = [];
 | 
					  @state() private _modes: LightPickerMode[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _currentValue?: string;
 | 
					  @query("ha-md-dialog") private _dialog?: HaMdDialog;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _colorHovered(ev: CustomEvent<HASSDomEvents["color-hovered"]>) {
 | 
					 | 
				
			||||||
    if (ev.detail && "color_temp_kelvin" in ev.detail) {
 | 
					 | 
				
			||||||
      this._currentValue = formatTempColor(ev.detail.color_temp_kelvin);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this._currentValue = undefined;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async showDialog(
 | 
					  public async showDialog(
 | 
				
			||||||
    dialogParams: LightColorFavoriteDialogParams
 | 
					    dialogParams: LightColorFavoriteDialogParams
 | 
				
			||||||
@@ -58,10 +53,7 @@ class DialogLightColorFavorite extends LitElement {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public closeDialog(): void {
 | 
					  public closeDialog(): void {
 | 
				
			||||||
    this._dialogParams = undefined;
 | 
					    this._dialog?.close();
 | 
				
			||||||
    this._entry = undefined;
 | 
					 | 
				
			||||||
    this._color = undefined;
 | 
					 | 
				
			||||||
    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _updateModes() {
 | 
					  private _updateModes() {
 | 
				
			||||||
@@ -130,9 +122,20 @@ class DialogLightColorFavorite extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private async _cancel() {
 | 
					  private async _cancel() {
 | 
				
			||||||
    this._dialogParams?.cancel?.();
 | 
					    this._dialogParams?.cancel?.();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _cancelDialog() {
 | 
				
			||||||
 | 
					    this._cancel();
 | 
				
			||||||
    this.closeDialog();
 | 
					    this.closeDialog();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _dialogClosed(): void {
 | 
				
			||||||
 | 
					    this._dialogParams = undefined;
 | 
				
			||||||
 | 
					    this._entry = undefined;
 | 
				
			||||||
 | 
					    this._color = undefined;
 | 
				
			||||||
 | 
					    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async _save() {
 | 
					  private async _save() {
 | 
				
			||||||
    if (!this._color) {
 | 
					    if (!this._color) {
 | 
				
			||||||
      this._cancel();
 | 
					      this._cancel();
 | 
				
			||||||
@@ -156,82 +159,83 @@ class DialogLightColorFavorite extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-dialog
 | 
					      <ha-md-dialog
 | 
				
			||||||
        open
 | 
					        open
 | 
				
			||||||
        @closed=${this._cancel}
 | 
					        @cancel=${this._cancel}
 | 
				
			||||||
        .heading=${this._dialogParams?.title ?? ""}
 | 
					        @closed=${this._dialogClosed}
 | 
				
			||||||
        flexContent
 | 
					        aria-labelledby="dialog-light-color-favorite-title"
 | 
				
			||||||
 | 
					        .getOpenAnimation=${getMobileOpenFromBottomAnimation}
 | 
				
			||||||
 | 
					        .getCloseAnimation=${getMobileCloseToBottomAnimation}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <ha-dialog-header slot="heading">
 | 
					        <ha-dialog-header slot="headline">
 | 
				
			||||||
          <ha-icon-button
 | 
					          <ha-icon-button
 | 
				
			||||||
            slot="navigationIcon"
 | 
					            slot="navigationIcon"
 | 
				
			||||||
            dialogAction="cancel"
 | 
					            @click=${this.closeDialog}
 | 
				
			||||||
            .label=${this.hass.localize("ui.common.close")}
 | 
					            .label=${this.hass.localize("ui.common.close")}
 | 
				
			||||||
            .path=${mdiClose}
 | 
					            .path=${mdiClose}
 | 
				
			||||||
          ></ha-icon-button>
 | 
					          ></ha-icon-button>
 | 
				
			||||||
          <span slot="title">${this._dialogParams?.title}</span>
 | 
					          <span slot="title" id="dialog-light-color-favorite-title"
 | 
				
			||||||
 | 
					            >${this._dialogParams?.title}</span
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
        </ha-dialog-header>
 | 
					        </ha-dialog-header>
 | 
				
			||||||
        <div class="header">
 | 
					        <div slot="content">
 | 
				
			||||||
          <span class="value">${this._currentValue}</span>
 | 
					          <div class="header">
 | 
				
			||||||
          ${this._modes.length > 1
 | 
					            ${this._modes.length > 1
 | 
				
			||||||
            ? html`
 | 
					              ? html`
 | 
				
			||||||
                <div class="modes">
 | 
					                  <div class="modes">
 | 
				
			||||||
                  ${this._modes.map(
 | 
					                    ${this._modes.map(
 | 
				
			||||||
                    (value) => html`
 | 
					                      (value) => html`
 | 
				
			||||||
                      <ha-icon-button-toggle
 | 
					                        <ha-icon-button-toggle
 | 
				
			||||||
                        border-only
 | 
					                          border-only
 | 
				
			||||||
                        .selected=${value === this._mode}
 | 
					                          .selected=${value === this._mode}
 | 
				
			||||||
                        .label=${this.hass.localize(
 | 
					                          .label=${this.hass.localize(
 | 
				
			||||||
                          `ui.dialogs.more_info_control.light.color_picker.mode.${value}`
 | 
					                            `ui.dialogs.more_info_control.light.color_picker.mode.${value}`
 | 
				
			||||||
                        )}
 | 
					                          )}
 | 
				
			||||||
                        .mode=${value}
 | 
					                          .mode=${value}
 | 
				
			||||||
                        @click=${this._modeChanged}
 | 
					                          @click=${this._modeChanged}
 | 
				
			||||||
                      >
 | 
					                        >
 | 
				
			||||||
                        <span
 | 
					                          <span
 | 
				
			||||||
                          class="wheel ${classMap({ [value]: true })}"
 | 
					                            class="wheel ${classMap({ [value]: true })}"
 | 
				
			||||||
                        ></span>
 | 
					                          ></span>
 | 
				
			||||||
                      </ha-icon-button-toggle>
 | 
					                        </ha-icon-button-toggle>
 | 
				
			||||||
                    `
 | 
					                      `
 | 
				
			||||||
                  )}
 | 
					                    )}
 | 
				
			||||||
                </div>
 | 
					                  </div>
 | 
				
			||||||
              `
 | 
					                `
 | 
				
			||||||
            : nothing}
 | 
					              : nothing}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
 | 
					          <div class="content">
 | 
				
			||||||
 | 
					            ${this._mode === "color_temp"
 | 
				
			||||||
 | 
					              ? html`
 | 
				
			||||||
 | 
					                  <light-color-temp-picker
 | 
				
			||||||
 | 
					                    .hass=${this.hass}
 | 
				
			||||||
 | 
					                    .stateObj=${this.stateObj}
 | 
				
			||||||
 | 
					                    @color-changed=${this._colorChanged}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                  </light-color-temp-picker>
 | 
				
			||||||
 | 
					                `
 | 
				
			||||||
 | 
					              : nothing}
 | 
				
			||||||
 | 
					            ${this._mode === "color"
 | 
				
			||||||
 | 
					              ? html`
 | 
				
			||||||
 | 
					                  <light-color-rgb-picker
 | 
				
			||||||
 | 
					                    .hass=${this.hass}
 | 
				
			||||||
 | 
					                    .stateObj=${this.stateObj}
 | 
				
			||||||
 | 
					                    @color-changed=${this._colorChanged}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                  </light-color-rgb-picker>
 | 
				
			||||||
 | 
					                `
 | 
				
			||||||
 | 
					              : nothing}
 | 
				
			||||||
 | 
					          </div>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
 | 
					        <div slot="actions">
 | 
				
			||||||
        <div class="content">
 | 
					          <ha-button @click=${this._cancelDialog}>
 | 
				
			||||||
          ${this._mode === "color_temp"
 | 
					            ${this.hass.localize("ui.common.cancel")}
 | 
				
			||||||
            ? html`
 | 
					          </ha-button>
 | 
				
			||||||
                <light-color-temp-picker
 | 
					          <ha-button @click=${this._save} .disabled=${!this._color}
 | 
				
			||||||
                  .hass=${this.hass}
 | 
					            >${this.hass.localize("ui.common.save")}</ha-button
 | 
				
			||||||
                  .stateObj=${this.stateObj}
 | 
					          >
 | 
				
			||||||
                  @color-changed=${this._colorChanged}
 | 
					 | 
				
			||||||
                  @color-hovered=${this._colorHovered}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                </light-color-temp-picker>
 | 
					 | 
				
			||||||
              `
 | 
					 | 
				
			||||||
            : nothing}
 | 
					 | 
				
			||||||
          ${this._mode === "color"
 | 
					 | 
				
			||||||
            ? html`
 | 
					 | 
				
			||||||
                <light-color-rgb-picker
 | 
					 | 
				
			||||||
                  .hass=${this.hass}
 | 
					 | 
				
			||||||
                  .stateObj=${this.stateObj}
 | 
					 | 
				
			||||||
                  @color-changed=${this._colorChanged}
 | 
					 | 
				
			||||||
                  @color-hovered=${this._colorHovered}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                </light-color-rgb-picker>
 | 
					 | 
				
			||||||
              `
 | 
					 | 
				
			||||||
            : nothing}
 | 
					 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <ha-button slot="secondaryAction" dialogAction="cancel">
 | 
					      </ha-md-dialog>
 | 
				
			||||||
          ${this.hass.localize("ui.common.cancel")}
 | 
					 | 
				
			||||||
        </ha-button>
 | 
					 | 
				
			||||||
        <ha-button
 | 
					 | 
				
			||||||
          slot="primaryAction"
 | 
					 | 
				
			||||||
          @click=${this._save}
 | 
					 | 
				
			||||||
          .disabled=${!this._color}
 | 
					 | 
				
			||||||
          >${this.hass.localize("ui.common.save")}</ha-button
 | 
					 | 
				
			||||||
        >
 | 
					 | 
				
			||||||
      </ha-dialog>
 | 
					 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -239,19 +243,23 @@ class DialogLightColorFavorite extends LitElement {
 | 
				
			|||||||
    return [
 | 
					    return [
 | 
				
			||||||
      haStyleDialog,
 | 
					      haStyleDialog,
 | 
				
			||||||
      css`
 | 
					      css`
 | 
				
			||||||
        ha-dialog {
 | 
					        ha-md-dialog {
 | 
				
			||||||
          --dialog-content-padding: 0;
 | 
					          min-width: 420px; /* prevent width jumps when switching modes */
 | 
				
			||||||
 | 
					          max-height: min(
 | 
				
			||||||
 | 
					            600px,
 | 
				
			||||||
 | 
					            100% - 48px
 | 
				
			||||||
 | 
					          ); /* prevent scrolling on desktop */
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
					        @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
				
			||||||
          ha-dialog {
 | 
					          ha-md-dialog {
 | 
				
			||||||
            --dialog-surface-margin-top: 100px;
 | 
					            min-width: 100%;
 | 
				
			||||||
            --mdc-dialog-min-height: auto;
 | 
					            min-height: auto;
 | 
				
			||||||
            --mdc-dialog-max-height: calc(100% - 100px);
 | 
					            max-height: calc(100% - 100px);
 | 
				
			||||||
            --ha-dialog-border-radius: var(
 | 
					            margin-bottom: 0;
 | 
				
			||||||
              --ha-dialog-bottom-sheet-border-radius,
 | 
					
 | 
				
			||||||
              28px 28px 0 0
 | 
					            --md-dialog-container-shape-start-start: 28px;
 | 
				
			||||||
            );
 | 
					            --md-dialog-container-shape-start-end: 28px;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -287,21 +295,6 @@ class DialogLightColorFavorite extends LitElement {
 | 
				
			|||||||
            rgb(255, 160, 0) 100%
 | 
					            rgb(255, 160, 0) 100%
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .value {
 | 
					 | 
				
			||||||
          pointer-events: none;
 | 
					 | 
				
			||||||
          position: absolute;
 | 
					 | 
				
			||||||
          top: 0;
 | 
					 | 
				
			||||||
          left: 0;
 | 
					 | 
				
			||||||
          right: 0;
 | 
					 | 
				
			||||||
          margin: auto;
 | 
					 | 
				
			||||||
          font-style: normal;
 | 
					 | 
				
			||||||
          font-weight: 500;
 | 
					 | 
				
			||||||
          font-size: 16px;
 | 
					 | 
				
			||||||
          height: 48px;
 | 
					 | 
				
			||||||
          line-height: 48px;
 | 
					 | 
				
			||||||
          letter-spacing: 0.1px;
 | 
					 | 
				
			||||||
          text-align: center;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,7 @@ import { isUnavailableState } from "../../../data/entity";
 | 
				
			|||||||
import { computeObjectId } from "../../../common/entity/compute_object_id";
 | 
					import { computeObjectId } from "../../../common/entity/compute_object_id";
 | 
				
			||||||
import { listenMediaQuery } from "../../../common/dom/media_query";
 | 
					import { listenMediaQuery } from "../../../common/dom/media_query";
 | 
				
			||||||
import "../components/ha-more-info-state-header";
 | 
					import "../components/ha-more-info-state-header";
 | 
				
			||||||
 | 
					import { ExtEntityRegistryEntry } from "../../../data/entity_registry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("more-info-script")
 | 
					@customElement("more-info-script")
 | 
				
			||||||
class MoreInfoScript extends LitElement {
 | 
					class MoreInfoScript extends LitElement {
 | 
				
			||||||
@@ -28,6 +29,8 @@ class MoreInfoScript extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public stateObj?: ScriptEntity;
 | 
					  @property({ attribute: false }) public stateObj?: ScriptEntity;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ attribute: false }) public entry?: ExtEntityRegistryEntry;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _scriptData: Record<string, any> = {};
 | 
					  @state() private _scriptData: Record<string, any> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private narrow = false;
 | 
					  @state() private narrow = false;
 | 
				
			||||||
@@ -59,8 +62,9 @@ class MoreInfoScript extends LitElement {
 | 
				
			|||||||
    const stateObj = this.stateObj;
 | 
					    const stateObj = this.stateObj;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const fields =
 | 
					    const fields =
 | 
				
			||||||
      this.hass.services.script[computeObjectId(this.stateObj.entity_id)]
 | 
					      this.hass.services.script[
 | 
				
			||||||
        ?.fields;
 | 
					        this.entry?.unique_id || computeObjectId(this.stateObj.entity_id)
 | 
				
			||||||
 | 
					      ]?.fields;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const hasFields = fields && Object.keys(fields).length > 0;
 | 
					    const hasFields = fields && Object.keys(fields).length > 0;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -138,17 +142,30 @@ class MoreInfoScript extends LitElement {
 | 
				
			|||||||
  protected override willUpdate(changedProperties: PropertyValues): void {
 | 
					  protected override willUpdate(changedProperties: PropertyValues): void {
 | 
				
			||||||
    super.willUpdate(changedProperties);
 | 
					    super.willUpdate(changedProperties);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!changedProperties.has("stateObj")) {
 | 
					    if (changedProperties.has("stateObj")) {
 | 
				
			||||||
      return;
 | 
					      const oldState = changedProperties.get("stateObj") as
 | 
				
			||||||
 | 
					        | HassEntity
 | 
				
			||||||
 | 
					        | undefined;
 | 
				
			||||||
 | 
					      const newState = this.stateObj;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        newState &&
 | 
				
			||||||
 | 
					        (!oldState || oldState.entity_id !== newState.entity_id)
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        this._scriptData = {
 | 
				
			||||||
 | 
					          action:
 | 
				
			||||||
 | 
					            this.entry?.entity_id === newState.entity_id
 | 
				
			||||||
 | 
					              ? `script.${this.entry.unique_id}`
 | 
				
			||||||
 | 
					              : newState.entity_id,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const oldState = changedProperties.get("stateObj") as
 | 
					    if (this.entry?.unique_id && changedProperties.has("entry")) {
 | 
				
			||||||
      | HassEntity
 | 
					      const action = `script.${this.entry?.unique_id}`;
 | 
				
			||||||
      | undefined;
 | 
					      if (this._scriptData?.action !== action) {
 | 
				
			||||||
    const newState = this.stateObj;
 | 
					        this._scriptData = { ...this._scriptData, action };
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
    if (newState && (!oldState || oldState.entity_id !== newState.entity_id)) {
 | 
					 | 
				
			||||||
      this._scriptData = { action: newState.entity_id, data: {} };
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -161,7 +178,7 @@ class MoreInfoScript extends LitElement {
 | 
				
			|||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    this.hass.callService(
 | 
					    this.hass.callService(
 | 
				
			||||||
      "script",
 | 
					      "script",
 | 
				
			||||||
      computeObjectId(this.stateObj!.entity_id),
 | 
					      this.entry?.unique_id || computeObjectId(this.stateObj!.entity_id),
 | 
				
			||||||
      this._scriptData.data
 | 
					      this._scriptData.data
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										629
									
								
								src/dialogs/voice-command-dialog/assist-chat.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										629
									
								
								src/dialogs/voice-command-dialog/assist-chat.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,629 @@
 | 
				
			|||||||
 | 
					import { mdiAlertCircle, mdiMicrophone, mdiSend } from "@mdi/js";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  css,
 | 
				
			||||||
 | 
					  CSSResultGroup,
 | 
				
			||||||
 | 
					  html,
 | 
				
			||||||
 | 
					  LitElement,
 | 
				
			||||||
 | 
					  nothing,
 | 
				
			||||||
 | 
					  PropertyValues,
 | 
				
			||||||
 | 
					  TemplateResult,
 | 
				
			||||||
 | 
					} from "lit";
 | 
				
			||||||
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
 | 
					import "../../components/ha-icon-button";
 | 
				
			||||||
 | 
					import "../../components/ha-textfield";
 | 
				
			||||||
 | 
					import type { HaTextField } from "../../components/ha-textfield";
 | 
				
			||||||
 | 
					import {
 | 
				
			||||||
 | 
					  AssistPipeline,
 | 
				
			||||||
 | 
					  getAssistPipeline,
 | 
				
			||||||
 | 
					  runAssistPipeline,
 | 
				
			||||||
 | 
					} from "../../data/assist_pipeline";
 | 
				
			||||||
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
 | 
					import { AudioRecorder } from "../../util/audio-recorder";
 | 
				
			||||||
 | 
					import { documentationUrl } from "../../util/documentation-url";
 | 
				
			||||||
 | 
					import { showAlertDialog } from "../generic/show-dialog-box";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface Message {
 | 
				
			||||||
 | 
					  who: string;
 | 
				
			||||||
 | 
					  text?: string | TemplateResult;
 | 
				
			||||||
 | 
					  error?: boolean;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("assist-chat")
 | 
				
			||||||
 | 
					export class HaAssistChat extends LitElement {
 | 
				
			||||||
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ attribute: "pipeline-id" }) public pipelineId!: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @state() private _conversation?: Message[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @state() private _pipeline?: AssistPipeline;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @state() private _showSendButton = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @query("#scroll-container") private _scrollContainer!: HTMLDivElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @query("#message-input") private _messageInput!: HaTextField;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _conversationId: string | null = null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _audioRecorder?: AudioRecorder;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _audioBuffer?: Int16Array[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _audio?: HTMLAudioElement;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _stt_binary_handler_id?: number | null;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected render() {
 | 
				
			||||||
 | 
					    const supportsMicrophone = AudioRecorder.isSupported;
 | 
				
			||||||
 | 
					    const supportsSTT = this._pipeline?.stt_engine;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <div class="messages">
 | 
				
			||||||
 | 
					        <div class="messages-container" id="scroll-container">
 | 
				
			||||||
 | 
					          ${this._conversation!.map(
 | 
				
			||||||
 | 
					            // New lines matter for messages
 | 
				
			||||||
 | 
					            // prettier-ignore
 | 
				
			||||||
 | 
					            (message) => html`
 | 
				
			||||||
 | 
					                <div class=${this._computeMessageClasses(message)}>${message.text}</div>
 | 
				
			||||||
 | 
					              `
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      <div class="input">
 | 
				
			||||||
 | 
					        <ha-textfield
 | 
				
			||||||
 | 
					          id="message-input"
 | 
				
			||||||
 | 
					          @keyup=${this._handleKeyUp}
 | 
				
			||||||
 | 
					          @input=${this._handleInput}
 | 
				
			||||||
 | 
					          .label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
 | 
				
			||||||
 | 
					          dialogInitialFocus
 | 
				
			||||||
 | 
					          iconTrailing
 | 
				
			||||||
 | 
					        >
 | 
				
			||||||
 | 
					          <span slot="trailingIcon">
 | 
				
			||||||
 | 
					            ${this._showSendButton || !supportsSTT
 | 
				
			||||||
 | 
					              ? html`
 | 
				
			||||||
 | 
					                  <ha-icon-button
 | 
				
			||||||
 | 
					                    class="listening-icon"
 | 
				
			||||||
 | 
					                    .path=${mdiSend}
 | 
				
			||||||
 | 
					                    @click=${this._handleSendMessage}
 | 
				
			||||||
 | 
					                    .label=${this.hass.localize(
 | 
				
			||||||
 | 
					                      "ui.dialogs.voice_command.send_text"
 | 
				
			||||||
 | 
					                    )}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                  </ha-icon-button>
 | 
				
			||||||
 | 
					                `
 | 
				
			||||||
 | 
					              : html`
 | 
				
			||||||
 | 
					                  ${this._audioRecorder?.active
 | 
				
			||||||
 | 
					                    ? html`
 | 
				
			||||||
 | 
					                        <div class="bouncer">
 | 
				
			||||||
 | 
					                          <div class="double-bounce1"></div>
 | 
				
			||||||
 | 
					                          <div class="double-bounce2"></div>
 | 
				
			||||||
 | 
					                        </div>
 | 
				
			||||||
 | 
					                      `
 | 
				
			||||||
 | 
					                    : nothing}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					                  <div class="listening-icon">
 | 
				
			||||||
 | 
					                    <ha-icon-button
 | 
				
			||||||
 | 
					                      .path=${mdiMicrophone}
 | 
				
			||||||
 | 
					                      @click=${this._handleListeningButton}
 | 
				
			||||||
 | 
					                      .label=${this.hass.localize(
 | 
				
			||||||
 | 
					                        "ui.dialogs.voice_command.start_listening"
 | 
				
			||||||
 | 
					                      )}
 | 
				
			||||||
 | 
					                    >
 | 
				
			||||||
 | 
					                    </ha-icon-button>
 | 
				
			||||||
 | 
					                    ${!supportsMicrophone
 | 
				
			||||||
 | 
					                      ? html`
 | 
				
			||||||
 | 
					                          <ha-svg-icon
 | 
				
			||||||
 | 
					                            .path=${mdiAlertCircle}
 | 
				
			||||||
 | 
					                            class="unsupported"
 | 
				
			||||||
 | 
					                          ></ha-svg-icon>
 | 
				
			||||||
 | 
					                        `
 | 
				
			||||||
 | 
					                      : null}
 | 
				
			||||||
 | 
					                  </div>
 | 
				
			||||||
 | 
					                `}
 | 
				
			||||||
 | 
					          </span>
 | 
				
			||||||
 | 
					        </ha-textfield>
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected willUpdate(changedProperties: PropertyValues): void {
 | 
				
			||||||
 | 
					    if (changedProperties.has("pipelineId")) {
 | 
				
			||||||
 | 
					      this._getPipeline();
 | 
				
			||||||
 | 
					      this._conversation = [
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          who: "hass",
 | 
				
			||||||
 | 
					          text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					      ];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async _getPipeline() {
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      this._pipeline = await getAssistPipeline(this.hass, this.pipelineId);
 | 
				
			||||||
 | 
					    } catch (e: any) {
 | 
				
			||||||
 | 
					      // Pipeline doesn't exist, we won't be able to check
 | 
				
			||||||
 | 
					      // if it supports STT. We gracefully handle this.
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected updated(changedProps: PropertyValues) {
 | 
				
			||||||
 | 
					    super.updated(changedProps);
 | 
				
			||||||
 | 
					    if (changedProps.has("_conversation") || changedProps.has("results")) {
 | 
				
			||||||
 | 
					      this._scrollMessagesBottom();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _addMessage(message: Message) {
 | 
				
			||||||
 | 
					    this._conversation = [...this._conversation!, message];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _handleKeyUp(ev: KeyboardEvent) {
 | 
				
			||||||
 | 
					    const input = ev.target as HaTextField;
 | 
				
			||||||
 | 
					    if (ev.key === "Enter" && input.value) {
 | 
				
			||||||
 | 
					      this._processText(input.value);
 | 
				
			||||||
 | 
					      input.value = "";
 | 
				
			||||||
 | 
					      this._showSendButton = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _handleInput(ev: InputEvent) {
 | 
				
			||||||
 | 
					    const value = (ev.target as HaTextField).value;
 | 
				
			||||||
 | 
					    if (value && !this._showSendButton) {
 | 
				
			||||||
 | 
					      this._showSendButton = true;
 | 
				
			||||||
 | 
					    } else if (!value && this._showSendButton) {
 | 
				
			||||||
 | 
					      this._showSendButton = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _handleSendMessage() {
 | 
				
			||||||
 | 
					    if (this._messageInput.value) {
 | 
				
			||||||
 | 
					      this._processText(this._messageInput.value.trim());
 | 
				
			||||||
 | 
					      this._messageInput.value = "";
 | 
				
			||||||
 | 
					      this._showSendButton = false;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async _processText(text: string) {
 | 
				
			||||||
 | 
					    this._audio?.pause();
 | 
				
			||||||
 | 
					    this._addMessage({ who: "user", text });
 | 
				
			||||||
 | 
					    const message: Message = {
 | 
				
			||||||
 | 
					      who: "hass",
 | 
				
			||||||
 | 
					      text: "…",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    // To make sure the answer is placed at the right user text, we add it before we process it
 | 
				
			||||||
 | 
					    this._addMessage(message);
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const unsub = await runAssistPipeline(
 | 
				
			||||||
 | 
					        this.hass,
 | 
				
			||||||
 | 
					        (event) => {
 | 
				
			||||||
 | 
					          if (event.type === "intent-end") {
 | 
				
			||||||
 | 
					            this._conversationId = event.data.intent_output.conversation_id;
 | 
				
			||||||
 | 
					            const plain = event.data.intent_output.response.speech?.plain;
 | 
				
			||||||
 | 
					            if (plain) {
 | 
				
			||||||
 | 
					              message.text = plain.speech;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this.requestUpdate("_conversation");
 | 
				
			||||||
 | 
					            unsub();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          if (event.type === "error") {
 | 
				
			||||||
 | 
					            message.text = event.data.message;
 | 
				
			||||||
 | 
					            message.error = true;
 | 
				
			||||||
 | 
					            this.requestUpdate("_conversation");
 | 
				
			||||||
 | 
					            unsub();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          start_stage: "intent",
 | 
				
			||||||
 | 
					          input: { text },
 | 
				
			||||||
 | 
					          end_stage: "intent",
 | 
				
			||||||
 | 
					          pipeline: this.pipelineId,
 | 
				
			||||||
 | 
					          conversation_id: this._conversationId,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch {
 | 
				
			||||||
 | 
					      message.text = this.hass.localize("ui.dialogs.voice_command.error");
 | 
				
			||||||
 | 
					      message.error = true;
 | 
				
			||||||
 | 
					      this.requestUpdate("_conversation");
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _handleListeningButton(ev) {
 | 
				
			||||||
 | 
					    ev.stopPropagation();
 | 
				
			||||||
 | 
					    ev.preventDefault();
 | 
				
			||||||
 | 
					    this.toggleListening();
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public toggleListening() {
 | 
				
			||||||
 | 
					    const supportsMicrophone = AudioRecorder.isSupported;
 | 
				
			||||||
 | 
					    if (!supportsMicrophone) {
 | 
				
			||||||
 | 
					      this._showNotSupportedMessage();
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (!this._audioRecorder?.active) {
 | 
				
			||||||
 | 
					      this._startListening();
 | 
				
			||||||
 | 
					    } else {
 | 
				
			||||||
 | 
					      this.stopListening();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async _showNotSupportedMessage() {
 | 
				
			||||||
 | 
					    this._addMessage({
 | 
				
			||||||
 | 
					      who: "hass",
 | 
				
			||||||
 | 
					      text:
 | 
				
			||||||
 | 
					        // New lines matter for messages
 | 
				
			||||||
 | 
					        // prettier-ignore
 | 
				
			||||||
 | 
					        html`${this.hass.localize(
 | 
				
			||||||
 | 
					          "ui.dialogs.voice_command.not_supported_microphone_browser"
 | 
				
			||||||
 | 
					        )}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        ${this.hass.localize(
 | 
				
			||||||
 | 
					          "ui.dialogs.voice_command.not_supported_microphone_documentation",
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            documentation_link: html`<a
 | 
				
			||||||
 | 
					                target="_blank"
 | 
				
			||||||
 | 
					                rel="noopener noreferrer"
 | 
				
			||||||
 | 
					                href=${documentationUrl(
 | 
				
			||||||
 | 
					                  this.hass,
 | 
				
			||||||
 | 
					                  "/docs/configuration/securing/#remote-access"
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					              >${this.hass.localize(
 | 
				
			||||||
 | 
					                  "ui.dialogs.voice_command.not_supported_microphone_documentation_link"
 | 
				
			||||||
 | 
					                )}</a>`,
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        )}`,
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private async _startListening() {
 | 
				
			||||||
 | 
					    this._audio?.pause();
 | 
				
			||||||
 | 
					    if (!this._audioRecorder) {
 | 
				
			||||||
 | 
					      this._audioRecorder = new AudioRecorder((audio) => {
 | 
				
			||||||
 | 
					        if (this._audioBuffer) {
 | 
				
			||||||
 | 
					          this._audioBuffer.push(audio);
 | 
				
			||||||
 | 
					        } else {
 | 
				
			||||||
 | 
					          this._sendAudioChunk(audio);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this._stt_binary_handler_id = undefined;
 | 
				
			||||||
 | 
					    this._audioBuffer = [];
 | 
				
			||||||
 | 
					    const userMessage: Message = {
 | 
				
			||||||
 | 
					      who: "user",
 | 
				
			||||||
 | 
					      text: "…",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    this._audioRecorder.start().then(() => {
 | 
				
			||||||
 | 
					      this._addMessage(userMessage);
 | 
				
			||||||
 | 
					      this.requestUpdate("_audioRecorder");
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    const hassMessage: Message = {
 | 
				
			||||||
 | 
					      who: "hass",
 | 
				
			||||||
 | 
					      text: "…",
 | 
				
			||||||
 | 
					    };
 | 
				
			||||||
 | 
					    // To make sure the answer is placed at the right user text, we add it before we process it
 | 
				
			||||||
 | 
					    try {
 | 
				
			||||||
 | 
					      const unsub = await runAssistPipeline(
 | 
				
			||||||
 | 
					        this.hass,
 | 
				
			||||||
 | 
					        (event) => {
 | 
				
			||||||
 | 
					          if (event.type === "run-start") {
 | 
				
			||||||
 | 
					            this._stt_binary_handler_id =
 | 
				
			||||||
 | 
					              event.data.runner_data.stt_binary_handler_id;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // When we start STT stage, the WS has a binary handler
 | 
				
			||||||
 | 
					          if (event.type === "stt-start" && this._audioBuffer) {
 | 
				
			||||||
 | 
					            // Send the buffer over the WS to the STT engine.
 | 
				
			||||||
 | 
					            for (const buffer of this._audioBuffer) {
 | 
				
			||||||
 | 
					              this._sendAudioChunk(buffer);
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this._audioBuffer = undefined;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          // Stop recording if the server is done with STT stage
 | 
				
			||||||
 | 
					          if (event.type === "stt-end") {
 | 
				
			||||||
 | 
					            this._stt_binary_handler_id = undefined;
 | 
				
			||||||
 | 
					            this.stopListening();
 | 
				
			||||||
 | 
					            userMessage.text = event.data.stt_output.text;
 | 
				
			||||||
 | 
					            this.requestUpdate("_conversation");
 | 
				
			||||||
 | 
					            // To make sure the answer is placed at the right user text, we add it before we process it
 | 
				
			||||||
 | 
					            this._addMessage(hassMessage);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (event.type === "intent-end") {
 | 
				
			||||||
 | 
					            this._conversationId = event.data.intent_output.conversation_id;
 | 
				
			||||||
 | 
					            const plain = event.data.intent_output.response.speech?.plain;
 | 
				
			||||||
 | 
					            if (plain) {
 | 
				
			||||||
 | 
					              hassMessage.text = plain.speech;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this.requestUpdate("_conversation");
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (event.type === "tts-end") {
 | 
				
			||||||
 | 
					            const url = event.data.tts_output.url;
 | 
				
			||||||
 | 
					            this._audio = new Audio(url);
 | 
				
			||||||
 | 
					            this._audio.play();
 | 
				
			||||||
 | 
					            this._audio.addEventListener("ended", this._unloadAudio);
 | 
				
			||||||
 | 
					            this._audio.addEventListener("pause", this._unloadAudio);
 | 
				
			||||||
 | 
					            this._audio.addEventListener("canplaythrough", this._playAudio);
 | 
				
			||||||
 | 
					            this._audio.addEventListener("error", this._audioError);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (event.type === "run-end") {
 | 
				
			||||||
 | 
					            this._stt_binary_handler_id = undefined;
 | 
				
			||||||
 | 
					            unsub();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          if (event.type === "error") {
 | 
				
			||||||
 | 
					            this._stt_binary_handler_id = undefined;
 | 
				
			||||||
 | 
					            if (userMessage.text === "…") {
 | 
				
			||||||
 | 
					              userMessage.text = event.data.message;
 | 
				
			||||||
 | 
					              userMessage.error = true;
 | 
				
			||||||
 | 
					            } else {
 | 
				
			||||||
 | 
					              hassMessage.text = event.data.message;
 | 
				
			||||||
 | 
					              hassMessage.error = true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            this.stopListening();
 | 
				
			||||||
 | 
					            this.requestUpdate("_conversation");
 | 
				
			||||||
 | 
					            unsub();
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					        },
 | 
				
			||||||
 | 
					        {
 | 
				
			||||||
 | 
					          start_stage: "stt",
 | 
				
			||||||
 | 
					          end_stage: this._pipeline?.tts_engine ? "tts" : "intent",
 | 
				
			||||||
 | 
					          input: { sample_rate: this._audioRecorder.sampleRate! },
 | 
				
			||||||
 | 
					          pipeline: this._pipeline?.id,
 | 
				
			||||||
 | 
					          conversation_id: this._conversationId,
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					    } catch (err: any) {
 | 
				
			||||||
 | 
					      await showAlertDialog(this, {
 | 
				
			||||||
 | 
					        title: "Error starting pipeline",
 | 
				
			||||||
 | 
					        text: err.message || err,
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					      this.stopListening();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  public stopListening() {
 | 
				
			||||||
 | 
					    this._audioRecorder?.stop();
 | 
				
			||||||
 | 
					    this.requestUpdate("_audioRecorder");
 | 
				
			||||||
 | 
					    // We're currently STTing, so finish audio
 | 
				
			||||||
 | 
					    if (this._stt_binary_handler_id) {
 | 
				
			||||||
 | 
					      if (this._audioBuffer) {
 | 
				
			||||||
 | 
					        for (const chunk of this._audioBuffer) {
 | 
				
			||||||
 | 
					          this._sendAudioChunk(chunk);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      // Send empty message to indicate we're done streaming.
 | 
				
			||||||
 | 
					      this._sendAudioChunk(new Int16Array());
 | 
				
			||||||
 | 
					      this._stt_binary_handler_id = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this._audioBuffer = undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _sendAudioChunk(chunk: Int16Array) {
 | 
				
			||||||
 | 
					    this.hass.connection.socket!.binaryType = "arraybuffer";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // eslint-disable-next-line eqeqeq
 | 
				
			||||||
 | 
					    if (this._stt_binary_handler_id == undefined) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    // Turn into 8 bit so we can prefix our handler ID.
 | 
				
			||||||
 | 
					    const data = new Uint8Array(1 + chunk.length * 2);
 | 
				
			||||||
 | 
					    data[0] = this._stt_binary_handler_id;
 | 
				
			||||||
 | 
					    data.set(new Uint8Array(chunk.buffer), 1);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this.hass.connection.socket!.send(data);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _playAudio = () => {
 | 
				
			||||||
 | 
					    this._audio?.play();
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _audioError = () => {
 | 
				
			||||||
 | 
					    showAlertDialog(this, { title: "Error playing audio." });
 | 
				
			||||||
 | 
					    this._audio?.removeAttribute("src");
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _unloadAudio = () => {
 | 
				
			||||||
 | 
					    this._audio?.removeAttribute("src");
 | 
				
			||||||
 | 
					    this._audio = undefined;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _scrollMessagesBottom() {
 | 
				
			||||||
 | 
					    const scrollContainer = this._scrollContainer;
 | 
				
			||||||
 | 
					    if (!scrollContainer) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    scrollContainer.scrollTo(0, 99999);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _computeMessageClasses(message: Message) {
 | 
				
			||||||
 | 
					    return `message ${message.who} ${message.error ? " error" : ""}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static get styles(): CSSResultGroup {
 | 
				
			||||||
 | 
					    return css`
 | 
				
			||||||
 | 
					      .listening-icon {
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					        color: var(--secondary-text-color);
 | 
				
			||||||
 | 
					        margin-right: -24px;
 | 
				
			||||||
 | 
					        margin-inline-end: -24px;
 | 
				
			||||||
 | 
					        margin-inline-start: initial;
 | 
				
			||||||
 | 
					        direction: var(--direction);
 | 
				
			||||||
 | 
					        transform: scaleX(var(--scale-direction));
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .listening-icon[active] {
 | 
				
			||||||
 | 
					        color: var(--primary-color);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .unsupported {
 | 
				
			||||||
 | 
					        color: var(--error-color);
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        --mdc-icon-size: 16px;
 | 
				
			||||||
 | 
					        right: 5px;
 | 
				
			||||||
 | 
					        inset-inline-end: 5px;
 | 
				
			||||||
 | 
					        inset-inline-start: initial;
 | 
				
			||||||
 | 
					        top: 0px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      ha-textfield {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					        overflow: hidden;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      a.button {
 | 
				
			||||||
 | 
					        text-decoration: none;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .side-by-side {
 | 
				
			||||||
 | 
					        display: flex;
 | 
				
			||||||
 | 
					        margin: 8px 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .side-by-side > * {
 | 
				
			||||||
 | 
					        flex: 1 0;
 | 
				
			||||||
 | 
					        padding: 4px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .messages {
 | 
				
			||||||
 | 
					        display: block;
 | 
				
			||||||
 | 
					        height: 400px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        position: relative;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
				
			||||||
 | 
					        ha-dialog {
 | 
				
			||||||
 | 
					          --mdc-dialog-max-width: 100%;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .messages {
 | 
				
			||||||
 | 
					          height: 100%;
 | 
				
			||||||
 | 
					          flex: 1;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .messages-container {
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        bottom: 0px;
 | 
				
			||||||
 | 
					        right: 0px;
 | 
				
			||||||
 | 
					        left: 0px;
 | 
				
			||||||
 | 
					        padding: 24px;
 | 
				
			||||||
 | 
					        box-sizing: border-box;
 | 
				
			||||||
 | 
					        overflow-y: auto;
 | 
				
			||||||
 | 
					        max-height: 100%;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .message {
 | 
				
			||||||
 | 
					        white-space: pre-line;
 | 
				
			||||||
 | 
					        font-size: 18px;
 | 
				
			||||||
 | 
					        clear: both;
 | 
				
			||||||
 | 
					        margin: 8px 0;
 | 
				
			||||||
 | 
					        padding: 8px;
 | 
				
			||||||
 | 
					        border-radius: 15px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .message p {
 | 
				
			||||||
 | 
					        margin: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .message p:not(:last-child) {
 | 
				
			||||||
 | 
					        margin-bottom: 8px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .message.user {
 | 
				
			||||||
 | 
					        margin-left: 24px;
 | 
				
			||||||
 | 
					        margin-inline-start: 24px;
 | 
				
			||||||
 | 
					        margin-inline-end: initial;
 | 
				
			||||||
 | 
					        float: var(--float-end);
 | 
				
			||||||
 | 
					        text-align: right;
 | 
				
			||||||
 | 
					        border-bottom-right-radius: 0px;
 | 
				
			||||||
 | 
					        background-color: var(--primary-color);
 | 
				
			||||||
 | 
					        color: var(--text-primary-color);
 | 
				
			||||||
 | 
					        direction: var(--direction);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .message.hass {
 | 
				
			||||||
 | 
					        margin-right: 24px;
 | 
				
			||||||
 | 
					        margin-inline-end: 24px;
 | 
				
			||||||
 | 
					        margin-inline-start: initial;
 | 
				
			||||||
 | 
					        float: var(--float-start);
 | 
				
			||||||
 | 
					        border-bottom-left-radius: 0px;
 | 
				
			||||||
 | 
					        background-color: var(--secondary-background-color);
 | 
				
			||||||
 | 
					        color: var(--primary-text-color);
 | 
				
			||||||
 | 
					        direction: var(--direction);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .message.user a {
 | 
				
			||||||
 | 
					        color: var(--text-primary-color);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .message.hass a {
 | 
				
			||||||
 | 
					        color: var(--primary-text-color);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .message img {
 | 
				
			||||||
 | 
					        width: 100%;
 | 
				
			||||||
 | 
					        border-radius: 10px;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .message.error {
 | 
				
			||||||
 | 
					        background-color: var(--error-color);
 | 
				
			||||||
 | 
					        color: var(--text-primary-color);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .input {
 | 
				
			||||||
 | 
					        margin-left: 0;
 | 
				
			||||||
 | 
					        margin-right: 0;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      .bouncer {
 | 
				
			||||||
 | 
					        width: 48px;
 | 
				
			||||||
 | 
					        height: 48px;
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .double-bounce1,
 | 
				
			||||||
 | 
					      .double-bounce2 {
 | 
				
			||||||
 | 
					        width: 48px;
 | 
				
			||||||
 | 
					        height: 48px;
 | 
				
			||||||
 | 
					        border-radius: 50%;
 | 
				
			||||||
 | 
					        background-color: var(--primary-color);
 | 
				
			||||||
 | 
					        opacity: 0.2;
 | 
				
			||||||
 | 
					        position: absolute;
 | 
				
			||||||
 | 
					        top: 0;
 | 
				
			||||||
 | 
					        left: 0;
 | 
				
			||||||
 | 
					        -webkit-animation: sk-bounce 2s infinite ease-in-out;
 | 
				
			||||||
 | 
					        animation: sk-bounce 2s infinite ease-in-out;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      .double-bounce2 {
 | 
				
			||||||
 | 
					        -webkit-animation-delay: -1s;
 | 
				
			||||||
 | 
					        animation-delay: -1s;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      @-webkit-keyframes sk-bounce {
 | 
				
			||||||
 | 
					        0%,
 | 
				
			||||||
 | 
					        100% {
 | 
				
			||||||
 | 
					          -webkit-transform: scale(0);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        50% {
 | 
				
			||||||
 | 
					          -webkit-transform: scale(1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      @keyframes sk-bounce {
 | 
				
			||||||
 | 
					        0%,
 | 
				
			||||||
 | 
					        100% {
 | 
				
			||||||
 | 
					          transform: scale(0);
 | 
				
			||||||
 | 
					          -webkit-transform: scale(0);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        50% {
 | 
				
			||||||
 | 
					          transform: scale(1);
 | 
				
			||||||
 | 
					          -webkit-transform: scale(1);
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
				
			||||||
 | 
					        .message {
 | 
				
			||||||
 | 
					          font-size: 16px;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					    "assist-chat": HaAssistChat;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,11 +1,8 @@
 | 
				
			|||||||
import "@material/mwc-button/mwc-button";
 | 
					import "@material/mwc-button/mwc-button";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  mdiAlertCircle,
 | 
					 | 
				
			||||||
  mdiChevronDown,
 | 
					  mdiChevronDown,
 | 
				
			||||||
  mdiClose,
 | 
					  mdiClose,
 | 
				
			||||||
  mdiHelpCircleOutline,
 | 
					  mdiHelpCircleOutline,
 | 
				
			||||||
  mdiMicrophone,
 | 
					 | 
				
			||||||
  mdiSend,
 | 
					 | 
				
			||||||
  mdiStar,
 | 
					  mdiStar,
 | 
				
			||||||
} from "@mdi/js";
 | 
					} from "@mdi/js";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -15,7 +12,6 @@ import {
 | 
				
			|||||||
  LitElement,
 | 
					  LitElement,
 | 
				
			||||||
  nothing,
 | 
					  nothing,
 | 
				
			||||||
  PropertyValues,
 | 
					  PropertyValues,
 | 
				
			||||||
  TemplateResult,
 | 
					 | 
				
			||||||
} from "lit";
 | 
					} from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
import { storage } from "../../common/decorators/storage";
 | 
					import { storage } from "../../common/decorators/storage";
 | 
				
			||||||
@@ -27,35 +23,25 @@ import "../../components/ha-dialog";
 | 
				
			|||||||
import "../../components/ha-dialog-header";
 | 
					import "../../components/ha-dialog-header";
 | 
				
			||||||
import "../../components/ha-icon-button";
 | 
					import "../../components/ha-icon-button";
 | 
				
			||||||
import "../../components/ha-list-item";
 | 
					import "../../components/ha-list-item";
 | 
				
			||||||
import "../../components/ha-textfield";
 | 
					 | 
				
			||||||
import type { HaTextField } from "../../components/ha-textfield";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AssistPipeline,
 | 
					  AssistPipeline,
 | 
				
			||||||
  getAssistPipeline,
 | 
					  getAssistPipeline,
 | 
				
			||||||
  listAssistPipelines,
 | 
					  listAssistPipelines,
 | 
				
			||||||
  runAssistPipeline,
 | 
					 | 
				
			||||||
} from "../../data/assist_pipeline";
 | 
					} from "../../data/assist_pipeline";
 | 
				
			||||||
import { haStyleDialog } from "../../resources/styles";
 | 
					import { haStyleDialog } from "../../resources/styles";
 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import { AudioRecorder } from "../../util/audio-recorder";
 | 
					import { AudioRecorder } from "../../util/audio-recorder";
 | 
				
			||||||
import { documentationUrl } from "../../util/documentation-url";
 | 
					import { documentationUrl } from "../../util/documentation-url";
 | 
				
			||||||
import { showAlertDialog } from "../generic/show-dialog-box";
 | 
					 | 
				
			||||||
import { VoiceCommandDialogParams } from "./show-ha-voice-command-dialog";
 | 
					import { VoiceCommandDialogParams } from "./show-ha-voice-command-dialog";
 | 
				
			||||||
import { supportsFeature } from "../../common/entity/supports-feature";
 | 
					import { supportsFeature } from "../../common/entity/supports-feature";
 | 
				
			||||||
import { ConversationEntityFeature } from "../../data/conversation";
 | 
					import { ConversationEntityFeature } from "../../data/conversation";
 | 
				
			||||||
 | 
					import "./assist-chat";
 | 
				
			||||||
interface Message {
 | 
					import type { HaAssistChat } from "./assist-chat";
 | 
				
			||||||
  who: string;
 | 
					 | 
				
			||||||
  text?: string | TemplateResult;
 | 
					 | 
				
			||||||
  error?: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-voice-command-dialog")
 | 
					@customElement("ha-voice-command-dialog")
 | 
				
			||||||
export class HaVoiceCommandDialog extends LitElement {
 | 
					export class HaVoiceCommandDialog extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _conversation?: Message[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _opened = false;
 | 
					  @state() private _opened = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @storage({
 | 
					  @storage({
 | 
				
			||||||
@@ -67,25 +53,11 @@ export class HaVoiceCommandDialog extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _pipeline?: AssistPipeline;
 | 
					  @state() private _pipeline?: AssistPipeline;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _showSendButton = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _pipelines?: AssistPipeline[];
 | 
					  @state() private _pipelines?: AssistPipeline[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _preferredPipeline?: string;
 | 
					  @state() private _preferredPipeline?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("#scroll-container") private _scrollContainer!: HTMLDivElement;
 | 
					  @query("assist-chat") private _assistChat!: HaAssistChat;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  @query("#message-input") private _messageInput!: HaTextField;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _conversationId: string | null = null;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _audioRecorder?: AudioRecorder;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _audioBuffer?: Int16Array[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _audio?: HTMLAudioElement;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _stt_binary_handler_id?: number | null;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _pipelinePromise?: Promise<AssistPipeline>;
 | 
					  private _pipelinePromise?: Promise<AssistPipeline>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -101,15 +73,8 @@ export class HaVoiceCommandDialog extends LitElement {
 | 
				
			|||||||
      this._pipelineId = params.pipeline_id;
 | 
					      this._pipelineId = params.pipeline_id;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._conversation = [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        who: "hass",
 | 
					 | 
				
			||||||
        text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
    this._opened = true;
 | 
					    this._opened = true;
 | 
				
			||||||
    await this.updateComplete;
 | 
					    await this.updateComplete;
 | 
				
			||||||
    this._scrollMessagesBottom();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    await this._pipelinePromise;
 | 
					    await this._pipelinePromise;
 | 
				
			||||||
    if (
 | 
					    if (
 | 
				
			||||||
@@ -117,7 +82,7 @@ export class HaVoiceCommandDialog extends LitElement {
 | 
				
			|||||||
      this._pipeline?.stt_engine &&
 | 
					      this._pipeline?.stt_engine &&
 | 
				
			||||||
      AudioRecorder.isSupported
 | 
					      AudioRecorder.isSupported
 | 
				
			||||||
    ) {
 | 
					    ) {
 | 
				
			||||||
      this._toggleListening();
 | 
					      this._assistChat.toggleListening();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -125,11 +90,7 @@ export class HaVoiceCommandDialog extends LitElement {
 | 
				
			|||||||
    this._opened = false;
 | 
					    this._opened = false;
 | 
				
			||||||
    this._pipeline = undefined;
 | 
					    this._pipeline = undefined;
 | 
				
			||||||
    this._pipelines = undefined;
 | 
					    this._pipelines = undefined;
 | 
				
			||||||
    this._conversation = undefined;
 | 
					    this._assistChat.stopListening();
 | 
				
			||||||
    this._conversationId = null;
 | 
					 | 
				
			||||||
    this._audioRecorder?.close();
 | 
					 | 
				
			||||||
    this._audioRecorder = undefined;
 | 
					 | 
				
			||||||
    this._audio?.pause();
 | 
					 | 
				
			||||||
    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
					    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -146,15 +107,13 @@ export class HaVoiceCommandDialog extends LitElement {
 | 
				
			|||||||
            ConversationEntityFeature.CONTROL
 | 
					            ConversationEntityFeature.CONTROL
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        : true;
 | 
					        : true;
 | 
				
			||||||
    const supportsMicrophone = AudioRecorder.isSupported;
 | 
					 | 
				
			||||||
    const supportsSTT = this._pipeline?.stt_engine;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-dialog
 | 
					      <ha-dialog
 | 
				
			||||||
        open
 | 
					        open
 | 
				
			||||||
        @closed=${this.closeDialog}
 | 
					        @closed=${this.closeDialog}
 | 
				
			||||||
        .heading=${this.hass.localize("ui.dialogs.voice_command.title")}
 | 
					        .heading=${this.hass.localize("ui.dialogs.voice_command.title")}
 | 
				
			||||||
        flexContent
 | 
					        hideactions
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <ha-dialog-header slot="heading">
 | 
					        <ha-dialog-header slot="heading">
 | 
				
			||||||
          <ha-icon-button
 | 
					          <ha-icon-button
 | 
				
			||||||
@@ -231,71 +190,10 @@ export class HaVoiceCommandDialog extends LitElement {
 | 
				
			|||||||
                )}
 | 
					                )}
 | 
				
			||||||
              </ha-alert>
 | 
					              </ha-alert>
 | 
				
			||||||
            `}
 | 
					            `}
 | 
				
			||||||
        <div class="messages">
 | 
					        <assist-chat
 | 
				
			||||||
          <div class="messages-container" id="scroll-container">
 | 
					          .hass=${this.hass}
 | 
				
			||||||
            ${this._conversation!.map(
 | 
					          .pipelineId=${this._pipelineId}
 | 
				
			||||||
              // New lines matter for messages
 | 
					        ></assist-chat>
 | 
				
			||||||
              // prettier-ignore
 | 
					 | 
				
			||||||
              (message) => html`
 | 
					 | 
				
			||||||
                <div class=${this._computeMessageClasses(message)}>${message.text}</div>
 | 
					 | 
				
			||||||
              `
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <div class="input" slot="primaryAction">
 | 
					 | 
				
			||||||
          <ha-textfield
 | 
					 | 
				
			||||||
            id="message-input"
 | 
					 | 
				
			||||||
            @keyup=${this._handleKeyUp}
 | 
					 | 
				
			||||||
            @input=${this._handleInput}
 | 
					 | 
				
			||||||
            .label=${this.hass.localize(`ui.dialogs.voice_command.input_label`)}
 | 
					 | 
				
			||||||
            dialogInitialFocus
 | 
					 | 
				
			||||||
            iconTrailing
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <span slot="trailingIcon">
 | 
					 | 
				
			||||||
              ${this._showSendButton || !supportsSTT
 | 
					 | 
				
			||||||
                ? html`
 | 
					 | 
				
			||||||
                    <ha-icon-button
 | 
					 | 
				
			||||||
                      class="listening-icon"
 | 
					 | 
				
			||||||
                      .path=${mdiSend}
 | 
					 | 
				
			||||||
                      @click=${this._handleSendMessage}
 | 
					 | 
				
			||||||
                      .label=${this.hass.localize(
 | 
					 | 
				
			||||||
                        "ui.dialogs.voice_command.send_text"
 | 
					 | 
				
			||||||
                      )}
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                    </ha-icon-button>
 | 
					 | 
				
			||||||
                  `
 | 
					 | 
				
			||||||
                : html`
 | 
					 | 
				
			||||||
                    ${this._audioRecorder?.active
 | 
					 | 
				
			||||||
                      ? html`
 | 
					 | 
				
			||||||
                          <div class="bouncer">
 | 
					 | 
				
			||||||
                            <div class="double-bounce1"></div>
 | 
					 | 
				
			||||||
                            <div class="double-bounce2"></div>
 | 
					 | 
				
			||||||
                          </div>
 | 
					 | 
				
			||||||
                        `
 | 
					 | 
				
			||||||
                      : nothing}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
                    <div class="listening-icon">
 | 
					 | 
				
			||||||
                      <ha-icon-button
 | 
					 | 
				
			||||||
                        .path=${mdiMicrophone}
 | 
					 | 
				
			||||||
                        @click=${this._handleListeningButton}
 | 
					 | 
				
			||||||
                        .label=${this.hass.localize(
 | 
					 | 
				
			||||||
                          "ui.dialogs.voice_command.start_listening"
 | 
					 | 
				
			||||||
                        )}
 | 
					 | 
				
			||||||
                      >
 | 
					 | 
				
			||||||
                      </ha-icon-button>
 | 
					 | 
				
			||||||
                      ${!supportsMicrophone
 | 
					 | 
				
			||||||
                        ? html`
 | 
					 | 
				
			||||||
                            <ha-svg-icon
 | 
					 | 
				
			||||||
                              .path=${mdiAlertCircle}
 | 
					 | 
				
			||||||
                              class="unsupported"
 | 
					 | 
				
			||||||
                            ></ha-svg-icon>
 | 
					 | 
				
			||||||
                          `
 | 
					 | 
				
			||||||
                        : null}
 | 
					 | 
				
			||||||
                    </div>
 | 
					 | 
				
			||||||
                  `}
 | 
					 | 
				
			||||||
            </span>
 | 
					 | 
				
			||||||
          </ha-textfield>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </ha-dialog>
 | 
					      </ha-dialog>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -333,339 +231,12 @@ export class HaVoiceCommandDialog extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private async _selectPipeline(ev: CustomEvent) {
 | 
					  private async _selectPipeline(ev: CustomEvent) {
 | 
				
			||||||
    this._pipelineId = (ev.currentTarget as any).pipeline;
 | 
					    this._pipelineId = (ev.currentTarget as any).pipeline;
 | 
				
			||||||
    this._conversation = [
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        who: "hass",
 | 
					 | 
				
			||||||
        text: this.hass.localize("ui.dialogs.voice_command.how_can_i_help"),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
    await this.updateComplete;
 | 
					 | 
				
			||||||
    this._scrollMessagesBottom();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected updated(changedProps: PropertyValues) {
 | 
					 | 
				
			||||||
    super.updated(changedProps);
 | 
					 | 
				
			||||||
    if (changedProps.has("_conversation") || changedProps.has("results")) {
 | 
					 | 
				
			||||||
      this._scrollMessagesBottom();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _addMessage(message: Message) {
 | 
					 | 
				
			||||||
    this._conversation = [...this._conversation!, message];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleKeyUp(ev: KeyboardEvent) {
 | 
					 | 
				
			||||||
    const input = ev.target as HaTextField;
 | 
					 | 
				
			||||||
    if (ev.key === "Enter" && input.value) {
 | 
					 | 
				
			||||||
      this._processText(input.value);
 | 
					 | 
				
			||||||
      input.value = "";
 | 
					 | 
				
			||||||
      this._showSendButton = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleInput(ev: InputEvent) {
 | 
					 | 
				
			||||||
    const value = (ev.target as HaTextField).value;
 | 
					 | 
				
			||||||
    if (value && !this._showSendButton) {
 | 
					 | 
				
			||||||
      this._showSendButton = true;
 | 
					 | 
				
			||||||
    } else if (!value && this._showSendButton) {
 | 
					 | 
				
			||||||
      this._showSendButton = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleSendMessage() {
 | 
					 | 
				
			||||||
    if (this._messageInput.value) {
 | 
					 | 
				
			||||||
      this._processText(this._messageInput.value.trim());
 | 
					 | 
				
			||||||
      this._messageInput.value = "";
 | 
					 | 
				
			||||||
      this._showSendButton = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _processText(text: string) {
 | 
					 | 
				
			||||||
    this._audio?.pause();
 | 
					 | 
				
			||||||
    this._addMessage({ who: "user", text });
 | 
					 | 
				
			||||||
    const message: Message = {
 | 
					 | 
				
			||||||
      who: "hass",
 | 
					 | 
				
			||||||
      text: "…",
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    // To make sure the answer is placed at the right user text, we add it before we process it
 | 
					 | 
				
			||||||
    this._addMessage(message);
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const unsub = await runAssistPipeline(
 | 
					 | 
				
			||||||
        this.hass,
 | 
					 | 
				
			||||||
        (event) => {
 | 
					 | 
				
			||||||
          if (event.type === "intent-end") {
 | 
					 | 
				
			||||||
            this._conversationId = event.data.intent_output.conversation_id;
 | 
					 | 
				
			||||||
            const plain = event.data.intent_output.response.speech?.plain;
 | 
					 | 
				
			||||||
            if (plain) {
 | 
					 | 
				
			||||||
              message.text = plain.speech;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            this.requestUpdate("_conversation");
 | 
					 | 
				
			||||||
            unsub();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          if (event.type === "error") {
 | 
					 | 
				
			||||||
            message.text = event.data.message;
 | 
					 | 
				
			||||||
            message.error = true;
 | 
					 | 
				
			||||||
            this.requestUpdate("_conversation");
 | 
					 | 
				
			||||||
            unsub();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          start_stage: "intent",
 | 
					 | 
				
			||||||
          input: { text },
 | 
					 | 
				
			||||||
          end_stage: "intent",
 | 
					 | 
				
			||||||
          pipeline: this._pipeline?.id,
 | 
					 | 
				
			||||||
          conversation_id: this._conversationId,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
      message.text = this.hass.localize("ui.dialogs.voice_command.error");
 | 
					 | 
				
			||||||
      message.error = true;
 | 
					 | 
				
			||||||
      this.requestUpdate("_conversation");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleListeningButton(ev) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    ev.preventDefault();
 | 
					 | 
				
			||||||
    this._toggleListening();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _toggleListening() {
 | 
					 | 
				
			||||||
    const supportsMicrophone = AudioRecorder.isSupported;
 | 
					 | 
				
			||||||
    if (!supportsMicrophone) {
 | 
					 | 
				
			||||||
      this._showNotSupportedMessage();
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (!this._audioRecorder?.active) {
 | 
					 | 
				
			||||||
      this._startListening();
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this._stopListening();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _showNotSupportedMessage() {
 | 
					 | 
				
			||||||
    this._addMessage({
 | 
					 | 
				
			||||||
      who: "hass",
 | 
					 | 
				
			||||||
      text:
 | 
					 | 
				
			||||||
        // New lines matter for messages
 | 
					 | 
				
			||||||
        // prettier-ignore
 | 
					 | 
				
			||||||
        html`${this.hass.localize(
 | 
					 | 
				
			||||||
          "ui.dialogs.voice_command.not_supported_microphone_browser"
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ${this.hass.localize(
 | 
					 | 
				
			||||||
          "ui.dialogs.voice_command.not_supported_microphone_documentation",
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            documentation_link: html`<a
 | 
					 | 
				
			||||||
                target="_blank"
 | 
					 | 
				
			||||||
                rel="noopener noreferrer"
 | 
					 | 
				
			||||||
                href=${documentationUrl(
 | 
					 | 
				
			||||||
                  this.hass,
 | 
					 | 
				
			||||||
                  "/docs/configuration/securing/#remote-access"
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
              >${this.hass.localize(
 | 
					 | 
				
			||||||
                  "ui.dialogs.voice_command.not_supported_microphone_documentation_link"
 | 
					 | 
				
			||||||
                )}</a>`,
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        )}`,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _startListening() {
 | 
					 | 
				
			||||||
    this._audio?.pause();
 | 
					 | 
				
			||||||
    if (!this._audioRecorder) {
 | 
					 | 
				
			||||||
      this._audioRecorder = new AudioRecorder((audio) => {
 | 
					 | 
				
			||||||
        if (this._audioBuffer) {
 | 
					 | 
				
			||||||
          this._audioBuffer.push(audio);
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          this._sendAudioChunk(audio);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this._stt_binary_handler_id = undefined;
 | 
					 | 
				
			||||||
    this._audioBuffer = [];
 | 
					 | 
				
			||||||
    const userMessage: Message = {
 | 
					 | 
				
			||||||
      who: "user",
 | 
					 | 
				
			||||||
      text: "…",
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    await this._audioRecorder.start();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._addMessage(userMessage);
 | 
					 | 
				
			||||||
    this.requestUpdate("_audioRecorder");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const hassMessage: Message = {
 | 
					 | 
				
			||||||
      who: "hass",
 | 
					 | 
				
			||||||
      text: "…",
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
    // To make sure the answer is placed at the right user text, we add it before we process it
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const unsub = await runAssistPipeline(
 | 
					 | 
				
			||||||
        this.hass,
 | 
					 | 
				
			||||||
        (event) => {
 | 
					 | 
				
			||||||
          if (event.type === "run-start") {
 | 
					 | 
				
			||||||
            this._stt_binary_handler_id =
 | 
					 | 
				
			||||||
              event.data.runner_data.stt_binary_handler_id;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // When we start STT stage, the WS has a binary handler
 | 
					 | 
				
			||||||
          if (event.type === "stt-start" && this._audioBuffer) {
 | 
					 | 
				
			||||||
            // Send the buffer over the WS to the STT engine.
 | 
					 | 
				
			||||||
            for (const buffer of this._audioBuffer) {
 | 
					 | 
				
			||||||
              this._sendAudioChunk(buffer);
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            this._audioBuffer = undefined;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          // Stop recording if the server is done with STT stage
 | 
					 | 
				
			||||||
          if (event.type === "stt-end") {
 | 
					 | 
				
			||||||
            this._stt_binary_handler_id = undefined;
 | 
					 | 
				
			||||||
            this._stopListening();
 | 
					 | 
				
			||||||
            userMessage.text = event.data.stt_output.text;
 | 
					 | 
				
			||||||
            this.requestUpdate("_conversation");
 | 
					 | 
				
			||||||
            // To make sure the answer is placed at the right user text, we add it before we process it
 | 
					 | 
				
			||||||
            this._addMessage(hassMessage);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (event.type === "intent-end") {
 | 
					 | 
				
			||||||
            this._conversationId = event.data.intent_output.conversation_id;
 | 
					 | 
				
			||||||
            const plain = event.data.intent_output.response.speech?.plain;
 | 
					 | 
				
			||||||
            if (plain) {
 | 
					 | 
				
			||||||
              hassMessage.text = plain.speech;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            this.requestUpdate("_conversation");
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (event.type === "tts-end") {
 | 
					 | 
				
			||||||
            const url = event.data.tts_output.url;
 | 
					 | 
				
			||||||
            this._audio = new Audio(url);
 | 
					 | 
				
			||||||
            this._audio.play();
 | 
					 | 
				
			||||||
            this._audio.addEventListener("ended", this._unloadAudio);
 | 
					 | 
				
			||||||
            this._audio.addEventListener("pause", this._unloadAudio);
 | 
					 | 
				
			||||||
            this._audio.addEventListener("canplaythrough", this._playAudio);
 | 
					 | 
				
			||||||
            this._audio.addEventListener("error", this._audioError);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (event.type === "run-end") {
 | 
					 | 
				
			||||||
            this._stt_binary_handler_id = undefined;
 | 
					 | 
				
			||||||
            unsub();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
          if (event.type === "error") {
 | 
					 | 
				
			||||||
            this._stt_binary_handler_id = undefined;
 | 
					 | 
				
			||||||
            if (userMessage.text === "…") {
 | 
					 | 
				
			||||||
              userMessage.text = event.data.message;
 | 
					 | 
				
			||||||
              userMessage.error = true;
 | 
					 | 
				
			||||||
            } else {
 | 
					 | 
				
			||||||
              hassMessage.text = event.data.message;
 | 
					 | 
				
			||||||
              hassMessage.error = true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
            this._stopListening();
 | 
					 | 
				
			||||||
            this.requestUpdate("_conversation");
 | 
					 | 
				
			||||||
            unsub();
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        },
 | 
					 | 
				
			||||||
        {
 | 
					 | 
				
			||||||
          start_stage: "stt",
 | 
					 | 
				
			||||||
          end_stage: this._pipeline?.tts_engine ? "tts" : "intent",
 | 
					 | 
				
			||||||
          input: { sample_rate: this._audioRecorder.sampleRate! },
 | 
					 | 
				
			||||||
          pipeline: this._pipeline?.id,
 | 
					 | 
				
			||||||
          conversation_id: this._conversationId,
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    } catch (err: any) {
 | 
					 | 
				
			||||||
      await showAlertDialog(this, {
 | 
					 | 
				
			||||||
        title: "Error starting pipeline",
 | 
					 | 
				
			||||||
        text: err.message || err,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      this._stopListening();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _stopListening() {
 | 
					 | 
				
			||||||
    this._audioRecorder?.stop();
 | 
					 | 
				
			||||||
    this.requestUpdate("_audioRecorder");
 | 
					 | 
				
			||||||
    // We're currently STTing, so finish audio
 | 
					 | 
				
			||||||
    if (this._stt_binary_handler_id) {
 | 
					 | 
				
			||||||
      if (this._audioBuffer) {
 | 
					 | 
				
			||||||
        for (const chunk of this._audioBuffer) {
 | 
					 | 
				
			||||||
          this._sendAudioChunk(chunk);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      // Send empty message to indicate we're done streaming.
 | 
					 | 
				
			||||||
      this._sendAudioChunk(new Int16Array());
 | 
					 | 
				
			||||||
      this._stt_binary_handler_id = undefined;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this._audioBuffer = undefined;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _sendAudioChunk(chunk: Int16Array) {
 | 
					 | 
				
			||||||
    this.hass.connection.socket!.binaryType = "arraybuffer";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // eslint-disable-next-line eqeqeq
 | 
					 | 
				
			||||||
    if (this._stt_binary_handler_id == undefined) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    // Turn into 8 bit so we can prefix our handler ID.
 | 
					 | 
				
			||||||
    const data = new Uint8Array(1 + chunk.length * 2);
 | 
					 | 
				
			||||||
    data[0] = this._stt_binary_handler_id;
 | 
					 | 
				
			||||||
    data.set(new Uint8Array(chunk.buffer), 1);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this.hass.connection.socket!.send(data);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _playAudio = () => {
 | 
					 | 
				
			||||||
    this._audio?.play();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _audioError = () => {
 | 
					 | 
				
			||||||
    showAlertDialog(this, { title: "Error playing audio." });
 | 
					 | 
				
			||||||
    this._audio?.removeAttribute("src");
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _unloadAudio = () => {
 | 
					 | 
				
			||||||
    this._audio?.removeAttribute("src");
 | 
					 | 
				
			||||||
    this._audio = undefined;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _scrollMessagesBottom() {
 | 
					 | 
				
			||||||
    const scrollContainer = this._scrollContainer;
 | 
					 | 
				
			||||||
    if (!scrollContainer) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    scrollContainer.scrollTo(0, 99999);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _computeMessageClasses(message: Message) {
 | 
					 | 
				
			||||||
    return `message ${message.who} ${message.error ? " error" : ""}`;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static get styles(): CSSResultGroup {
 | 
					  static get styles(): CSSResultGroup {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
      haStyleDialog,
 | 
					      haStyleDialog,
 | 
				
			||||||
      css`
 | 
					      css`
 | 
				
			||||||
        .listening-icon {
 | 
					 | 
				
			||||||
          position: relative;
 | 
					 | 
				
			||||||
          color: var(--secondary-text-color);
 | 
					 | 
				
			||||||
          margin-right: -24px;
 | 
					 | 
				
			||||||
          margin-inline-end: -24px;
 | 
					 | 
				
			||||||
          margin-inline-start: initial;
 | 
					 | 
				
			||||||
          direction: var(--direction);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .listening-icon[active] {
 | 
					 | 
				
			||||||
          color: var(--primary-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .unsupported {
 | 
					 | 
				
			||||||
          color: var(--error-color);
 | 
					 | 
				
			||||||
          position: absolute;
 | 
					 | 
				
			||||||
          --mdc-icon-size: 16px;
 | 
					 | 
				
			||||||
          right: 5px;
 | 
					 | 
				
			||||||
          inset-inline-end: 5px;
 | 
					 | 
				
			||||||
          inset-inline-start: initial;
 | 
					 | 
				
			||||||
          top: 0px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ha-dialog {
 | 
					        ha-dialog {
 | 
				
			||||||
          --primary-action-button-flex: 1;
 | 
					          --primary-action-button-flex: 1;
 | 
				
			||||||
          --secondary-action-button-flex: 0;
 | 
					          --secondary-action-button-flex: 0;
 | 
				
			||||||
@@ -726,158 +297,6 @@ export class HaVoiceCommandDialog extends LitElement {
 | 
				
			|||||||
        ha-button-menu a {
 | 
					        ha-button-menu a {
 | 
				
			||||||
          text-decoration: none;
 | 
					          text-decoration: none;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        ha-textfield {
 | 
					 | 
				
			||||||
          display: block;
 | 
					 | 
				
			||||||
          overflow: hidden;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        a.button {
 | 
					 | 
				
			||||||
          text-decoration: none;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        a.button > mwc-button {
 | 
					 | 
				
			||||||
          width: 100%;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .side-by-side {
 | 
					 | 
				
			||||||
          display: flex;
 | 
					 | 
				
			||||||
          margin: 8px 0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .side-by-side > * {
 | 
					 | 
				
			||||||
          flex: 1 0;
 | 
					 | 
				
			||||||
          padding: 4px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .messages {
 | 
					 | 
				
			||||||
          display: block;
 | 
					 | 
				
			||||||
          height: 400px;
 | 
					 | 
				
			||||||
          box-sizing: border-box;
 | 
					 | 
				
			||||||
          position: relative;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
					 | 
				
			||||||
          ha-dialog {
 | 
					 | 
				
			||||||
            --mdc-dialog-max-width: 100%;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          .messages {
 | 
					 | 
				
			||||||
            height: 100%;
 | 
					 | 
				
			||||||
            flex: 1;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .messages-container {
 | 
					 | 
				
			||||||
          position: absolute;
 | 
					 | 
				
			||||||
          bottom: 0px;
 | 
					 | 
				
			||||||
          right: 0px;
 | 
					 | 
				
			||||||
          left: 0px;
 | 
					 | 
				
			||||||
          padding: 24px;
 | 
					 | 
				
			||||||
          box-sizing: border-box;
 | 
					 | 
				
			||||||
          overflow-y: auto;
 | 
					 | 
				
			||||||
          max-height: 100%;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .message {
 | 
					 | 
				
			||||||
          white-space: pre-line;
 | 
					 | 
				
			||||||
          font-size: 18px;
 | 
					 | 
				
			||||||
          clear: both;
 | 
					 | 
				
			||||||
          margin: 8px 0;
 | 
					 | 
				
			||||||
          padding: 8px;
 | 
					 | 
				
			||||||
          border-radius: 15px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .message p {
 | 
					 | 
				
			||||||
          margin: 0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .message p:not(:last-child) {
 | 
					 | 
				
			||||||
          margin-bottom: 8px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message.user {
 | 
					 | 
				
			||||||
          margin-left: 24px;
 | 
					 | 
				
			||||||
          margin-inline-start: 24px;
 | 
					 | 
				
			||||||
          margin-inline-end: initial;
 | 
					 | 
				
			||||||
          float: var(--float-end);
 | 
					 | 
				
			||||||
          text-align: right;
 | 
					 | 
				
			||||||
          border-bottom-right-radius: 0px;
 | 
					 | 
				
			||||||
          background-color: var(--primary-color);
 | 
					 | 
				
			||||||
          color: var(--text-primary-color);
 | 
					 | 
				
			||||||
          direction: var(--direction);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message.hass {
 | 
					 | 
				
			||||||
          margin-right: 24px;
 | 
					 | 
				
			||||||
          margin-inline-end: 24px;
 | 
					 | 
				
			||||||
          margin-inline-start: initial;
 | 
					 | 
				
			||||||
          float: var(--float-start);
 | 
					 | 
				
			||||||
          border-bottom-left-radius: 0px;
 | 
					 | 
				
			||||||
          background-color: var(--secondary-background-color);
 | 
					 | 
				
			||||||
          color: var(--primary-text-color);
 | 
					 | 
				
			||||||
          direction: var(--direction);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message.user a {
 | 
					 | 
				
			||||||
          color: var(--text-primary-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message.hass a {
 | 
					 | 
				
			||||||
          color: var(--primary-text-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message img {
 | 
					 | 
				
			||||||
          width: 100%;
 | 
					 | 
				
			||||||
          border-radius: 10px;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .message.error {
 | 
					 | 
				
			||||||
          background-color: var(--error-color);
 | 
					 | 
				
			||||||
          color: var(--text-primary-color);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .input {
 | 
					 | 
				
			||||||
          margin-left: 0;
 | 
					 | 
				
			||||||
          margin-right: 0;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        .bouncer {
 | 
					 | 
				
			||||||
          width: 48px;
 | 
					 | 
				
			||||||
          height: 48px;
 | 
					 | 
				
			||||||
          position: absolute;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .double-bounce1,
 | 
					 | 
				
			||||||
        .double-bounce2 {
 | 
					 | 
				
			||||||
          width: 48px;
 | 
					 | 
				
			||||||
          height: 48px;
 | 
					 | 
				
			||||||
          border-radius: 50%;
 | 
					 | 
				
			||||||
          background-color: var(--primary-color);
 | 
					 | 
				
			||||||
          opacity: 0.2;
 | 
					 | 
				
			||||||
          position: absolute;
 | 
					 | 
				
			||||||
          top: 0;
 | 
					 | 
				
			||||||
          left: 0;
 | 
					 | 
				
			||||||
          -webkit-animation: sk-bounce 2s infinite ease-in-out;
 | 
					 | 
				
			||||||
          animation: sk-bounce 2s infinite ease-in-out;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        .double-bounce2 {
 | 
					 | 
				
			||||||
          -webkit-animation-delay: -1s;
 | 
					 | 
				
			||||||
          animation-delay: -1s;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        @-webkit-keyframes sk-bounce {
 | 
					 | 
				
			||||||
          0%,
 | 
					 | 
				
			||||||
          100% {
 | 
					 | 
				
			||||||
            -webkit-transform: scale(0);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          50% {
 | 
					 | 
				
			||||||
            -webkit-transform: scale(1);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        @keyframes sk-bounce {
 | 
					 | 
				
			||||||
          0%,
 | 
					 | 
				
			||||||
          100% {
 | 
					 | 
				
			||||||
            transform: scale(0);
 | 
					 | 
				
			||||||
            -webkit-transform: scale(0);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          50% {
 | 
					 | 
				
			||||||
            transform: scale(1);
 | 
					 | 
				
			||||||
            -webkit-transform: scale(1);
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
					 | 
				
			||||||
          .message {
 | 
					 | 
				
			||||||
            font-size: 16px;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,6 +21,7 @@ import { subscribeRepairsIssueRegistry } from "../data/repairs";
 | 
				
			|||||||
import { subscribeAreaRegistry } from "../data/ws-area_registry";
 | 
					import { subscribeAreaRegistry } from "../data/ws-area_registry";
 | 
				
			||||||
import { subscribeDeviceRegistry } from "../data/ws-device_registry";
 | 
					import { subscribeDeviceRegistry } from "../data/ws-device_registry";
 | 
				
			||||||
import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display";
 | 
					import { subscribeEntityRegistryDisplay } from "../data/ws-entity_registry_display";
 | 
				
			||||||
 | 
					import { subscribeFloorRegistry } from "../data/ws-floor_registry";
 | 
				
			||||||
import { subscribePanels } from "../data/ws-panels";
 | 
					import { subscribePanels } from "../data/ws-panels";
 | 
				
			||||||
import { subscribeThemes } from "../data/ws-themes";
 | 
					import { subscribeThemes } from "../data/ws-themes";
 | 
				
			||||||
import { subscribeUser } from "../data/ws-user";
 | 
					import { subscribeUser } from "../data/ws-user";
 | 
				
			||||||
@@ -117,6 +118,7 @@ window.hassConnection.then(({ conn }) => {
 | 
				
			|||||||
  subscribeEntityRegistryDisplay(conn, noop);
 | 
					  subscribeEntityRegistryDisplay(conn, noop);
 | 
				
			||||||
  subscribeDeviceRegistry(conn, noop);
 | 
					  subscribeDeviceRegistry(conn, noop);
 | 
				
			||||||
  subscribeAreaRegistry(conn, noop);
 | 
					  subscribeAreaRegistry(conn, noop);
 | 
				
			||||||
 | 
					  subscribeFloorRegistry(conn, noop);
 | 
				
			||||||
  subscribeConfig(conn, noop);
 | 
					  subscribeConfig(conn, noop);
 | 
				
			||||||
  subscribeServices(conn, noop);
 | 
					  subscribeServices(conn, noop);
 | 
				
			||||||
  subscribePanels(conn, noop);
 | 
					  subscribePanels(conn, noop);
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -57,6 +57,11 @@ interface EMOutgoingMessageBarCodeNotify extends EMMessage {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
interface EMOutgoingMessageMatterCommission extends EMMessage {
 | 
					interface EMOutgoingMessageMatterCommission extends EMMessage {
 | 
				
			||||||
  type: "matter/commission";
 | 
					  type: "matter/commission";
 | 
				
			||||||
 | 
					  payload?: {
 | 
				
			||||||
 | 
					    mac_extended_address: string | null;
 | 
				
			||||||
 | 
					    border_agent_id: string | null;
 | 
				
			||||||
 | 
					    active_operational_dataset: string | null;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface EMOutgoingMessageImportThreadCredentials extends EMMessage {
 | 
					interface EMOutgoingMessageImportThreadCredentials extends EMMessage {
 | 
				
			||||||
@@ -136,7 +141,7 @@ interface EMOutgoingMessageThreadStoreInPlatformKeychain extends EMMessage {
 | 
				
			|||||||
  type: "thread/store_in_platform_keychain";
 | 
					  type: "thread/store_in_platform_keychain";
 | 
				
			||||||
  payload: {
 | 
					  payload: {
 | 
				
			||||||
    mac_extended_address: string;
 | 
					    mac_extended_address: string;
 | 
				
			||||||
    border_agent_id: string | null;
 | 
					    border_agent_id: string;
 | 
				
			||||||
    active_operational_dataset: string;
 | 
					    active_operational_dataset: string;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -35,10 +35,10 @@ import type {
 | 
				
			|||||||
  HaDataTable,
 | 
					  HaDataTable,
 | 
				
			||||||
  SortingDirection,
 | 
					  SortingDirection,
 | 
				
			||||||
} from "../components/data-table/ha-data-table";
 | 
					} from "../components/data-table/ha-data-table";
 | 
				
			||||||
import "../components/ha-button-menu-new";
 | 
					import "../components/ha-md-button-menu";
 | 
				
			||||||
import "../components/ha-dialog";
 | 
					import "../components/ha-dialog";
 | 
				
			||||||
import { HaMenu } from "../components/ha-menu";
 | 
					import { HaMenu } from "../components/ha-menu";
 | 
				
			||||||
import "../components/ha-menu-item";
 | 
					import "../components/ha-md-menu-item";
 | 
				
			||||||
import "../components/search-input-outlined";
 | 
					import "../components/search-input-outlined";
 | 
				
			||||||
import type { HomeAssistant, Route } from "../types";
 | 
					import type { HomeAssistant, Route } from "../types";
 | 
				
			||||||
import "./hass-tabs-subpage";
 | 
					import "./hass-tabs-subpage";
 | 
				
			||||||
@@ -330,7 +330,7 @@ export class HaTabsSubpageDataTable extends LitElement {
 | 
				
			|||||||
                    "ui.components.subpage-data-table.exit_selection_mode"
 | 
					                    "ui.components.subpage-data-table.exit_selection_mode"
 | 
				
			||||||
                  )}
 | 
					                  )}
 | 
				
			||||||
                ></ha-icon-button>
 | 
					                ></ha-icon-button>
 | 
				
			||||||
                <ha-button-menu-new positioning="absolute">
 | 
					                <ha-md-button-menu positioning="absolute">
 | 
				
			||||||
                  <ha-assist-chip
 | 
					                  <ha-assist-chip
 | 
				
			||||||
                    .label=${localize(
 | 
					                    .label=${localize(
 | 
				
			||||||
                      "ui.components.subpage-data-table.select"
 | 
					                      "ui.components.subpage-data-table.select"
 | 
				
			||||||
@@ -346,20 +346,26 @@ export class HaTabsSubpageDataTable extends LitElement {
 | 
				
			|||||||
                      .path=${mdiMenuDown}
 | 
					                      .path=${mdiMenuDown}
 | 
				
			||||||
                    ></ha-svg-icon
 | 
					                    ></ha-svg-icon
 | 
				
			||||||
                  ></ha-assist-chip>
 | 
					                  ></ha-assist-chip>
 | 
				
			||||||
                  <ha-menu-item .value=${undefined} @click=${this._selectAll}>
 | 
					                  <ha-md-menu-item
 | 
				
			||||||
 | 
					                    .value=${undefined}
 | 
				
			||||||
 | 
					                    @click=${this._selectAll}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
                    <div slot="headline">
 | 
					                    <div slot="headline">
 | 
				
			||||||
                      ${localize("ui.components.subpage-data-table.select_all")}
 | 
					                      ${localize("ui.components.subpage-data-table.select_all")}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </ha-menu-item>
 | 
					                  </ha-md-menu-item>
 | 
				
			||||||
                  <ha-menu-item .value=${undefined} @click=${this._selectNone}>
 | 
					                  <ha-md-menu-item
 | 
				
			||||||
 | 
					                    .value=${undefined}
 | 
				
			||||||
 | 
					                    @click=${this._selectNone}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
                    <div slot="headline">
 | 
					                    <div slot="headline">
 | 
				
			||||||
                      ${localize(
 | 
					                      ${localize(
 | 
				
			||||||
                        "ui.components.subpage-data-table.select_none"
 | 
					                        "ui.components.subpage-data-table.select_none"
 | 
				
			||||||
                      )}
 | 
					                      )}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </ha-menu-item>
 | 
					                  </ha-md-menu-item>
 | 
				
			||||||
                  <md-divider role="separator" tabindex="-1"></md-divider>
 | 
					                  <md-divider role="separator" tabindex="-1"></md-divider>
 | 
				
			||||||
                  <ha-menu-item
 | 
					                  <ha-md-menu-item
 | 
				
			||||||
                    .value=${undefined}
 | 
					                    .value=${undefined}
 | 
				
			||||||
                    @click=${this._disableSelectMode}
 | 
					                    @click=${this._disableSelectMode}
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
@@ -368,8 +374,8 @@ export class HaTabsSubpageDataTable extends LitElement {
 | 
				
			|||||||
                        "ui.components.subpage-data-table.close_select_mode"
 | 
					                        "ui.components.subpage-data-table.close_select_mode"
 | 
				
			||||||
                      )}
 | 
					                      )}
 | 
				
			||||||
                    </div>
 | 
					                    </div>
 | 
				
			||||||
                  </ha-menu-item>
 | 
					                  </ha-md-menu-item>
 | 
				
			||||||
                </ha-button-menu-new>
 | 
					                </ha-md-button-menu>
 | 
				
			||||||
                <p>
 | 
					                <p>
 | 
				
			||||||
                  ${localize("ui.components.subpage-data-table.selected", {
 | 
					                  ${localize("ui.components.subpage-data-table.selected", {
 | 
				
			||||||
                    selected: this.selected || "0",
 | 
					                    selected: this.selected || "0",
 | 
				
			||||||
@@ -476,27 +482,27 @@ export class HaTabsSubpageDataTable extends LitElement {
 | 
				
			|||||||
        ${Object.entries(this.columns).map(([id, column]) =>
 | 
					        ${Object.entries(this.columns).map(([id, column]) =>
 | 
				
			||||||
          column.groupable
 | 
					          column.groupable
 | 
				
			||||||
            ? html`
 | 
					            ? html`
 | 
				
			||||||
                <ha-menu-item
 | 
					                <ha-md-menu-item
 | 
				
			||||||
                  .value=${id}
 | 
					                  .value=${id}
 | 
				
			||||||
                  @click=${this._handleGroupBy}
 | 
					                  @click=${this._handleGroupBy}
 | 
				
			||||||
                  .selected=${id === this._groupColumn}
 | 
					                  .selected=${id === this._groupColumn}
 | 
				
			||||||
                  class=${classMap({ selected: id === this._groupColumn })}
 | 
					                  class=${classMap({ selected: id === this._groupColumn })}
 | 
				
			||||||
                >
 | 
					                >
 | 
				
			||||||
                  ${column.title || column.label}
 | 
					                  ${column.title || column.label}
 | 
				
			||||||
                </ha-menu-item>
 | 
					                </ha-md-menu-item>
 | 
				
			||||||
              `
 | 
					              `
 | 
				
			||||||
            : nothing
 | 
					            : nothing
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
        <ha-menu-item
 | 
					        <ha-md-menu-item
 | 
				
			||||||
          .value=${undefined}
 | 
					          .value=${undefined}
 | 
				
			||||||
          @click=${this._handleGroupBy}
 | 
					          @click=${this._handleGroupBy}
 | 
				
			||||||
          .selected=${this._groupColumn === undefined}
 | 
					          .selected=${this._groupColumn === undefined}
 | 
				
			||||||
          class=${classMap({ selected: this._groupColumn === undefined })}
 | 
					          class=${classMap({ selected: this._groupColumn === undefined })}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          ${localize("ui.components.subpage-data-table.dont_group_by")}
 | 
					          ${localize("ui.components.subpage-data-table.dont_group_by")}
 | 
				
			||||||
        </ha-menu-item>
 | 
					        </ha-md-menu-item>
 | 
				
			||||||
        <md-divider role="separator" tabindex="-1"></md-divider>
 | 
					        <md-divider role="separator" tabindex="-1"></md-divider>
 | 
				
			||||||
        <ha-menu-item
 | 
					        <ha-md-menu-item
 | 
				
			||||||
          @click=${this._collapseAllGroups}
 | 
					          @click=${this._collapseAllGroups}
 | 
				
			||||||
          .disabled=${this._groupColumn === undefined}
 | 
					          .disabled=${this._groupColumn === undefined}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
@@ -505,8 +511,8 @@ export class HaTabsSubpageDataTable extends LitElement {
 | 
				
			|||||||
            .path=${mdiUnfoldLessHorizontal}
 | 
					            .path=${mdiUnfoldLessHorizontal}
 | 
				
			||||||
          ></ha-svg-icon>
 | 
					          ></ha-svg-icon>
 | 
				
			||||||
          ${localize("ui.components.subpage-data-table.collapse_all_groups")}
 | 
					          ${localize("ui.components.subpage-data-table.collapse_all_groups")}
 | 
				
			||||||
        </ha-menu-item>
 | 
					        </ha-md-menu-item>
 | 
				
			||||||
        <ha-menu-item
 | 
					        <ha-md-menu-item
 | 
				
			||||||
          @click=${this._expandAllGroups}
 | 
					          @click=${this._expandAllGroups}
 | 
				
			||||||
          .disabled=${this._groupColumn === undefined}
 | 
					          .disabled=${this._groupColumn === undefined}
 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
@@ -515,13 +521,13 @@ export class HaTabsSubpageDataTable extends LitElement {
 | 
				
			|||||||
            .path=${mdiUnfoldMoreHorizontal}
 | 
					            .path=${mdiUnfoldMoreHorizontal}
 | 
				
			||||||
          ></ha-svg-icon>
 | 
					          ></ha-svg-icon>
 | 
				
			||||||
          ${localize("ui.components.subpage-data-table.expand_all_groups")}
 | 
					          ${localize("ui.components.subpage-data-table.expand_all_groups")}
 | 
				
			||||||
        </ha-menu-item>
 | 
					        </ha-md-menu-item>
 | 
				
			||||||
      </ha-menu>
 | 
					      </ha-menu>
 | 
				
			||||||
      <ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
 | 
					      <ha-menu anchor="sort-by-anchor" id="sort-by-menu" positioning="fixed">
 | 
				
			||||||
        ${Object.entries(this.columns).map(([id, column]) =>
 | 
					        ${Object.entries(this.columns).map(([id, column]) =>
 | 
				
			||||||
          column.sortable
 | 
					          column.sortable
 | 
				
			||||||
            ? html`
 | 
					            ? html`
 | 
				
			||||||
                <ha-menu-item
 | 
					                <ha-md-menu-item
 | 
				
			||||||
                  .value=${id}
 | 
					                  .value=${id}
 | 
				
			||||||
                  @click=${this._handleSortBy}
 | 
					                  @click=${this._handleSortBy}
 | 
				
			||||||
                  keep-open
 | 
					                  keep-open
 | 
				
			||||||
@@ -539,7 +545,7 @@ export class HaTabsSubpageDataTable extends LitElement {
 | 
				
			|||||||
                      `
 | 
					                      `
 | 
				
			||||||
                    : nothing}
 | 
					                    : nothing}
 | 
				
			||||||
                  ${column.title || column.label}
 | 
					                  ${column.title || column.label}
 | 
				
			||||||
                </ha-menu-item>
 | 
					                </ha-md-menu-item>
 | 
				
			||||||
              `
 | 
					              `
 | 
				
			||||||
            : nothing
 | 
					            : nothing
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
@@ -893,7 +899,7 @@ export class HaTabsSubpageDataTable extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      #sort-by-anchor,
 | 
					      #sort-by-anchor,
 | 
				
			||||||
      #group-by-anchor,
 | 
					      #group-by-anchor,
 | 
				
			||||||
      ha-button-menu-new ha-assist-chip {
 | 
					      ha-md-button-menu ha-assist-chip {
 | 
				
			||||||
        --md-assist-chip-trailing-space: 8px;
 | 
					        --md-assist-chip-trailing-space: 8px;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import "@material/mwc-button/mwc-button";
 | 
					 | 
				
			||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
					import { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  CSSResultGroup,
 | 
					  CSSResultGroup,
 | 
				
			||||||
@@ -13,6 +12,7 @@ import { isComponentLoaded } from "../common/config/is_component_loaded";
 | 
				
			|||||||
import { fireEvent } from "../common/dom/fire_event";
 | 
					import { fireEvent } from "../common/dom/fire_event";
 | 
				
			||||||
import { stringCompare } from "../common/string/compare";
 | 
					import { stringCompare } from "../common/string/compare";
 | 
				
			||||||
import { LocalizeFunc } from "../common/translations/localize";
 | 
					import { LocalizeFunc } from "../common/translations/localize";
 | 
				
			||||||
 | 
					import "../components/ha-button";
 | 
				
			||||||
import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
 | 
					import { ConfigEntry, subscribeConfigEntries } from "../data/config_entries";
 | 
				
			||||||
import { subscribeConfigFlowInProgress } from "../data/config_flow";
 | 
					import { subscribeConfigFlowInProgress } from "../data/config_flow";
 | 
				
			||||||
import { domainToName } from "../data/integration";
 | 
					import { domainToName } from "../data/integration";
 | 
				
			||||||
@@ -117,6 +117,30 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const foundIntegrations = domains.length;
 | 
					    const foundIntegrations = domains.length;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    // there is a possibility that the user has no integrations
 | 
				
			||||||
 | 
					    if (foundIntegrations === 0) {
 | 
				
			||||||
 | 
					      return html`
 | 
				
			||||||
 | 
					        <div class="all-set-icon">🎉</div>
 | 
				
			||||||
 | 
					        <h1>
 | 
				
			||||||
 | 
					          ${this.onboardingLocalize(
 | 
				
			||||||
 | 
					            "ui.panel.page-onboarding.integration.all_set"
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </h1>
 | 
				
			||||||
 | 
					        <p>
 | 
				
			||||||
 | 
					          ${this.onboardingLocalize(
 | 
				
			||||||
 | 
					            "ui.panel.page-onboarding.integration.lets_start"
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					        </p>
 | 
				
			||||||
 | 
					        <div class="footer">
 | 
				
			||||||
 | 
					          <ha-button unelevated @click=${this._finish}>
 | 
				
			||||||
 | 
					            ${this.onboardingLocalize(
 | 
				
			||||||
 | 
					              "ui.panel.page-onboarding.integration.finish"
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </ha-button>
 | 
				
			||||||
 | 
					        </div>
 | 
				
			||||||
 | 
					      `;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (domains.length > 12) {
 | 
					    if (domains.length > 12) {
 | 
				
			||||||
      domains = domains.slice(0, 11);
 | 
					      domains = domains.slice(0, 11);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -149,11 +173,11 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          : nothing}
 | 
					          : nothing}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      <div class="footer">
 | 
					      <div class="footer">
 | 
				
			||||||
        <mwc-button unelevated @click=${this._finish}>
 | 
					        <ha-button unelevated @click=${this._finish}>
 | 
				
			||||||
          ${this.onboardingLocalize(
 | 
					          ${this.onboardingLocalize(
 | 
				
			||||||
            "ui.panel.page-onboarding.integration.finish"
 | 
					            "ui.panel.page-onboarding.integration.finish"
 | 
				
			||||||
          )}
 | 
					          )}
 | 
				
			||||||
        </mwc-button>
 | 
					        </ha-button>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -193,6 +217,10 @@ class OnboardingIntegrations extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          align-items: center;
 | 
					          align-items: center;
 | 
				
			||||||
          height: 100%;
 | 
					          height: 100%;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					        .all-set-icon {
 | 
				
			||||||
 | 
					          font-size: 64px;
 | 
				
			||||||
 | 
					          text-align: center;
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,6 @@ import {
 | 
				
			|||||||
  mdiPencil,
 | 
					  mdiPencil,
 | 
				
			||||||
  mdiPlus,
 | 
					  mdiPlus,
 | 
				
			||||||
} from "@mdi/js";
 | 
					} from "@mdi/js";
 | 
				
			||||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
					 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  CSSResultGroup,
 | 
					  CSSResultGroup,
 | 
				
			||||||
  LitElement,
 | 
					  LitElement,
 | 
				
			||||||
@@ -15,15 +14,15 @@ import {
 | 
				
			|||||||
  html,
 | 
					  html,
 | 
				
			||||||
  nothing,
 | 
					  nothing,
 | 
				
			||||||
} from "lit";
 | 
					} from "lit";
 | 
				
			||||||
import { customElement, property, state } from "lit/decorators";
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
import { styleMap } from "lit/directives/style-map";
 | 
					import { styleMap } from "lit/directives/style-map";
 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
import { formatListWithAnds } from "../../../common/string/format-list";
 | 
					import { formatListWithAnds } from "../../../common/string/format-list";
 | 
				
			||||||
import "../../../components/ha-fab";
 | 
					import "../../../components/ha-fab";
 | 
				
			||||||
import "../../../components/ha-floor-icon";
 | 
					import "../../../components/ha-floor-icon";
 | 
				
			||||||
import "../../../components/ha-icon-button";
 | 
					import "../../../components/ha-icon-button";
 | 
				
			||||||
import "../../../components/ha-svg-icon";
 | 
					 | 
				
			||||||
import "../../../components/ha-sortable";
 | 
					import "../../../components/ha-sortable";
 | 
				
			||||||
 | 
					import "../../../components/ha-svg-icon";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  AreaRegistryEntry,
 | 
					  AreaRegistryEntry,
 | 
				
			||||||
  createAreaRegistryEntry,
 | 
					  createAreaRegistryEntry,
 | 
				
			||||||
@@ -34,7 +33,6 @@ import {
 | 
				
			|||||||
  createFloorRegistryEntry,
 | 
					  createFloorRegistryEntry,
 | 
				
			||||||
  deleteFloorRegistryEntry,
 | 
					  deleteFloorRegistryEntry,
 | 
				
			||||||
  getFloorAreaLookup,
 | 
					  getFloorAreaLookup,
 | 
				
			||||||
  subscribeFloorRegistry,
 | 
					 | 
				
			||||||
  updateFloorRegistryEntry,
 | 
					  updateFloorRegistryEntry,
 | 
				
			||||||
} from "../../../data/floor_registry";
 | 
					} from "../../../data/floor_registry";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -42,7 +40,6 @@ import {
 | 
				
			|||||||
  showConfirmationDialog,
 | 
					  showConfirmationDialog,
 | 
				
			||||||
} from "../../../dialogs/generic/show-dialog-box";
 | 
					} from "../../../dialogs/generic/show-dialog-box";
 | 
				
			||||||
import "../../../layouts/hass-tabs-subpage";
 | 
					import "../../../layouts/hass-tabs-subpage";
 | 
				
			||||||
import { SubscribeMixin } from "../../../mixins/subscribe-mixin";
 | 
					 | 
				
			||||||
import { HomeAssistant, Route } from "../../../types";
 | 
					import { HomeAssistant, Route } from "../../../types";
 | 
				
			||||||
import "../ha-config-section";
 | 
					import "../ha-config-section";
 | 
				
			||||||
import { configSections } from "../ha-panel-config";
 | 
					import { configSections } from "../ha-panel-config";
 | 
				
			||||||
@@ -57,7 +54,7 @@ const UNASSIGNED_PATH = ["__unassigned__"];
 | 
				
			|||||||
const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
 | 
					const SORT_OPTIONS = { sort: false, delay: 500, delayOnTouchOnly: true };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-config-areas-dashboard")
 | 
					@customElement("ha-config-areas-dashboard")
 | 
				
			||||||
export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
 | 
					export class HaConfigAreasDashboard extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ type: Boolean }) public isWide = false;
 | 
					  @property({ type: Boolean }) public isWide = false;
 | 
				
			||||||
@@ -66,14 +63,12 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public route!: Route;
 | 
					  @property({ attribute: false }) public route!: Route;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _processAreas = memoizeOne(
 | 
					  private _processAreas = memoizeOne(
 | 
				
			||||||
    (
 | 
					    (
 | 
				
			||||||
      areas: HomeAssistant["areas"],
 | 
					      areas: HomeAssistant["areas"],
 | 
				
			||||||
      devices: HomeAssistant["devices"],
 | 
					      devices: HomeAssistant["devices"],
 | 
				
			||||||
      entities: HomeAssistant["entities"],
 | 
					      entities: HomeAssistant["entities"],
 | 
				
			||||||
      floors: FloorRegistryEntry[]
 | 
					      floors: HomeAssistant["floors"]
 | 
				
			||||||
    ) => {
 | 
					    ) => {
 | 
				
			||||||
      const processArea = (area: AreaRegistryEntry) => {
 | 
					      const processArea = (area: AreaRegistryEntry) => {
 | 
				
			||||||
        let noDevicesInArea = 0;
 | 
					        let noDevicesInArea = 0;
 | 
				
			||||||
@@ -109,7 +104,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
        (area) => !area.floor_id || !floorAreaLookup[area.floor_id]
 | 
					        (area) => !area.floor_id || !floorAreaLookup[area.floor_id]
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        floors: floors.map((floor) => ({
 | 
					        floors: Object.values(floors).map((floor) => ({
 | 
				
			||||||
          ...floor,
 | 
					          ...floor,
 | 
				
			||||||
          areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
 | 
					          areas: (floorAreaLookup[floor.floor_id] || []).map(processArea),
 | 
				
			||||||
        })),
 | 
					        })),
 | 
				
			||||||
@@ -118,26 +113,18 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
					 | 
				
			||||||
    return [
 | 
					 | 
				
			||||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
					 | 
				
			||||||
        this._floors = floors;
 | 
					 | 
				
			||||||
      }),
 | 
					 | 
				
			||||||
    ];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render(): TemplateResult {
 | 
					  protected render(): TemplateResult {
 | 
				
			||||||
    const areasAndFloors =
 | 
					    const areasAndFloors =
 | 
				
			||||||
      !this.hass.areas ||
 | 
					      !this.hass.areas ||
 | 
				
			||||||
      !this.hass.devices ||
 | 
					      !this.hass.devices ||
 | 
				
			||||||
      !this.hass.entities ||
 | 
					      !this.hass.entities ||
 | 
				
			||||||
      !this._floors
 | 
					      !this.hass.floors
 | 
				
			||||||
        ? undefined
 | 
					        ? undefined
 | 
				
			||||||
        : this._processAreas(
 | 
					        : this._processAreas(
 | 
				
			||||||
            this.hass.areas,
 | 
					            this.hass.areas,
 | 
				
			||||||
            this.hass.devices,
 | 
					            this.hass.devices,
 | 
				
			||||||
            this.hass.entities,
 | 
					            this.hass.entities,
 | 
				
			||||||
            this._floors
 | 
					            this.hass.floors
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
@@ -327,7 +314,7 @@ export class HaConfigAreasDashboard extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
      this.hass.areas,
 | 
					      this.hass.areas,
 | 
				
			||||||
      this.hass.devices,
 | 
					      this.hass.devices,
 | 
				
			||||||
      this.hass.entities,
 | 
					      this.hass.entities,
 | 
				
			||||||
      this._floors!
 | 
					      this.hass.floors
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    let area: AreaRegistryEntry;
 | 
					    let area: AreaRegistryEntry;
 | 
				
			||||||
    if (ev.detail.oldPath === UNASSIGNED_PATH) {
 | 
					    if (ev.detail.oldPath === UNASSIGNED_PATH) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -86,7 +86,7 @@ export class HaChooseAction extends LitElement implements ActionElement {
 | 
				
			|||||||
    this._unsubMql = undefined;
 | 
					    this._unsubMql = undefined;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static get defaultConfig() {
 | 
					  public static get defaultConfig(): ChooseAction {
 | 
				
			||||||
    return { choose: [{ conditions: [], sequence: [] }] };
 | 
					    return { choose: [{ conditions: [], sequence: [] }] };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -20,7 +20,7 @@ export class HaConditionAction extends LitElement implements ActionElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public action!: Condition;
 | 
					  @property({ attribute: false }) public action!: Condition;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static get defaultConfig() {
 | 
					  public static get defaultConfig(): Omit<Condition, "state" | "entity_id"> {
 | 
				
			||||||
    return { condition: "state" };
 | 
					    return { condition: "state" };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -87,13 +87,12 @@ export class HaConditionAction extends LitElement implements ActionElement {
 | 
				
			|||||||
    const elClass = customElements.get(
 | 
					    const elClass = customElements.get(
 | 
				
			||||||
      `ha-automation-condition-${type}`
 | 
					      `ha-automation-condition-${type}`
 | 
				
			||||||
    ) as CustomElementConstructor & {
 | 
					    ) as CustomElementConstructor & {
 | 
				
			||||||
      defaultConfig: Omit<Condition, "condition">;
 | 
					      defaultConfig: Condition;
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (type !== this.action.condition) {
 | 
					    if (type !== this.action.condition) {
 | 
				
			||||||
      fireEvent(this, "value-changed", {
 | 
					      fireEvent(this, "value-changed", {
 | 
				
			||||||
        value: {
 | 
					        value: {
 | 
				
			||||||
          condition: type,
 | 
					 | 
				
			||||||
          ...elClass.defaultConfig,
 | 
					          ...elClass.defaultConfig,
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
      });
 | 
					      });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -19,7 +19,7 @@ export class HaDelayAction extends LitElement implements ActionElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _timeData?: HaDurationData;
 | 
					  @state() private _timeData?: HaDurationData;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static get defaultConfig() {
 | 
					  public static get defaultConfig(): DelayAction {
 | 
				
			||||||
    return { delay: "" };
 | 
					    return { delay: "" };
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,7 +36,7 @@ export class HaDeviceAction extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _origAction?: DeviceAction;
 | 
					  private _origAction?: DeviceAction;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static get defaultConfig() {
 | 
					  public static get defaultConfig(): DeviceAction {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      device_id: "",
 | 
					      device_id: "",
 | 
				
			||||||
      domain: "",
 | 
					      domain: "",
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -21,7 +21,7 @@ export class HaIfAction extends LitElement implements ActionElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _showElse = false;
 | 
					  @state() private _showElse = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public static get defaultConfig() {
 | 
					  public static get defaultConfig(): IfAction {
 | 
				
			||||||
    return {
 | 
					    return {
 | 
				
			||||||
      if: [],
 | 
					      if: [],
 | 
				
			||||||
      then: [],
 | 
					      then: [],
 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user