mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-11-04 00:19:47 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			iframe-car
			...
			triggers-d
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2c87327e34 | ||
| 
						 | 
					9b9078229a | 
							
								
								
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							@@ -26,7 +26,7 @@ jobs:
 | 
				
			|||||||
          ref: dev
 | 
					          ref: dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -61,7 +61,7 @@ jobs:
 | 
				
			|||||||
          ref: master
 | 
					          ref: master
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										12
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							@@ -26,7 +26,7 @@ jobs:
 | 
				
			|||||||
      - name: Check out files from GitHub
 | 
					      - name: Check out files from GitHub
 | 
				
			||||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
					        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -60,7 +60,7 @@ jobs:
 | 
				
			|||||||
      - name: Check out files from GitHub
 | 
					      - name: Check out files from GitHub
 | 
				
			||||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
					        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -78,7 +78,7 @@ jobs:
 | 
				
			|||||||
      - name: Check out files from GitHub
 | 
					      - name: Check out files from GitHub
 | 
				
			||||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
					        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
					        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: frontend-bundle-stats
 | 
					          name: frontend-bundle-stats
 | 
				
			||||||
          path: build/stats/*.json
 | 
					          path: build/stats/*.json
 | 
				
			||||||
@@ -102,7 +102,7 @@ jobs:
 | 
				
			|||||||
      - name: Check out files from GitHub
 | 
					      - name: Check out files from GitHub
 | 
				
			||||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
					        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
					        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: supervisor-bundle-stats
 | 
					          name: supervisor-bundle-stats
 | 
				
			||||||
          path: build/stats/*.json
 | 
					          path: build/stats/*.json
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							@@ -36,14 +36,14 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      # Initializes the CodeQL tools for scanning.
 | 
					      # Initializes the CodeQL tools for scanning.
 | 
				
			||||||
      - name: Initialize CodeQL
 | 
					      - name: Initialize CodeQL
 | 
				
			||||||
        uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
 | 
					        uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          languages: ${{ matrix.language }}
 | 
					          languages: ${{ matrix.language }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
 | 
					      # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java).
 | 
				
			||||||
      # If this step fails, then you should remove it and run the build manually (see below)
 | 
					      # If this step fails, then you should remove it and run the build manually (see below)
 | 
				
			||||||
      - name: Autobuild
 | 
					      - name: Autobuild
 | 
				
			||||||
        uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
 | 
					        uses: github/codeql-action/autobuild@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      # ℹ️ Command-line programs to run using the OS shell.
 | 
					      # ℹ️ Command-line programs to run using the OS shell.
 | 
				
			||||||
      # 📚 https://git.io/JvXDl
 | 
					      # 📚 https://git.io/JvXDl
 | 
				
			||||||
@@ -57,4 +57,4 @@ jobs:
 | 
				
			|||||||
      #   make release
 | 
					      #   make release
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Perform CodeQL Analysis
 | 
					      - name: Perform CodeQL Analysis
 | 
				
			||||||
        uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0
 | 
					        uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							@@ -27,7 +27,7 @@ jobs:
 | 
				
			|||||||
          ref: dev
 | 
					          ref: dev
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -62,7 +62,7 @@ jobs:
 | 
				
			|||||||
          ref: master
 | 
					          ref: master
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							@@ -19,7 +19,7 @@ jobs:
 | 
				
			|||||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
					        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							@@ -24,7 +24,7 @@ jobs:
 | 
				
			|||||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
					        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							@@ -28,7 +28,7 @@ jobs:
 | 
				
			|||||||
          python-version: ${{ env.PYTHON_VERSION }}
 | 
					          python-version: ${{ env.PYTHON_VERSION }}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
					        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
				
			||||||
        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@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
					        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          name: translations
 | 
					          name: translations
 | 
				
			||||||
          path: translations.tar.gz
 | 
					          path: translations.tar.gz
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/relative-ci.yaml
									
									
									
									
										vendored
									
									
								
							@@ -17,7 +17,7 @@ jobs:
 | 
				
			|||||||
    runs-on: ubuntu-latest
 | 
					    runs-on: ubuntu-latest
 | 
				
			||||||
    steps:
 | 
					    steps:
 | 
				
			||||||
      - name: Send bundle stats and build information to RelativeCI
 | 
					      - name: Send bundle stats and build information to RelativeCI
 | 
				
			||||||
        uses: relative-ci/agent-action@8504826a02078b05756e4c07e380023cc2c4274a # v3.1.0
 | 
					        uses: relative-ci/agent-action@1707825cbfcc7452b2913d273414705415ae64d4 # v3.0.1
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
 | 
					          key: ${{ secrets[format('RELATIVE_CI_KEY_{0}_{1}', matrix.bundle, matrix.build)] }}
 | 
				
			||||||
          token: ${{ github.token }}
 | 
					          token: ${{ github.token }}
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										8
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										8
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							@@ -34,7 +34,7 @@ jobs:
 | 
				
			|||||||
        uses: home-assistant/actions/helpers/verify-version@master
 | 
					        uses: home-assistant/actions/helpers/verify-version@master
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -75,7 +75,7 @@ jobs:
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
      # home-assistant/wheels doesn't support SHA pinning
 | 
					      # home-assistant/wheels doesn't support SHA pinning
 | 
				
			||||||
      - name: Build wheels
 | 
					      - name: Build wheels
 | 
				
			||||||
        uses: home-assistant/wheels@2025.10.0
 | 
					        uses: home-assistant/wheels@2025.09.1
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          abi: cp313
 | 
					          abi: cp313
 | 
				
			||||||
          tag: musllinux_1_2
 | 
					          tag: musllinux_1_2
 | 
				
			||||||
@@ -93,7 +93,7 @@ jobs:
 | 
				
			|||||||
      - name: Checkout the repository
 | 
					      - name: Checkout the repository
 | 
				
			||||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
					        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
@@ -122,7 +122,7 @@ jobs:
 | 
				
			|||||||
      - name: Checkout the repository
 | 
					      - name: Checkout the repository
 | 
				
			||||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
					        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
				
			||||||
      - name: Setup Node
 | 
					      - name: Setup Node
 | 
				
			||||||
        uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0
 | 
					        uses: actions/setup-node@a0853c24544627f65ddf259abe73b1d18a591444 # v5.0.0
 | 
				
			||||||
        with:
 | 
					        with:
 | 
				
			||||||
          node-version-file: ".nvmrc"
 | 
					          node-version-file: ".nvmrc"
 | 
				
			||||||
          cache: yarn
 | 
					          cache: yarn
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -16,9 +16,9 @@ import {
 | 
				
			|||||||
} from "../../../../src/common/auth/token_storage";
 | 
					} from "../../../../src/common/auth/token_storage";
 | 
				
			||||||
import { atLeastVersion } from "../../../../src/common/config/version";
 | 
					import { atLeastVersion } from "../../../../src/common/config/version";
 | 
				
			||||||
import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
 | 
					import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute";
 | 
				
			||||||
import "../../../../src/components/ha-button";
 | 
					 | 
				
			||||||
import "../../../../src/components/ha-icon";
 | 
					import "../../../../src/components/ha-icon";
 | 
				
			||||||
import "../../../../src/components/ha-list";
 | 
					import "../../../../src/components/ha-list";
 | 
				
			||||||
 | 
					import "../../../../src/components/ha-button";
 | 
				
			||||||
import "../../../../src/components/ha-list-item";
 | 
					import "../../../../src/components/ha-list-item";
 | 
				
			||||||
import "../../../../src/components/ha-svg-icon";
 | 
					import "../../../../src/components/ha-svg-icon";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
@@ -28,6 +28,7 @@ import {
 | 
				
			|||||||
import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
 | 
					import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types";
 | 
				
			||||||
import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
 | 
					import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view";
 | 
				
			||||||
import "../../../../src/layouts/hass-loading-screen";
 | 
					import "../../../../src/layouts/hass-loading-screen";
 | 
				
			||||||
 | 
					import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config";
 | 
				
			||||||
import "./hc-layout";
 | 
					import "./hc-layout";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("hc-cast")
 | 
					@customElement("hc-cast")
 | 
				
			||||||
@@ -95,9 +96,7 @@ class HcCast extends LitElement {
 | 
				
			|||||||
                <ha-list @action=${this._handlePickView} activatable>
 | 
					                <ha-list @action=${this._handlePickView} activatable>
 | 
				
			||||||
                  ${(
 | 
					                  ${(
 | 
				
			||||||
                    this.lovelaceViews ?? [
 | 
					                    this.lovelaceViews ?? [
 | 
				
			||||||
                      {
 | 
					                      generateDefaultViewConfig({}, {}, {}, {}, () => ""),
 | 
				
			||||||
                        title: "Home",
 | 
					 | 
				
			||||||
                      },
 | 
					 | 
				
			||||||
                    ]
 | 
					                    ]
 | 
				
			||||||
                  ).map(
 | 
					                  ).map(
 | 
				
			||||||
                    (view, idx) => html`
 | 
					                    (view, idx) => html`
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow.
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
# Material Design 3
 | 
					# Material Design 3
 | 
				
			||||||
 | 
					
 | 
				
			||||||
Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
 | 
					Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview).
 | 
				
			||||||
 | 
					
 | 
				
			||||||
# Guidelines
 | 
					# Guidelines
 | 
				
			||||||
 | 
					
 | 
				
			||||||
## Design
 | 
					## Design
 | 
				
			||||||
 | 
					
 | 
				
			||||||
- Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead.
 | 
					- Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead.
 | 
				
			||||||
- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines.
 | 
					- The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness.
 | 
				
			||||||
- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
 | 
					- Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake.
 | 
				
			||||||
- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
 | 
					- Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu.
 | 
				
			||||||
- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
 | 
					- The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom.
 | 
				
			||||||
@@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
- A best practice is to always use a title, even if it is optional by Material guidelines.
 | 
					- A best practice is to always use a title, even if it is optional by Material guidelines.
 | 
				
			||||||
- People mainly read the title and a button. Put the most important information in those two.
 | 
					- People mainly read the title and a button. Put the most important information in those two.
 | 
				
			||||||
- Try to avoid user generated content in the title, this could make the title unreadably long.
 | 
					- Try to avoid user generated content in the title, this could make the title unreadable long.
 | 
				
			||||||
- If users become unsure, they read the description. Make sure this explains what will happen.
 | 
					- If users become unsure, they read the description. Make sure this explains what will happen.
 | 
				
			||||||
- Strive for minimalism.
 | 
					- Strive for minimalism.
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										55
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										55
									
								
								package.json
									
									
									
									
									
								
							@@ -28,13 +28,13 @@
 | 
				
			|||||||
  "dependencies": {
 | 
					  "dependencies": {
 | 
				
			||||||
    "@babel/runtime": "7.28.4",
 | 
					    "@babel/runtime": "7.28.4",
 | 
				
			||||||
    "@braintree/sanitize-url": "7.1.1",
 | 
					    "@braintree/sanitize-url": "7.1.1",
 | 
				
			||||||
    "@codemirror/autocomplete": "6.19.1",
 | 
					    "@codemirror/autocomplete": "6.19.0",
 | 
				
			||||||
    "@codemirror/commands": "6.10.0",
 | 
					    "@codemirror/commands": "6.9.0",
 | 
				
			||||||
    "@codemirror/language": "6.11.3",
 | 
					    "@codemirror/language": "6.11.3",
 | 
				
			||||||
    "@codemirror/legacy-modes": "6.5.2",
 | 
					    "@codemirror/legacy-modes": "6.5.2",
 | 
				
			||||||
    "@codemirror/search": "6.5.11",
 | 
					    "@codemirror/search": "6.5.11",
 | 
				
			||||||
    "@codemirror/state": "6.5.2",
 | 
					    "@codemirror/state": "6.5.2",
 | 
				
			||||||
    "@codemirror/view": "6.38.6",
 | 
					    "@codemirror/view": "6.38.5",
 | 
				
			||||||
    "@date-fns/tz": "1.4.1",
 | 
					    "@date-fns/tz": "1.4.1",
 | 
				
			||||||
    "@egjs/hammerjs": "2.0.17",
 | 
					    "@egjs/hammerjs": "2.0.17",
 | 
				
			||||||
    "@formatjs/intl-datetimeformat": "6.18.2",
 | 
					    "@formatjs/intl-datetimeformat": "6.18.2",
 | 
				
			||||||
@@ -52,8 +52,8 @@
 | 
				
			|||||||
    "@fullcalendar/list": "6.1.19",
 | 
					    "@fullcalendar/list": "6.1.19",
 | 
				
			||||||
    "@fullcalendar/luxon3": "6.1.19",
 | 
					    "@fullcalendar/luxon3": "6.1.19",
 | 
				
			||||||
    "@fullcalendar/timegrid": "6.1.19",
 | 
					    "@fullcalendar/timegrid": "6.1.19",
 | 
				
			||||||
    "@home-assistant/webawesome": "3.0.0-beta.6.ha.6",
 | 
					    "@home-assistant/webawesome": "3.0.0-beta.6.ha.4",
 | 
				
			||||||
    "@lezer/highlight": "1.2.3",
 | 
					    "@lezer/highlight": "1.2.1",
 | 
				
			||||||
    "@lit-labs/motion": "1.0.9",
 | 
					    "@lit-labs/motion": "1.0.9",
 | 
				
			||||||
    "@lit-labs/observers": "2.0.6",
 | 
					    "@lit-labs/observers": "2.0.6",
 | 
				
			||||||
    "@lit-labs/virtualizer": "2.1.1",
 | 
					    "@lit-labs/virtualizer": "2.1.1",
 | 
				
			||||||
@@ -81,7 +81,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.4.1",
 | 
					    "@material/web": "2.4.0",
 | 
				
			||||||
    "@mdi/js": "7.4.47",
 | 
					    "@mdi/js": "7.4.47",
 | 
				
			||||||
    "@mdi/svg": "7.4.47",
 | 
					    "@mdi/svg": "7.4.47",
 | 
				
			||||||
    "@replit/codemirror-indentation-markers": "6.5.3",
 | 
					    "@replit/codemirror-indentation-markers": "6.5.3",
 | 
				
			||||||
@@ -89,8 +89,8 @@
 | 
				
			|||||||
    "@thomasloven/round-slider": "0.6.0",
 | 
					    "@thomasloven/round-slider": "0.6.0",
 | 
				
			||||||
    "@tsparticles/engine": "3.9.1",
 | 
					    "@tsparticles/engine": "3.9.1",
 | 
				
			||||||
    "@tsparticles/preset-links": "3.2.0",
 | 
					    "@tsparticles/preset-links": "3.2.0",
 | 
				
			||||||
    "@vaadin/combo-box": "24.9.4",
 | 
					    "@vaadin/combo-box": "24.9.2",
 | 
				
			||||||
    "@vaadin/vaadin-themable-mixin": "24.9.4",
 | 
					    "@vaadin/vaadin-themable-mixin": "24.9.2",
 | 
				
			||||||
    "@vibrant/color": "4.0.0",
 | 
					    "@vibrant/color": "4.0.0",
 | 
				
			||||||
    "@vue/web-component-wrapper": "1.3.0",
 | 
					    "@vue/web-component-wrapper": "1.3.0",
 | 
				
			||||||
    "@webcomponents/scoped-custom-element-registry": "0.0.10",
 | 
					    "@webcomponents/scoped-custom-element-registry": "0.0.10",
 | 
				
			||||||
@@ -122,7 +122,7 @@
 | 
				
			|||||||
    "lit": "3.3.1",
 | 
					    "lit": "3.3.1",
 | 
				
			||||||
    "lit-html": "3.3.1",
 | 
					    "lit-html": "3.3.1",
 | 
				
			||||||
    "luxon": "3.7.2",
 | 
					    "luxon": "3.7.2",
 | 
				
			||||||
    "marked": "16.4.1",
 | 
					    "marked": "16.4.0",
 | 
				
			||||||
    "memoize-one": "6.0.0",
 | 
					    "memoize-one": "6.0.0",
 | 
				
			||||||
    "node-vibrant": "4.0.3",
 | 
					    "node-vibrant": "4.0.3",
 | 
				
			||||||
    "object-hash": "3.0.0",
 | 
					    "object-hash": "3.0.0",
 | 
				
			||||||
@@ -148,16 +148,16 @@
 | 
				
			|||||||
    "xss": "1.0.15"
 | 
					    "xss": "1.0.15"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "devDependencies": {
 | 
					  "devDependencies": {
 | 
				
			||||||
    "@babel/core": "7.28.5",
 | 
					    "@babel/core": "7.28.4",
 | 
				
			||||||
    "@babel/helper-define-polyfill-provider": "0.6.5",
 | 
					    "@babel/helper-define-polyfill-provider": "0.6.5",
 | 
				
			||||||
    "@babel/plugin-transform-runtime": "7.28.5",
 | 
					    "@babel/plugin-transform-runtime": "7.28.3",
 | 
				
			||||||
    "@babel/preset-env": "7.28.5",
 | 
					    "@babel/preset-env": "7.28.3",
 | 
				
			||||||
    "@bundle-stats/plugin-webpack-filter": "4.21.5",
 | 
					    "@bundle-stats/plugin-webpack-filter": "4.21.5",
 | 
				
			||||||
    "@lokalise/node-api": "15.3.1",
 | 
					    "@lokalise/node-api": "15.3.0",
 | 
				
			||||||
    "@octokit/auth-oauth-device": "8.0.2",
 | 
					    "@octokit/auth-oauth-device": "8.0.2",
 | 
				
			||||||
    "@octokit/plugin-retry": "8.0.2",
 | 
					    "@octokit/plugin-retry": "8.0.2",
 | 
				
			||||||
    "@octokit/rest": "22.0.0",
 | 
					    "@octokit/rest": "22.0.0",
 | 
				
			||||||
    "@rsdoctor/rspack-plugin": "1.3.6",
 | 
					    "@rsdoctor/rspack-plugin": "1.3.1",
 | 
				
			||||||
    "@rspack/core": "1.5.8",
 | 
					    "@rspack/core": "1.5.8",
 | 
				
			||||||
    "@rspack/dev-server": "1.1.4",
 | 
					    "@rspack/dev-server": "1.1.4",
 | 
				
			||||||
    "@types/babel__plugin-transform-runtime": "7.9.5",
 | 
					    "@types/babel__plugin-transform-runtime": "7.9.5",
 | 
				
			||||||
@@ -167,30 +167,30 @@
 | 
				
			|||||||
    "@types/culori": "4.0.1",
 | 
					    "@types/culori": "4.0.1",
 | 
				
			||||||
    "@types/html-minifier-terser": "7.0.2",
 | 
					    "@types/html-minifier-terser": "7.0.2",
 | 
				
			||||||
    "@types/js-yaml": "4.0.9",
 | 
					    "@types/js-yaml": "4.0.9",
 | 
				
			||||||
    "@types/leaflet": "1.9.21",
 | 
					    "@types/leaflet": "1.9.20",
 | 
				
			||||||
    "@types/leaflet-draw": "1.0.13",
 | 
					    "@types/leaflet-draw": "1.0.13",
 | 
				
			||||||
    "@types/leaflet.markercluster": "1.5.6",
 | 
					    "@types/leaflet.markercluster": "1.5.6",
 | 
				
			||||||
    "@types/lodash.merge": "4.6.9",
 | 
					    "@types/lodash.merge": "4.6.9",
 | 
				
			||||||
    "@types/luxon": "3.7.1",
 | 
					    "@types/luxon": "3.7.1",
 | 
				
			||||||
    "@types/mocha": "10.0.10",
 | 
					    "@types/mocha": "10.0.10",
 | 
				
			||||||
    "@types/qrcode": "1.5.6",
 | 
					    "@types/qrcode": "1.5.5",
 | 
				
			||||||
    "@types/sortablejs": "1.15.9",
 | 
					    "@types/sortablejs": "1.15.8",
 | 
				
			||||||
    "@types/tar": "6.1.13",
 | 
					    "@types/tar": "6.1.13",
 | 
				
			||||||
    "@types/ua-parser-js": "0.7.39",
 | 
					    "@types/ua-parser-js": "0.7.39",
 | 
				
			||||||
    "@types/webspeechapi": "0.0.29",
 | 
					    "@types/webspeechapi": "0.0.29",
 | 
				
			||||||
    "@vitest/coverage-v8": "4.0.4",
 | 
					    "@vitest/coverage-v8": "3.2.4",
 | 
				
			||||||
    "babel-loader": "10.0.0",
 | 
					    "babel-loader": "10.0.0",
 | 
				
			||||||
    "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",
 | 
				
			||||||
    "del": "8.0.1",
 | 
					    "del": "8.0.1",
 | 
				
			||||||
    "eslint": "9.38.0",
 | 
					    "eslint": "9.37.0",
 | 
				
			||||||
    "eslint-config-airbnb-base": "15.0.0",
 | 
					    "eslint-config-airbnb-base": "15.0.0",
 | 
				
			||||||
    "eslint-config-prettier": "10.1.8",
 | 
					    "eslint-config-prettier": "10.1.8",
 | 
				
			||||||
    "eslint-import-resolver-webpack": "0.13.10",
 | 
					    "eslint-import-resolver-webpack": "0.13.10",
 | 
				
			||||||
    "eslint-plugin-import": "2.32.0",
 | 
					    "eslint-plugin-import": "2.32.0",
 | 
				
			||||||
    "eslint-plugin-lit": "2.1.1",
 | 
					    "eslint-plugin-lit": "2.1.1",
 | 
				
			||||||
    "eslint-plugin-lit-a11y": "5.1.1",
 | 
					    "eslint-plugin-lit-a11y": "5.1.1",
 | 
				
			||||||
    "eslint-plugin-unused-imports": "4.3.0",
 | 
					    "eslint-plugin-unused-imports": "4.2.0",
 | 
				
			||||||
    "eslint-plugin-wc": "3.0.2",
 | 
					    "eslint-plugin-wc": "3.0.2",
 | 
				
			||||||
    "fancy-log": "2.0.0",
 | 
					    "fancy-log": "2.0.0",
 | 
				
			||||||
    "fs-extra": "11.3.2",
 | 
					    "fs-extra": "11.3.2",
 | 
				
			||||||
@@ -201,9 +201,9 @@
 | 
				
			|||||||
    "gulp-rename": "2.1.0",
 | 
					    "gulp-rename": "2.1.0",
 | 
				
			||||||
    "html-minifier-terser": "7.2.0",
 | 
					    "html-minifier-terser": "7.2.0",
 | 
				
			||||||
    "husky": "9.1.7",
 | 
					    "husky": "9.1.7",
 | 
				
			||||||
    "jsdom": "27.0.1",
 | 
					    "jsdom": "27.0.0",
 | 
				
			||||||
    "jszip": "3.10.1",
 | 
					    "jszip": "3.10.1",
 | 
				
			||||||
    "lint-staged": "16.2.6",
 | 
					    "lint-staged": "16.2.3",
 | 
				
			||||||
    "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",
 | 
				
			||||||
@@ -213,13 +213,13 @@
 | 
				
			|||||||
    "rspack-manifest-plugin": "5.1.0",
 | 
					    "rspack-manifest-plugin": "5.1.0",
 | 
				
			||||||
    "serve": "14.2.5",
 | 
					    "serve": "14.2.5",
 | 
				
			||||||
    "sinon": "21.0.0",
 | 
					    "sinon": "21.0.0",
 | 
				
			||||||
    "tar": "7.5.2",
 | 
					    "tar": "7.5.1",
 | 
				
			||||||
    "terser-webpack-plugin": "5.3.14",
 | 
					    "terser-webpack-plugin": "5.3.14",
 | 
				
			||||||
    "ts-lit-plugin": "2.0.2",
 | 
					    "ts-lit-plugin": "2.0.2",
 | 
				
			||||||
    "typescript": "5.9.3",
 | 
					    "typescript": "5.9.3",
 | 
				
			||||||
    "typescript-eslint": "8.46.2",
 | 
					    "typescript-eslint": "8.46.0",
 | 
				
			||||||
    "vite-tsconfig-paths": "5.1.4",
 | 
					    "vite-tsconfig-paths": "5.1.4",
 | 
				
			||||||
    "vitest": "4.0.4",
 | 
					    "vitest": "3.2.4",
 | 
				
			||||||
    "webpack-stats-plugin": "1.1.3",
 | 
					    "webpack-stats-plugin": "1.1.3",
 | 
				
			||||||
    "webpackbar": "7.0.0",
 | 
					    "webpackbar": "7.0.0",
 | 
				
			||||||
    "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
 | 
					    "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch"
 | 
				
			||||||
@@ -235,8 +235,5 @@
 | 
				
			|||||||
    "tslib": "2.8.1",
 | 
					    "tslib": "2.8.1",
 | 
				
			||||||
    "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
 | 
					    "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch"
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  "packageManager": "yarn@4.10.3",
 | 
					  "packageManager": "yarn@4.10.3"
 | 
				
			||||||
  "volta": {
 | 
					 | 
				
			||||||
    "node": "22.21.1"
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
[project]
 | 
					[project]
 | 
				
			||||||
name         = "home-assistant-frontend"
 | 
					name         = "home-assistant-frontend"
 | 
				
			||||||
version      = "20251029.0"
 | 
					version      = "20250924.0"
 | 
				
			||||||
license      = "Apache-2.0"
 | 
					license      = "Apache-2.0"
 | 
				
			||||||
license-files = ["LICENSE*"]
 | 
					license-files = ["LICENSE*"]
 | 
				
			||||||
description  = "The Home Assistant frontend"
 | 
					description  = "The Home Assistant frontend"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -9,11 +9,6 @@ import { getEntityContext } from "./context/get_entity_context";
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const DEFAULT_SEPARATOR = " ";
 | 
					const DEFAULT_SEPARATOR = " ";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const DEFAULT_ENTITY_NAME = [
 | 
					 | 
				
			||||||
  { type: "device" },
 | 
					 | 
				
			||||||
  { type: "entity" },
 | 
					 | 
				
			||||||
] satisfies EntityNameItem[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type EntityNameItem =
 | 
					export type EntityNameItem =
 | 
				
			||||||
  | {
 | 
					  | {
 | 
				
			||||||
      type: "entity" | "device" | "area" | "floor";
 | 
					      type: "entity" | "device" | "area" | "floor";
 | 
				
			||||||
@@ -29,14 +24,14 @@ export interface EntityNameOptions {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const computeEntityNameDisplay = (
 | 
					export const computeEntityNameDisplay = (
 | 
				
			||||||
  stateObj: HassEntity,
 | 
					  stateObj: HassEntity,
 | 
				
			||||||
  name: EntityNameItem | EntityNameItem[] | undefined,
 | 
					  name: EntityNameItem | EntityNameItem[],
 | 
				
			||||||
  entities: HomeAssistant["entities"],
 | 
					  entities: HomeAssistant["entities"],
 | 
				
			||||||
  devices: HomeAssistant["devices"],
 | 
					  devices: HomeAssistant["devices"],
 | 
				
			||||||
  areas: HomeAssistant["areas"],
 | 
					  areas: HomeAssistant["areas"],
 | 
				
			||||||
  floors: HomeAssistant["floors"],
 | 
					  floors: HomeAssistant["floors"],
 | 
				
			||||||
  options?: EntityNameOptions
 | 
					  options?: EntityNameOptions
 | 
				
			||||||
) => {
 | 
					) => {
 | 
				
			||||||
  let items = ensureArray(name || DEFAULT_ENTITY_NAME);
 | 
					  let items = ensureArray(name);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  const separator = options?.separator ?? DEFAULT_SEPARATOR;
 | 
					  const separator = options?.separator ?? DEFAULT_SEPARATOR;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,10 +8,10 @@ interface AreaContext {
 | 
				
			|||||||
}
 | 
					}
 | 
				
			||||||
export const getAreaContext = (
 | 
					export const getAreaContext = (
 | 
				
			||||||
  area: AreaRegistryEntry,
 | 
					  area: AreaRegistryEntry,
 | 
				
			||||||
  hassFloors: HomeAssistant["floors"]
 | 
					  hass: HomeAssistant
 | 
				
			||||||
): AreaContext => {
 | 
					): AreaContext => {
 | 
				
			||||||
  const floorId = area.floor_id;
 | 
					  const floorId = area.floor_id;
 | 
				
			||||||
  const floor = floorId ? hassFloors[floorId] : undefined;
 | 
					  const floor = floorId ? hass.floors[floorId] : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return {
 | 
					  return {
 | 
				
			||||||
    area: area,
 | 
					    area: area,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,116 +0,0 @@
 | 
				
			|||||||
export interface SwipeGestureResult {
 | 
					 | 
				
			||||||
  velocity: number;
 | 
					 | 
				
			||||||
  delta: number;
 | 
					 | 
				
			||||||
  isSwipe: boolean;
 | 
					 | 
				
			||||||
  isDownwardSwipe: boolean;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface SwipeGestureConfig {
 | 
					 | 
				
			||||||
  velocitySwipeThreshold?: number;
 | 
					 | 
				
			||||||
  movementTimeThreshold?: number;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const VELOCITY_SWIPE_THRESHOLD = 0.5; // px/ms
 | 
					 | 
				
			||||||
const MOVEMENT_TIME_THRESHOLD = 100; // ms
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
/**
 | 
					 | 
				
			||||||
 * Recognizes swipe gestures and calculates velocity for touch interactions.
 | 
					 | 
				
			||||||
 * Tracks touch movement and provides velocity-based and position-based gesture detection.
 | 
					 | 
				
			||||||
 */
 | 
					 | 
				
			||||||
export class SwipeGestureRecognizer {
 | 
					 | 
				
			||||||
  private _startY = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _delta = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _startTime = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _lastY = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _lastTime = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _velocityThreshold: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _movementTimeThreshold: number;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  constructor(config: SwipeGestureConfig = {}) {
 | 
					 | 
				
			||||||
    this._velocityThreshold =
 | 
					 | 
				
			||||||
      config.velocitySwipeThreshold ?? VELOCITY_SWIPE_THRESHOLD; // px/ms
 | 
					 | 
				
			||||||
    this._movementTimeThreshold =
 | 
					 | 
				
			||||||
      config.movementTimeThreshold ?? MOVEMENT_TIME_THRESHOLD; // ms
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Initialize gesture tracking with starting touch position
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  public start(clientY: number): void {
 | 
					 | 
				
			||||||
    const now = Date.now();
 | 
					 | 
				
			||||||
    this._startY = clientY;
 | 
					 | 
				
			||||||
    this._startTime = now;
 | 
					 | 
				
			||||||
    this._lastY = clientY;
 | 
					 | 
				
			||||||
    this._lastTime = now;
 | 
					 | 
				
			||||||
    this._delta = 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Update gesture state during movement
 | 
					 | 
				
			||||||
   * Returns the current delta (negative when dragging down)
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  public move(clientY: number): number {
 | 
					 | 
				
			||||||
    const now = Date.now();
 | 
					 | 
				
			||||||
    this._delta = this._startY - clientY;
 | 
					 | 
				
			||||||
    this._lastY = clientY;
 | 
					 | 
				
			||||||
    this._lastTime = now;
 | 
					 | 
				
			||||||
    return this._delta;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Calculate final gesture result when touch ends
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  public end(): SwipeGestureResult {
 | 
					 | 
				
			||||||
    const velocity = this.getVelocity();
 | 
					 | 
				
			||||||
    const hasSignificantVelocity = Math.abs(velocity) > this._velocityThreshold;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      velocity,
 | 
					 | 
				
			||||||
      delta: this._delta,
 | 
					 | 
				
			||||||
      isSwipe: hasSignificantVelocity,
 | 
					 | 
				
			||||||
      isDownwardSwipe: velocity > 0,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Get current drag delta (negative when dragging down)
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  public getDelta(): number {
 | 
					 | 
				
			||||||
    return this._delta;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Calculate velocity based on recent movement
 | 
					 | 
				
			||||||
   * Returns 0 if no recent movement detected
 | 
					 | 
				
			||||||
   * Positive velocity means downward swipe
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  public getVelocity(): number {
 | 
					 | 
				
			||||||
    const now = Date.now();
 | 
					 | 
				
			||||||
    const timeSinceLastMove = now - this._lastTime;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Only consider velocity if the last movement was recent
 | 
					 | 
				
			||||||
    if (timeSinceLastMove >= this._movementTimeThreshold) {
 | 
					 | 
				
			||||||
      return 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const timeDelta = this._lastTime - this._startTime;
 | 
					 | 
				
			||||||
    return timeDelta > 0 ? (this._lastY - this._startY) / timeDelta : 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Reset all tracking state
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  public reset(): void {
 | 
					 | 
				
			||||||
    this._startY = 0;
 | 
					 | 
				
			||||||
    this._delta = 0;
 | 
					 | 
				
			||||||
    this._startTime = 0;
 | 
					 | 
				
			||||||
    this._lastY = 0;
 | 
					 | 
				
			||||||
    this._lastTime = 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -6,7 +6,6 @@ import { showConfirmationDialog } from "../../dialogs/generic/show-dialog-box";
 | 
				
			|||||||
import "./ha-progress-button";
 | 
					import "./ha-progress-button";
 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
import type { Appearance } from "../ha-button";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-call-service-button")
 | 
					@customElement("ha-call-service-button")
 | 
				
			||||||
class HaCallServiceButton extends LitElement {
 | 
					class HaCallServiceButton extends LitElement {
 | 
				
			||||||
@@ -26,14 +25,12 @@ class HaCallServiceButton extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property() public confirmation?;
 | 
					  @property() public confirmation?;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property() public appearance: Appearance = "plain";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public render(): TemplateResult {
 | 
					  public render(): TemplateResult {
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-progress-button
 | 
					      <ha-progress-button
 | 
				
			||||||
        .progress=${this.progress}
 | 
					        .progress=${this.progress}
 | 
				
			||||||
        .disabled=${this.disabled}
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
        .appearance=${this.appearance}
 | 
					        appearance="plain"
 | 
				
			||||||
        @click=${this._buttonTapped}
 | 
					        @click=${this._buttonTapped}
 | 
				
			||||||
        tabindex="0"
 | 
					        tabindex="0"
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,22 +1,21 @@
 | 
				
			|||||||
import type { LineSeriesOption } from "echarts";
 | 
					import type { LineSeriesOption } from "echarts";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export function downSampleLineData<
 | 
					export function downSampleLineData(
 | 
				
			||||||
  T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number],
 | 
					  data: LineSeriesOption["data"],
 | 
				
			||||||
>(
 | 
					  chartWidth: number,
 | 
				
			||||||
  data: T[] | undefined,
 | 
					 | 
				
			||||||
  maxDetails: number,
 | 
					 | 
				
			||||||
  minX?: number,
 | 
					  minX?: number,
 | 
				
			||||||
  maxX?: number
 | 
					  maxX?: number
 | 
				
			||||||
): T[] {
 | 
					) {
 | 
				
			||||||
  if (!data) {
 | 
					  if (!data || data.length < 10) {
 | 
				
			||||||
    return [];
 | 
					    return data;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (data.length <= maxDetails) {
 | 
					  const width = chartWidth * window.devicePixelRatio;
 | 
				
			||||||
 | 
					  if (data.length <= width) {
 | 
				
			||||||
    return data;
 | 
					    return data;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  const min = minX ?? getPointData(data[0]!)[0];
 | 
					  const min = minX ?? getPointData(data[0]!)[0];
 | 
				
			||||||
  const max = maxX ?? getPointData(data[data.length - 1]!)[0];
 | 
					  const max = maxX ?? getPointData(data[data.length - 1]!)[0];
 | 
				
			||||||
  const step = Math.ceil((max - min) / Math.floor(maxDetails));
 | 
					  const step = Math.floor((max - min) / width);
 | 
				
			||||||
  const frames = new Map<
 | 
					  const frames = new Map<
 | 
				
			||||||
    number,
 | 
					    number,
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
@@ -48,7 +47,7 @@ export function downSampleLineData<
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // Convert frames back to points
 | 
					  // Convert frames back to points
 | 
				
			||||||
  const result: T[] = [];
 | 
					  const result: typeof data = [];
 | 
				
			||||||
  for (const [_i, frame] of frames) {
 | 
					  for (const [_i, frame] of frames) {
 | 
				
			||||||
    // Use min/max points to preserve visual accuracy
 | 
					    // Use min/max points to preserve visual accuracy
 | 
				
			||||||
    // The order of the data must be preserved so max may be before min
 | 
					    // The order of the data must be preserved so max may be before min
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -22,7 +22,7 @@ import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			|||||||
import { listenMediaQuery } from "../../common/dom/media_query";
 | 
					import { listenMediaQuery } from "../../common/dom/media_query";
 | 
				
			||||||
import { themesContext } from "../../data/context";
 | 
					import { themesContext } from "../../data/context";
 | 
				
			||||||
import type { Themes } from "../../data/ws-themes";
 | 
					import type { Themes } from "../../data/ws-themes";
 | 
				
			||||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
					import type { ECOption } from "../../resources/echarts";
 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import { isMac } from "../../util/is_mac";
 | 
					import { isMac } from "../../util/is_mac";
 | 
				
			||||||
import "../chips/ha-assist-chip";
 | 
					import "../chips/ha-assist-chip";
 | 
				
			||||||
@@ -35,7 +35,6 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000;
 | 
				
			|||||||
const LEGEND_OVERFLOW_LIMIT = 10;
 | 
					const LEGEND_OVERFLOW_LIMIT = 10;
 | 
				
			||||||
const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
 | 
					const LEGEND_OVERFLOW_LIMIT_MOBILE = 6;
 | 
				
			||||||
const DOUBLE_TAP_TIME = 300;
 | 
					const DOUBLE_TAP_TIME = 300;
 | 
				
			||||||
const RESIZE_ANIMATION_DURATION = 250;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type CustomLegendOption = ECOption["legend"] & {
 | 
					export type CustomLegendOption = ECOption["legend"] & {
 | 
				
			||||||
  type: "custom";
 | 
					  type: "custom";
 | 
				
			||||||
@@ -89,19 +88,9 @@ export class HaChartBase extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _lastTapTime?: number;
 | 
					  private _lastTapTime?: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _shouldResizeChart = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // @ts-ignore
 | 
					  // @ts-ignore
 | 
				
			||||||
  private _resizeController = new ResizeController(this, {
 | 
					  private _resizeController = new ResizeController(this, {
 | 
				
			||||||
    callback: () => {
 | 
					    callback: () => this.chart?.resize(),
 | 
				
			||||||
      if (this.chart) {
 | 
					 | 
				
			||||||
        if (!this.chart.getZr().animation.isFinished()) {
 | 
					 | 
				
			||||||
          this._shouldResizeChart = true;
 | 
					 | 
				
			||||||
        } else {
 | 
					 | 
				
			||||||
          this.chart.resize();
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _loading = false;
 | 
					  private _loading = false;
 | 
				
			||||||
@@ -206,15 +195,6 @@ export class HaChartBase extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    if (changedProps.has("options")) {
 | 
					    if (changedProps.has("options")) {
 | 
				
			||||||
      chartOptions = { ...chartOptions, ...this._createOptions() };
 | 
					      chartOptions = { ...chartOptions, ...this._createOptions() };
 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        this._compareCustomLegendOptions(
 | 
					 | 
				
			||||||
          changedProps.get("options"),
 | 
					 | 
				
			||||||
          this.options
 | 
					 | 
				
			||||||
        )
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        // custom legend changes may require a resize to layout properly
 | 
					 | 
				
			||||||
        this._shouldResizeChart = true;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    } else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
 | 
					    } else if (this._isTouchDevice && changedProps.has("_isZoomed")) {
 | 
				
			||||||
      chartOptions.dataZoom = this._getDataZoomConfig();
 | 
					      chartOptions.dataZoom = this._getDataZoomConfig();
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -306,7 +286,7 @@ export class HaChartBase extends LitElement {
 | 
				
			|||||||
          itemStyle = {
 | 
					          itemStyle = {
 | 
				
			||||||
            color: dataset?.color as string,
 | 
					            color: dataset?.color as string,
 | 
				
			||||||
            ...(dataset?.itemStyle as { borderColor?: string }),
 | 
					            ...(dataset?.itemStyle as { borderColor?: string }),
 | 
				
			||||||
            ...itemStyle,
 | 
					            itemStyle,
 | 
				
			||||||
          };
 | 
					          };
 | 
				
			||||||
          const color = itemStyle?.color as string;
 | 
					          const color = itemStyle?.color as string;
 | 
				
			||||||
          const borderColor = itemStyle?.borderColor as string;
 | 
					          const borderColor = itemStyle?.borderColor as string;
 | 
				
			||||||
@@ -366,7 +346,7 @@ export class HaChartBase extends LitElement {
 | 
				
			|||||||
      if (this.chart) {
 | 
					      if (this.chart) {
 | 
				
			||||||
        this.chart.dispose();
 | 
					        this.chart.dispose();
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const echarts = (await import("../../resources/echarts/echarts")).default;
 | 
					      const echarts = (await import("../../resources/echarts")).default;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (this.extraComponents?.length) {
 | 
					      if (this.extraComponents?.length) {
 | 
				
			||||||
        echarts.use(this.extraComponents);
 | 
					        echarts.use(this.extraComponents);
 | 
				
			||||||
@@ -386,7 +366,6 @@ export class HaChartBase extends LitElement {
 | 
				
			|||||||
      if (!this.options?.dataZoom) {
 | 
					      if (!this.options?.dataZoom) {
 | 
				
			||||||
        this.chart.getZr().on("dblclick", this._handleClickZoom);
 | 
					        this.chart.getZr().on("dblclick", this._handleClickZoom);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      this.chart.on("finished", this._handleChartRenderFinished);
 | 
					 | 
				
			||||||
      if (this._isTouchDevice) {
 | 
					      if (this._isTouchDevice) {
 | 
				
			||||||
        this.chart.getZr().on("click", (e: ECElementEvent) => {
 | 
					        this.chart.getZr().on("click", (e: ECElementEvent) => {
 | 
				
			||||||
          if (!e.zrByTouch) {
 | 
					          if (!e.zrByTouch) {
 | 
				
			||||||
@@ -518,7 +497,6 @@ export class HaChartBase extends LitElement {
 | 
				
			|||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
    this.requestUpdate("_hiddenDatasets");
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getDataZoomConfig(): DataZoomComponentOption | undefined {
 | 
					  private _getDataZoomConfig(): DataZoomComponentOption | undefined {
 | 
				
			||||||
@@ -827,7 +805,7 @@ export class HaChartBase extends LitElement {
 | 
				
			|||||||
            sampling: undefined,
 | 
					            sampling: undefined,
 | 
				
			||||||
            data: downSampleLineData(
 | 
					            data: downSampleLineData(
 | 
				
			||||||
              data as LineSeriesOption["data"],
 | 
					              data as LineSeriesOption["data"],
 | 
				
			||||||
              this.clientWidth * window.devicePixelRatio,
 | 
					              this.clientWidth,
 | 
				
			||||||
              minX,
 | 
					              minX,
 | 
				
			||||||
              maxX
 | 
					              maxX
 | 
				
			||||||
            ),
 | 
					            ),
 | 
				
			||||||
@@ -967,33 +945,6 @@ export class HaChartBase extends LitElement {
 | 
				
			|||||||
    });
 | 
					    });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _handleChartRenderFinished = () => {
 | 
					 | 
				
			||||||
    if (this._shouldResizeChart) {
 | 
					 | 
				
			||||||
      this.chart?.resize({
 | 
					 | 
				
			||||||
        animation: this._reducedMotion
 | 
					 | 
				
			||||||
          ? undefined
 | 
					 | 
				
			||||||
          : { duration: RESIZE_ANIMATION_DURATION },
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      this._shouldResizeChart = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _compareCustomLegendOptions(
 | 
					 | 
				
			||||||
    oldOptions: ECOption | undefined,
 | 
					 | 
				
			||||||
    newOptions: ECOption | undefined
 | 
					 | 
				
			||||||
  ): boolean {
 | 
					 | 
				
			||||||
    const oldLegends = ensureArray(
 | 
					 | 
				
			||||||
      oldOptions?.legend || []
 | 
					 | 
				
			||||||
    ) as LegendComponentOption[];
 | 
					 | 
				
			||||||
    const newLegends = ensureArray(
 | 
					 | 
				
			||||||
      newOptions?.legend || []
 | 
					 | 
				
			||||||
    ) as LegendComponentOption[];
 | 
					 | 
				
			||||||
    return (
 | 
					 | 
				
			||||||
      oldLegends.some((l) => l.show && l.type === "custom") !==
 | 
					 | 
				
			||||||
      newLegends.some((l) => l.show && l.type === "custom")
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static styles = css`
 | 
					  static styles = css`
 | 
				
			||||||
    :host {
 | 
					    :host {
 | 
				
			||||||
      display: block;
 | 
					      display: block;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,7 +6,7 @@ import type { TopLevelFormatterParams } from "echarts/types/dist/shared";
 | 
				
			|||||||
import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
 | 
					import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js";
 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
import { listenMediaQuery } from "../../common/dom/media_query";
 | 
					import { listenMediaQuery } from "../../common/dom/media_query";
 | 
				
			||||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
					import type { ECOption } from "../../resources/echarts";
 | 
				
			||||||
import "./ha-chart-base";
 | 
					import "./ha-chart-base";
 | 
				
			||||||
import type { HaChartBase } from "./ha-chart-base";
 | 
					import type { HaChartBase } from "./ha-chart-base";
 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,13 +1,13 @@
 | 
				
			|||||||
import { customElement, property, state } from "lit/decorators";
 | 
					import { customElement, property, state } from "lit/decorators";
 | 
				
			||||||
import { LitElement, html, css } from "lit";
 | 
					import { LitElement, html, css } from "lit";
 | 
				
			||||||
import type { EChartsType } from "echarts/core";
 | 
					import type { EChartsType } from "echarts/core";
 | 
				
			||||||
 | 
					import type { CallbackDataParams } from "echarts/types/dist/shared";
 | 
				
			||||||
import type { SankeySeriesOption } from "echarts/types/dist/echarts";
 | 
					import type { SankeySeriesOption } from "echarts/types/dist/echarts";
 | 
				
			||||||
import type { CallbackDataParams } from "echarts/types/src/util/types";
 | 
					import { SankeyChart } from "echarts/charts";
 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
 | 
					import { ResizeController } from "@lit-labs/observers/resize-controller";
 | 
				
			||||||
import SankeyChart from "../../resources/echarts/components/sankey/install";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
					import type { ECOption } from "../../resources/echarts";
 | 
				
			||||||
import { measureTextWidth } from "../../util/text";
 | 
					import { measureTextWidth } from "../../util/text";
 | 
				
			||||||
import { filterXSS } from "../../common/util/xss";
 | 
					import { filterXSS } from "../../common/util/xss";
 | 
				
			||||||
import "./ha-chart-base";
 | 
					import "./ha-chart-base";
 | 
				
			||||||
@@ -39,7 +39,7 @@ type ProcessedLink = Link & {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const OVERFLOW_MARGIN = 5;
 | 
					const OVERFLOW_MARGIN = 5;
 | 
				
			||||||
const FONT_SIZE = 12;
 | 
					const FONT_SIZE = 12;
 | 
				
			||||||
const NODE_GAP = 6;
 | 
					const NODE_GAP = 8;
 | 
				
			||||||
const LABEL_DISTANCE = 5;
 | 
					const LABEL_DISTANCE = 5;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-sankey-chart")
 | 
					@customElement("ha-sankey-chart")
 | 
				
			||||||
@@ -164,7 +164,6 @@ export class HaSankeyChart extends LitElement {
 | 
				
			|||||||
      lineStyle: {
 | 
					      lineStyle: {
 | 
				
			||||||
        color: "gradient",
 | 
					        color: "gradient",
 | 
				
			||||||
        opacity: 0.4,
 | 
					        opacity: 0.4,
 | 
				
			||||||
        curveness: 0.5,
 | 
					 | 
				
			||||||
      },
 | 
					      },
 | 
				
			||||||
      layoutIterations: 0,
 | 
					      layoutIterations: 0,
 | 
				
			||||||
      label: {
 | 
					      label: {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -11,7 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl";
 | 
				
			|||||||
import type { LineChartEntity, LineChartState } from "../../data/history";
 | 
					import type { LineChartEntity, LineChartState } from "../../data/history";
 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
 | 
					import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
 | 
				
			||||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
					import type { ECOption } from "../../resources/echarts";
 | 
				
			||||||
import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
 | 
					import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getNumberFormatOptions,
 | 
					  getNumberFormatOptions,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,8 +15,8 @@ import type { TimelineEntity } from "../../data/history";
 | 
				
			|||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
 | 
					import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base";
 | 
				
			||||||
import { computeTimelineColor } from "./timeline-color";
 | 
					import { computeTimelineColor } from "./timeline-color";
 | 
				
			||||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
					import type { ECOption } from "../../resources/echarts";
 | 
				
			||||||
import echarts from "../../resources/echarts/echarts";
 | 
					import echarts from "../../resources/echarts";
 | 
				
			||||||
import { luminosity } from "../../common/color/rgb";
 | 
					import { luminosity } from "../../common/color/rgb";
 | 
				
			||||||
import { hex2rgb } from "../../common/color/convert-color";
 | 
					import { hex2rgb } from "../../common/color/convert-color";
 | 
				
			||||||
import { measureTextWidth } from "../../util/text";
 | 
					import { measureTextWidth } from "../../util/text";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,7 @@ import {
 | 
				
			|||||||
  getStatisticMetadata,
 | 
					  getStatisticMetadata,
 | 
				
			||||||
  statisticsHaveType,
 | 
					  statisticsHaveType,
 | 
				
			||||||
} from "../../data/recorder";
 | 
					} from "../../data/recorder";
 | 
				
			||||||
import type { ECOption } from "../../resources/echarts/echarts";
 | 
					import type { ECOption } from "../../resources/echarts";
 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import type { CustomLegendOption } from "./ha-chart-base";
 | 
					import type { CustomLegendOption } from "./ha-chart-base";
 | 
				
			||||||
import "./ha-chart-base";
 | 
					import "./ha-chart-base";
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,9 @@
 | 
				
			|||||||
import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
 | 
					 | 
				
			||||||
import { FilterChip } from "@material/web/chips/internal/filter-chip";
 | 
					import { FilterChip } from "@material/web/chips/internal/filter-chip";
 | 
				
			||||||
import { styles } from "@material/web/chips/internal/filter-styles";
 | 
					import { styles } from "@material/web/chips/internal/filter-styles";
 | 
				
			||||||
import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles";
 | 
					import { styles as selectableStyles } from "@material/web/chips/internal/selectable-styles";
 | 
				
			||||||
import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
 | 
					import { styles as sharedStyles } from "@material/web/chips/internal/shared-styles";
 | 
				
			||||||
import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles";
 | 
					import { styles as trailingIconStyles } from "@material/web/chips/internal/trailing-icon-styles";
 | 
				
			||||||
 | 
					import { styles as elevatedStyles } from "@material/web/chips/internal/elevated-styles";
 | 
				
			||||||
import { css, html } from "lit";
 | 
					import { css, html } from "lit";
 | 
				
			||||||
import { customElement, property } from "lit/decorators";
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -30,7 +30,6 @@ export class HaFilterChip extends FilterChip {
 | 
				
			|||||||
          var(--rgb-primary-text-color),
 | 
					          var(--rgb-primary-text-color),
 | 
				
			||||||
          0.15
 | 
					          0.15
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        border-radius: var(--ha-border-radius-md);
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
    `,
 | 
					    `,
 | 
				
			||||||
  ];
 | 
					  ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js";
 | 
					import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
 | 
				
			||||||
import type { CSSResultGroup } from "lit";
 | 
					import type { CSSResultGroup } from "lit";
 | 
				
			||||||
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";
 | 
				
			||||||
@@ -129,7 +129,7 @@ export class DialogDataTableSettings extends LitElement {
 | 
				
			|||||||
                  ${canMove && isVisible
 | 
					                  ${canMove && isVisible
 | 
				
			||||||
                    ? html`<ha-svg-icon
 | 
					                    ? html`<ha-svg-icon
 | 
				
			||||||
                        class="handle"
 | 
					                        class="handle"
 | 
				
			||||||
                        .path=${mdiDragHorizontalVariant}
 | 
					                        .path=${mdiDrag}
 | 
				
			||||||
                        slot="graphic"
 | 
					                        slot="graphic"
 | 
				
			||||||
                      ></ha-svg-icon>`
 | 
					                      ></ha-svg-icon>`
 | 
				
			||||||
                    : nothing}
 | 
					                    : nothing}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -5,18 +5,24 @@ import { customElement, property, query, state } from "lit/decorators";
 | 
				
			|||||||
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 { computeAreaName } from "../../common/entity/compute_area_name";
 | 
					import { computeAreaName } from "../../common/entity/compute_area_name";
 | 
				
			||||||
import { computeDeviceName } from "../../common/entity/compute_device_name";
 | 
					import {
 | 
				
			||||||
 | 
					  computeDeviceName,
 | 
				
			||||||
 | 
					  computeDeviceNameDisplay,
 | 
				
			||||||
 | 
					} from "../../common/entity/compute_device_name";
 | 
				
			||||||
 | 
					import { computeDomain } from "../../common/entity/compute_domain";
 | 
				
			||||||
import { getDeviceContext } from "../../common/entity/context/get_device_context";
 | 
					import { getDeviceContext } from "../../common/entity/context/get_device_context";
 | 
				
			||||||
import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
 | 
					import { getConfigEntries, type ConfigEntry } from "../../data/config_entries";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getDevices,
 | 
					  getDeviceEntityDisplayLookup,
 | 
				
			||||||
  type DevicePickerItem,
 | 
					  type DeviceEntityDisplayLookup,
 | 
				
			||||||
  type DeviceRegistryEntry,
 | 
					  type DeviceRegistryEntry,
 | 
				
			||||||
} from "../../data/device_registry";
 | 
					} from "../../data/device_registry";
 | 
				
			||||||
 | 
					import { domainToName } from "../../data/integration";
 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import { brandsUrl } from "../../util/brands-url";
 | 
					import { brandsUrl } from "../../util/brands-url";
 | 
				
			||||||
import "../ha-generic-picker";
 | 
					import "../ha-generic-picker";
 | 
				
			||||||
import type { HaGenericPicker } from "../ha-generic-picker";
 | 
					import type { HaGenericPicker } from "../ha-generic-picker";
 | 
				
			||||||
 | 
					import type { PickerComboBoxItem } from "../ha-picker-combo-box";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type HaDevicePickerDeviceFilterFunc = (
 | 
					export type HaDevicePickerDeviceFilterFunc = (
 | 
				
			||||||
  device: DeviceRegistryEntry
 | 
					  device: DeviceRegistryEntry
 | 
				
			||||||
@@ -24,6 +30,11 @@ export type HaDevicePickerDeviceFilterFunc = (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
 | 
					export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface DevicePickerItem extends PickerComboBoxItem {
 | 
				
			||||||
 | 
					  domain?: string;
 | 
				
			||||||
 | 
					  domain_name?: string;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-device-picker")
 | 
					@customElement("ha-device-picker")
 | 
				
			||||||
export class HaDevicePicker extends LitElement {
 | 
					export class HaDevicePicker extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
@@ -93,8 +104,6 @@ export class HaDevicePicker extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _configEntryLookup: Record<string, ConfigEntry> = {};
 | 
					  @state() private _configEntryLookup: Record<string, ConfigEntry> = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getDevicesMemoized = memoizeOne(getDevices);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected firstUpdated(_changedProperties: PropertyValues): void {
 | 
					  protected firstUpdated(_changedProperties: PropertyValues): void {
 | 
				
			||||||
    super.firstUpdated(_changedProperties);
 | 
					    super.firstUpdated(_changedProperties);
 | 
				
			||||||
    this._loadConfigEntries();
 | 
					    this._loadConfigEntries();
 | 
				
			||||||
@@ -108,18 +117,162 @@ export class HaDevicePicker extends LitElement {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getItems = () =>
 | 
					  private _getItems = () =>
 | 
				
			||||||
    this._getDevicesMemoized(
 | 
					    this._getDevices(
 | 
				
			||||||
      this.hass,
 | 
					      this.hass.devices,
 | 
				
			||||||
 | 
					      this.hass.entities,
 | 
				
			||||||
      this._configEntryLookup,
 | 
					      this._configEntryLookup,
 | 
				
			||||||
      this.includeDomains,
 | 
					      this.includeDomains,
 | 
				
			||||||
      this.excludeDomains,
 | 
					      this.excludeDomains,
 | 
				
			||||||
      this.includeDeviceClasses,
 | 
					      this.includeDeviceClasses,
 | 
				
			||||||
      this.deviceFilter,
 | 
					      this.deviceFilter,
 | 
				
			||||||
      this.entityFilter,
 | 
					      this.entityFilter,
 | 
				
			||||||
      this.excludeDevices,
 | 
					      this.excludeDevices
 | 
				
			||||||
      this.value
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _getDevices = memoizeOne(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					      haDevices: HomeAssistant["devices"],
 | 
				
			||||||
 | 
					      haEntities: HomeAssistant["entities"],
 | 
				
			||||||
 | 
					      configEntryLookup: Record<string, ConfigEntry>,
 | 
				
			||||||
 | 
					      includeDomains: this["includeDomains"],
 | 
				
			||||||
 | 
					      excludeDomains: this["excludeDomains"],
 | 
				
			||||||
 | 
					      includeDeviceClasses: this["includeDeviceClasses"],
 | 
				
			||||||
 | 
					      deviceFilter: this["deviceFilter"],
 | 
				
			||||||
 | 
					      entityFilter: this["entityFilter"],
 | 
				
			||||||
 | 
					      excludeDevices: this["excludeDevices"]
 | 
				
			||||||
 | 
					    ): DevicePickerItem[] => {
 | 
				
			||||||
 | 
					      const devices = Object.values(haDevices);
 | 
				
			||||||
 | 
					      const entities = Object.values(haEntities);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let deviceEntityLookup: DeviceEntityDisplayLookup = {};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        includeDomains ||
 | 
				
			||||||
 | 
					        excludeDomains ||
 | 
				
			||||||
 | 
					        includeDeviceClasses ||
 | 
				
			||||||
 | 
					        entityFilter
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let inputDevices = devices.filter(
 | 
				
			||||||
 | 
					        (device) => device.id === this.value || !device.disabled_by
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (includeDomains) {
 | 
				
			||||||
 | 
					        inputDevices = inputDevices.filter((device) => {
 | 
				
			||||||
 | 
					          const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					          if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return deviceEntityLookup[device.id].some((entity) =>
 | 
				
			||||||
 | 
					            includeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (excludeDomains) {
 | 
				
			||||||
 | 
					        inputDevices = inputDevices.filter((device) => {
 | 
				
			||||||
 | 
					          const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					          if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					            return true;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return entities.every(
 | 
				
			||||||
 | 
					            (entity) =>
 | 
				
			||||||
 | 
					              !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (excludeDevices) {
 | 
				
			||||||
 | 
					        inputDevices = inputDevices.filter(
 | 
				
			||||||
 | 
					          (device) => !excludeDevices!.includes(device.id)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (includeDeviceClasses) {
 | 
				
			||||||
 | 
					        inputDevices = inputDevices.filter((device) => {
 | 
				
			||||||
 | 
					          const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					          if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return deviceEntityLookup[device.id].some((entity) => {
 | 
				
			||||||
 | 
					            const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					            if (!stateObj) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              stateObj.attributes.device_class &&
 | 
				
			||||||
 | 
					              includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (entityFilter) {
 | 
				
			||||||
 | 
					        inputDevices = inputDevices.filter((device) => {
 | 
				
			||||||
 | 
					          const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					          if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					            return false;
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return devEntities.some((entity) => {
 | 
				
			||||||
 | 
					            const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					            if (!stateObj) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return entityFilter(stateObj);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (deviceFilter) {
 | 
				
			||||||
 | 
					        inputDevices = inputDevices.filter(
 | 
				
			||||||
 | 
					          (device) =>
 | 
				
			||||||
 | 
					            // We always want to include the device of the current value
 | 
				
			||||||
 | 
					            device.id === this.value || deviceFilter!(device)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
 | 
				
			||||||
 | 
					        const deviceName = computeDeviceNameDisplay(
 | 
				
			||||||
 | 
					          device,
 | 
				
			||||||
 | 
					          this.hass,
 | 
				
			||||||
 | 
					          deviceEntityLookup[device.id]
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const { area } = getDeviceContext(device, this.hass);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const areaName = area ? computeAreaName(area) : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const configEntry = device.primary_config_entry
 | 
				
			||||||
 | 
					          ? configEntryLookup?.[device.primary_config_entry]
 | 
				
			||||||
 | 
					          : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const domain = configEntry?.domain;
 | 
				
			||||||
 | 
					        const domainName = domain
 | 
				
			||||||
 | 
					          ? domainToName(this.hass.localize, domain)
 | 
				
			||||||
 | 
					          : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          id: device.id,
 | 
				
			||||||
 | 
					          label: "",
 | 
				
			||||||
 | 
					          primary:
 | 
				
			||||||
 | 
					            deviceName ||
 | 
				
			||||||
 | 
					            this.hass.localize("ui.components.device-picker.unnamed_device"),
 | 
				
			||||||
 | 
					          secondary: areaName,
 | 
				
			||||||
 | 
					          domain: configEntry?.domain,
 | 
				
			||||||
 | 
					          domain_name: domainName,
 | 
				
			||||||
 | 
					          search_labels: [deviceName, areaName, domain, domainName].filter(
 | 
				
			||||||
 | 
					            Boolean
 | 
				
			||||||
 | 
					          ) as string[],
 | 
				
			||||||
 | 
					          sorting_label: deviceName || "zzz",
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return outputDevices;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _valueRenderer = memoizeOne(
 | 
					  private _valueRenderer = memoizeOne(
 | 
				
			||||||
    (configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => {
 | 
					    (configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => {
 | 
				
			||||||
      const deviceId = value;
 | 
					      const deviceId = value;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1 +0,0 @@
 | 
				
			|||||||
export const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__";
 | 
					 | 
				
			||||||
@@ -1,13 +1,13 @@
 | 
				
			|||||||
import { mdiDragHorizontalVariant } from "@mdi/js";
 | 
					import { mdiDrag } from "@mdi/js";
 | 
				
			||||||
import { css, html, LitElement, nothing } from "lit";
 | 
					import { css, html, LitElement, nothing } from "lit";
 | 
				
			||||||
import { customElement, property } from "lit/decorators";
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
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 { isValidEntityId } from "../../common/entity/valid_entity_id";
 | 
					import { isValidEntityId } from "../../common/entity/valid_entity_id";
 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
 | 
					 | 
				
			||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
					import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
				
			||||||
import "../ha-sortable";
 | 
					import "../ha-sortable";
 | 
				
			||||||
import "./ha-entity-picker";
 | 
					import "./ha-entity-picker";
 | 
				
			||||||
 | 
					import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-entities-picker")
 | 
					@customElement("ha-entities-picker")
 | 
				
			||||||
class HaEntitiesPicker extends LitElement {
 | 
					class HaEntitiesPicker extends LitElement {
 | 
				
			||||||
@@ -118,7 +118,7 @@ class HaEntitiesPicker extends LitElement {
 | 
				
			|||||||
                  ? html`
 | 
					                  ? html`
 | 
				
			||||||
                      <ha-svg-icon
 | 
					                      <ha-svg-icon
 | 
				
			||||||
                        class="entity-handle"
 | 
					                        class="entity-handle"
 | 
				
			||||||
                        .path=${mdiDragHorizontalVariant}
 | 
					                        .path=${mdiDrag}
 | 
				
			||||||
                      ></ha-svg-icon>
 | 
					                      ></ha-svg-icon>
 | 
				
			||||||
                    `
 | 
					                    `
 | 
				
			||||||
                  : nothing}
 | 
					                  : nothing}
 | 
				
			||||||
@@ -147,7 +147,6 @@ class HaEntitiesPicker extends LitElement {
 | 
				
			|||||||
          .createDomains=${this.createDomains}
 | 
					          .createDomains=${this.createDomains}
 | 
				
			||||||
          .required=${this.required && !currentEntities.length}
 | 
					          .required=${this.required && !currentEntities.length}
 | 
				
			||||||
          @value-changed=${this._addEntity}
 | 
					          @value-changed=${this._addEntity}
 | 
				
			||||||
          .addButton=${currentEntities.length > 0}
 | 
					 | 
				
			||||||
        ></ha-entity-picker>
 | 
					        ></ha-entity-picker>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import type { HassEntity } from "home-assistant-js-websocket";
 | 
				
			||||||
import type { PropertyValues } from "lit";
 | 
					import type { PropertyValues } from "lit";
 | 
				
			||||||
import { LitElement, html, nothing } from "lit";
 | 
					import { LitElement, html, nothing } from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
@@ -7,6 +8,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
				
			|||||||
import "../ha-combo-box";
 | 
					import "../ha-combo-box";
 | 
				
			||||||
import type { HaComboBox } from "../ha-combo-box";
 | 
					import type { HaComboBox } from "../ha-combo-box";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface AttributeOption {
 | 
					interface AttributeOption {
 | 
				
			||||||
  value: string;
 | 
					  value: string;
 | 
				
			||||||
  label: string;
 | 
					  label: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import "@material/mwc-menu/mwc-menu-surface";
 | 
					import "@material/mwc-menu/mwc-menu-surface";
 | 
				
			||||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
 | 
					import { mdiDrag, mdiPlus } from "@mdi/js";
 | 
				
			||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
					import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
				
			||||||
import type { IFuseOptions } from "fuse.js";
 | 
					import type { IFuseOptions } from "fuse.js";
 | 
				
			||||||
import Fuse from "fuse.js";
 | 
					import Fuse from "fuse.js";
 | 
				
			||||||
@@ -20,13 +20,11 @@ import "../chips/ha-chip-set";
 | 
				
			|||||||
import "../chips/ha-input-chip";
 | 
					import "../chips/ha-input-chip";
 | 
				
			||||||
import "../ha-combo-box";
 | 
					import "../ha-combo-box";
 | 
				
			||||||
import type { HaComboBox } from "../ha-combo-box";
 | 
					import type { HaComboBox } from "../ha-combo-box";
 | 
				
			||||||
import "../ha-input-helper-text";
 | 
					 | 
				
			||||||
import "../ha-sortable";
 | 
					import "../ha-sortable";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface EntityNameOption {
 | 
					interface EntityNameOption {
 | 
				
			||||||
  primary: string;
 | 
					  primary: string;
 | 
				
			||||||
  secondary?: string;
 | 
					  secondary?: string;
 | 
				
			||||||
  field_label: string;
 | 
					 | 
				
			||||||
  value: string;
 | 
					  value: string;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -43,23 +41,6 @@ const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]);
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
 | 
					const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const formatOptionValue = (item: EntityNameItem) => {
 | 
					 | 
				
			||||||
  if (item.type === "text" && item.text) {
 | 
					 | 
				
			||||||
    return item.text;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return `___${item.type}___`;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const parseOptionValue = (value: string): EntityNameItem => {
 | 
					 | 
				
			||||||
  if (value.startsWith("___") && value.endsWith("___")) {
 | 
					 | 
				
			||||||
    const type = value.slice(3, -3);
 | 
					 | 
				
			||||||
    if (KNOWN_TYPES.has(type)) {
 | 
					 | 
				
			||||||
      return { type: type as EntityNameType };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
  return { type: "text", text: value };
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ha-entity-name-picker")
 | 
					@customElement("ha-entity-name-picker")
 | 
				
			||||||
export class HaEntityNamePicker extends LitElement {
 | 
					export class HaEntityNamePicker extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
@@ -87,8 +68,8 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private _editIndex?: number;
 | 
					  private _editIndex?: number;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _validTypes = memoizeOne((entityId?: string) => {
 | 
					  private _validOptions = memoizeOne((entityId?: string) => {
 | 
				
			||||||
    const options = new Set<string>(["text"]);
 | 
					    const options = new Set<string>();
 | 
				
			||||||
    if (!entityId) {
 | 
					    if (!entityId) {
 | 
				
			||||||
      return options;
 | 
					      return options;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -120,43 +101,33 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
      return [];
 | 
					      return [];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const types = this._validTypes(entityId);
 | 
					    const options = this._validOptions(entityId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const items = (
 | 
					    const items = (
 | 
				
			||||||
      ["entity", "device", "area", "floor"] as const
 | 
					      ["entity", "device", "area", "floor"] as const
 | 
				
			||||||
    ).map<EntityNameOption>((name) => {
 | 
					    ).map<EntityNameOption>((name) => {
 | 
				
			||||||
      const stateObj = this.hass.states[entityId];
 | 
					      const stateObj = this.hass.states[entityId];
 | 
				
			||||||
      const isValid = types.has(name);
 | 
					      const isValid = options.has(name);
 | 
				
			||||||
      const primary = this.hass.localize(
 | 
					      const primary = this.hass.localize(
 | 
				
			||||||
        `ui.components.entity.entity-name-picker.types.${name}`
 | 
					        `ui.components.entity.entity-name-picker.types.${name}`
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
      const secondary =
 | 
					      const secondary =
 | 
				
			||||||
        (stateObj && isValid
 | 
					        stateObj && isValid
 | 
				
			||||||
          ? this.hass.formatEntityName(stateObj, { type: name })
 | 
					          ? this.hass.formatEntityName(stateObj, { type: name })
 | 
				
			||||||
          : this.hass.localize(
 | 
					          : this.hass.localize(
 | 
				
			||||||
              `ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
 | 
					              `ui.components.entity.entity-name-picker.types.${name}_missing` as LocalizeKeys
 | 
				
			||||||
            )) || "-";
 | 
					            ) || "-";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        primary,
 | 
					        primary,
 | 
				
			||||||
        secondary,
 | 
					        secondary,
 | 
				
			||||||
        field_label: primary,
 | 
					        value: name,
 | 
				
			||||||
        value: formatOptionValue({ type: name }),
 | 
					 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return items;
 | 
					    return items;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _customNameOption = memoizeOne((text: string) => ({
 | 
					 | 
				
			||||||
    primary: this.hass.localize(
 | 
					 | 
				
			||||||
      "ui.components.entity.entity-name-picker.custom_name"
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
    secondary: `"${text}"`,
 | 
					 | 
				
			||||||
    field_label: text,
 | 
					 | 
				
			||||||
    value: formatOptionValue({ type: "text", text }),
 | 
					 | 
				
			||||||
  }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _formatItem = (item: EntityNameItem) => {
 | 
					  private _formatItem = (item: EntityNameItem) => {
 | 
				
			||||||
    if (item.type === "text") {
 | 
					    if (item.type === "text") {
 | 
				
			||||||
      return `"${item.text}"`;
 | 
					      return `"${item.text}"`;
 | 
				
			||||||
@@ -170,9 +141,9 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected render() {
 | 
					  protected render() {
 | 
				
			||||||
    const value = this._items;
 | 
					    const value = this._value;
 | 
				
			||||||
    const options = this._getOptions(this.entityId);
 | 
					    const options = this._getOptions(this.entityId);
 | 
				
			||||||
    const validTypes = this._validTypes(this.entityId);
 | 
					    const validOptions = this._validOptions(this.entityId);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
					      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
				
			||||||
@@ -186,11 +157,12 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
        >
 | 
					        >
 | 
				
			||||||
          <ha-chip-set>
 | 
					          <ha-chip-set>
 | 
				
			||||||
            ${repeat(
 | 
					            ${repeat(
 | 
				
			||||||
              this._items,
 | 
					              this._value,
 | 
				
			||||||
              (item) => item,
 | 
					              (item) => item,
 | 
				
			||||||
              (item: EntityNameItem, idx) => {
 | 
					              (item: EntityNameItem, idx) => {
 | 
				
			||||||
                const label = this._formatItem(item);
 | 
					                const label = this._formatItem(item);
 | 
				
			||||||
                const isValid = validTypes.has(item.type);
 | 
					                const isValid =
 | 
				
			||||||
 | 
					                  item.type === "text" || validOptions.has(item.type);
 | 
				
			||||||
                return html`
 | 
					                return html`
 | 
				
			||||||
                  <ha-input-chip
 | 
					                  <ha-input-chip
 | 
				
			||||||
                    data-idx=${idx}
 | 
					                    data-idx=${idx}
 | 
				
			||||||
@@ -201,10 +173,7 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
                    .disabled=${this.disabled}
 | 
					                    .disabled=${this.disabled}
 | 
				
			||||||
                    class=${!isValid ? "invalid" : ""}
 | 
					                    class=${!isValid ? "invalid" : ""}
 | 
				
			||||||
                  >
 | 
					                  >
 | 
				
			||||||
                    <ha-svg-icon
 | 
					                    <ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
 | 
				
			||||||
                      slot="icon"
 | 
					 | 
				
			||||||
                      .path=${mdiDragHorizontalVariant}
 | 
					 | 
				
			||||||
                    ></ha-svg-icon>
 | 
					 | 
				
			||||||
                    <span>${label}</span>
 | 
					                    <span>${label}</span>
 | 
				
			||||||
                  </ha-input-chip>
 | 
					                  </ha-input-chip>
 | 
				
			||||||
                `;
 | 
					                `;
 | 
				
			||||||
@@ -238,13 +207,14 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
            .hass=${this.hass}
 | 
					            .hass=${this.hass}
 | 
				
			||||||
            .value=${""}
 | 
					            .value=${""}
 | 
				
			||||||
            .autofocus=${this.autofocus}
 | 
					            .autofocus=${this.autofocus}
 | 
				
			||||||
            .disabled=${this.disabled}
 | 
					            .disabled=${this.disabled || !this.entityId}
 | 
				
			||||||
            .required=${this.required && !value.length}
 | 
					            .required=${this.required && !value.length}
 | 
				
			||||||
 | 
					            .helper=${this.helper}
 | 
				
			||||||
            .items=${options}
 | 
					            .items=${options}
 | 
				
			||||||
            allow-custom-value
 | 
					            allow-custom-value
 | 
				
			||||||
            item-id-path="value"
 | 
					            item-id-path="value"
 | 
				
			||||||
            item-value-path="value"
 | 
					            item-value-path="value"
 | 
				
			||||||
            item-label-path="field_label"
 | 
					            item-label-path="primary"
 | 
				
			||||||
            .renderer=${rowRenderer}
 | 
					            .renderer=${rowRenderer}
 | 
				
			||||||
            @opened-changed=${this._openedChanged}
 | 
					            @opened-changed=${this._openedChanged}
 | 
				
			||||||
            @value-changed=${this._comboBoxValueChanged}
 | 
					            @value-changed=${this._comboBoxValueChanged}
 | 
				
			||||||
@@ -253,20 +223,9 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
          </ha-combo-box>
 | 
					          </ha-combo-box>
 | 
				
			||||||
        </mwc-menu-surface>
 | 
					        </mwc-menu-surface>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      ${this._renderHelper()}
 | 
					 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _renderHelper() {
 | 
					 | 
				
			||||||
    return this.helper
 | 
					 | 
				
			||||||
      ? html`
 | 
					 | 
				
			||||||
          <ha-input-helper-text .disabled=${this.disabled}>
 | 
					 | 
				
			||||||
            ${this.helper}
 | 
					 | 
				
			||||||
          </ha-input-helper-text>
 | 
					 | 
				
			||||||
        `
 | 
					 | 
				
			||||||
      : nothing;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _onClosed(ev) {
 | 
					  private _onClosed(ev) {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    this._opened = false;
 | 
					    this._opened = false;
 | 
				
			||||||
@@ -295,16 +254,13 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
    this._opened = true;
 | 
					    this._opened = true;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private get _items(): EntityNameItem[] {
 | 
					  private get _value(): EntityNameItem[] {
 | 
				
			||||||
    return this._toItems(this.value);
 | 
					    return this._toItems(this.value);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _toItems = memoizeOne((value?: typeof this.value) => {
 | 
					  private _toItems = memoizeOne((value?: typeof this.value) => {
 | 
				
			||||||
    if (typeof value === "string") {
 | 
					    if (typeof value === "string") {
 | 
				
			||||||
      if (value === "") {
 | 
					      return [{ type: "text", text: value } as const];
 | 
				
			||||||
        return [];
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return [{ type: "text", text: value } satisfies EntityNameItem];
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    return value ? ensureArray(value) : [];
 | 
					    return value ? ensureArray(value) : [];
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
@@ -312,7 +268,7 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
  private _toValue = memoizeOne(
 | 
					  private _toValue = memoizeOne(
 | 
				
			||||||
    (items: EntityNameItem[]): typeof this.value => {
 | 
					    (items: EntityNameItem[]): typeof this.value => {
 | 
				
			||||||
      if (items.length === 0) {
 | 
					      if (items.length === 0) {
 | 
				
			||||||
        return undefined;
 | 
					        return [];
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      if (items.length === 1) {
 | 
					      if (items.length === 1) {
 | 
				
			||||||
        const item = items[0];
 | 
					        const item = items[0];
 | 
				
			||||||
@@ -328,21 +284,20 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
      const options = this._comboBox.items || [];
 | 
					      const options = this._comboBox.items || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const initialItem =
 | 
					      const initialItem =
 | 
				
			||||||
        this._editIndex != null ? this._items[this._editIndex] : undefined;
 | 
					        this._editIndex != null ? this._value[this._editIndex] : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const initialValue = initialItem ? formatOptionValue(initialItem) : "";
 | 
					      const initialValue = initialItem
 | 
				
			||||||
 | 
					        ? initialItem.type === "text"
 | 
				
			||||||
 | 
					          ? initialItem.text
 | 
				
			||||||
 | 
					          : initialItem.type
 | 
				
			||||||
 | 
					        : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const filteredItems = this._filterSelectedOptions(options, initialValue);
 | 
					      const filteredItems = this._filterSelectedOptions(options, initialValue);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      if (initialItem?.type === "text" && initialItem.text) {
 | 
					 | 
				
			||||||
        filteredItems.push(this._customNameOption(initialItem.text));
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this._comboBox.filteredItems = filteredItems;
 | 
					      this._comboBox.filteredItems = filteredItems;
 | 
				
			||||||
      this._comboBox.setInputValue(initialValue);
 | 
					      this._comboBox.setInputValue(initialValue);
 | 
				
			||||||
    } else {
 | 
					    } else {
 | 
				
			||||||
      this._opened = false;
 | 
					      this._opened = false;
 | 
				
			||||||
      this._comboBox.setInputValue("");
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -350,16 +305,15 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
    options: EntityNameOption[],
 | 
					    options: EntityNameOption[],
 | 
				
			||||||
    current?: string
 | 
					    current?: string
 | 
				
			||||||
  ) => {
 | 
					  ) => {
 | 
				
			||||||
    const items = this._items;
 | 
					    const value = this._value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const excludedValues = new Set(
 | 
					    const types = value.map((item) => item.type) as string[];
 | 
				
			||||||
      items
 | 
					 | 
				
			||||||
        .filter((item) => UNIQUE_TYPES.has(item.type))
 | 
					 | 
				
			||||||
        .map((item) => formatOptionValue(item))
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const filteredOptions = options.filter(
 | 
					    const filteredOptions = options.filter(
 | 
				
			||||||
      (option) => !excludedValues.has(option.value) || option.value === current
 | 
					      (option) =>
 | 
				
			||||||
 | 
					        !UNIQUE_TYPES.has(option.value) ||
 | 
				
			||||||
 | 
					        !types.includes(option.value) ||
 | 
				
			||||||
 | 
					        option.value === current
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    return filteredOptions;
 | 
					    return filteredOptions;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
@@ -370,14 +324,20 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
    const options = this._comboBox.items || [];
 | 
					    const options = this._comboBox.items || [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const currentItem =
 | 
					    const currentItem =
 | 
				
			||||||
      this._editIndex != null ? this._items[this._editIndex] : undefined;
 | 
					      this._editIndex != null ? this._value[this._editIndex] : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const currentValue = currentItem ? formatOptionValue(currentItem) : "";
 | 
					    const currentValue = currentItem
 | 
				
			||||||
 | 
					      ? currentItem.type === "text"
 | 
				
			||||||
 | 
					        ? currentItem.text
 | 
				
			||||||
 | 
					        : currentItem.type
 | 
				
			||||||
 | 
					      : "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    let filteredItems = this._filterSelectedOptions(options, currentValue);
 | 
					    this._comboBox.filteredItems = this._filterSelectedOptions(
 | 
				
			||||||
 | 
					      options,
 | 
				
			||||||
 | 
					      currentValue
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!filter) {
 | 
					    if (!filter) {
 | 
				
			||||||
      this._comboBox.filteredItems = filteredItems;
 | 
					 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -389,16 +349,16 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
      ignoreDiacritics: true,
 | 
					      ignoreDiacritics: true,
 | 
				
			||||||
    };
 | 
					    };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const fuse = new Fuse(filteredItems, fuseOptions);
 | 
					    const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
 | 
				
			||||||
    filteredItems = fuse.search(filter).map((result) => result.item);
 | 
					    const filteredItems = fuse.search(filter).map((result) => result.item);
 | 
				
			||||||
    filteredItems.push(this._customNameOption(input));
 | 
					
 | 
				
			||||||
    this._comboBox.filteredItems = filteredItems;
 | 
					    this._comboBox.filteredItems = filteredItems;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async _moveItem(ev: CustomEvent) {
 | 
					  private async _moveItem(ev: CustomEvent) {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    const { oldIndex, newIndex } = ev.detail;
 | 
					    const { oldIndex, newIndex } = ev.detail;
 | 
				
			||||||
    const value = this._items;
 | 
					    const value = this._value;
 | 
				
			||||||
    const newValue = value.concat();
 | 
					    const newValue = value.concat();
 | 
				
			||||||
    const element = newValue.splice(oldIndex, 1)[0];
 | 
					    const element = newValue.splice(oldIndex, 1)[0];
 | 
				
			||||||
    newValue.splice(newIndex, 0, element);
 | 
					    newValue.splice(newIndex, 0, element);
 | 
				
			||||||
@@ -409,7 +369,7 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  private async _removeItem(ev) {
 | 
					  private async _removeItem(ev) {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    const value = [...this._items];
 | 
					    const value = [...this._value];
 | 
				
			||||||
    const idx = parseInt(ev.target.dataset.idx, 10);
 | 
					    const idx = parseInt(ev.target.dataset.idx, 10);
 | 
				
			||||||
    value.splice(idx, 1);
 | 
					    value.splice(idx, 1);
 | 
				
			||||||
    this._setValue(value);
 | 
					    this._setValue(value);
 | 
				
			||||||
@@ -425,9 +385,11 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const item: EntityNameItem = parseOptionValue(value);
 | 
					    const item: EntityNameItem = KNOWN_TYPES.has(value as any)
 | 
				
			||||||
 | 
					      ? { type: value as EntityNameType }
 | 
				
			||||||
 | 
					      : { type: "text", text: value };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const newValue = [...this._items];
 | 
					    const newValue = [...this._value];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this._editIndex != null) {
 | 
					    if (this._editIndex != null) {
 | 
				
			||||||
      newValue[this._editIndex] = item;
 | 
					      newValue[this._editIndex] = item;
 | 
				
			||||||
@@ -521,11 +483,6 @@ export class HaEntityNamePicker extends LitElement {
 | 
				
			|||||||
    .sortable-drag {
 | 
					    .sortable-drag {
 | 
				
			||||||
      cursor: grabbing;
 | 
					      cursor: grabbing;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    ha-input-helper-text {
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
      margin: var(--ha-space-2) 0 0;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,15 @@
 | 
				
			|||||||
import { mdiPlus, mdiShape } from "@mdi/js";
 | 
					import { mdiPlus, mdiShape } from "@mdi/js";
 | 
				
			||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
					import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
				
			||||||
 | 
					import type { HassEntity } from "home-assistant-js-websocket";
 | 
				
			||||||
import { html, LitElement, nothing, type PropertyValues } from "lit";
 | 
					import { html, LitElement, nothing, type PropertyValues } from "lit";
 | 
				
			||||||
import { customElement, property, query } from "lit/decorators";
 | 
					import { customElement, property, query } from "lit/decorators";
 | 
				
			||||||
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 { computeDomain } from "../../common/entity/compute_domain";
 | 
				
			||||||
import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
 | 
					import { computeEntityNameList } from "../../common/entity/compute_entity_name_display";
 | 
				
			||||||
 | 
					import { computeStateName } from "../../common/entity/compute_state_name";
 | 
				
			||||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
 | 
					import { isValidEntityId } from "../../common/entity/valid_entity_id";
 | 
				
			||||||
import { computeRTL } from "../../common/util/compute_rtl";
 | 
					import { computeRTL } from "../../common/util/compute_rtl";
 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getEntities,
 | 
					 | 
				
			||||||
  type EntityComboBoxItem,
 | 
					 | 
				
			||||||
} from "../../data/entity_registry";
 | 
					 | 
				
			||||||
import { domainToName } from "../../data/integration";
 | 
					import { domainToName } from "../../data/integration";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  isHelperDomain,
 | 
					  isHelperDomain,
 | 
				
			||||||
@@ -22,11 +20,21 @@ import type { HomeAssistant } from "../../types";
 | 
				
			|||||||
import "../ha-combo-box-item";
 | 
					import "../ha-combo-box-item";
 | 
				
			||||||
import "../ha-generic-picker";
 | 
					import "../ha-generic-picker";
 | 
				
			||||||
import type { HaGenericPicker } from "../ha-generic-picker";
 | 
					import type { HaGenericPicker } from "../ha-generic-picker";
 | 
				
			||||||
import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box";
 | 
					import type {
 | 
				
			||||||
 | 
					  PickerComboBoxItem,
 | 
				
			||||||
 | 
					  PickerComboBoxSearchFn,
 | 
				
			||||||
 | 
					} from "../ha-picker-combo-box";
 | 
				
			||||||
import type { PickerValueRenderer } from "../ha-picker-field";
 | 
					import type { PickerValueRenderer } from "../ha-picker-field";
 | 
				
			||||||
import "../ha-svg-icon";
 | 
					import "../ha-svg-icon";
 | 
				
			||||||
import "./state-badge";
 | 
					import "./state-badge";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface EntityComboBoxItem extends PickerComboBoxItem {
 | 
				
			||||||
 | 
					  domain_name?: string;
 | 
				
			||||||
 | 
					  stateObj?: HassEntity;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const CREATE_ID = "___create-new-entity___";
 | 
					const CREATE_ID = "___create-new-entity___";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-entity-picker")
 | 
					@customElement("ha-entity-picker")
 | 
				
			||||||
@@ -113,9 +121,6 @@ export class HaEntityPicker extends LitElement {
 | 
				
			|||||||
  @property({ attribute: "hide-clear-icon", type: Boolean })
 | 
					  @property({ attribute: "hide-clear-icon", type: Boolean })
 | 
				
			||||||
  public hideClearIcon = false;
 | 
					  public hideClearIcon = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: "add-button", type: Boolean })
 | 
					 | 
				
			||||||
  public addButton = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @query("ha-generic-picker") private _picker?: HaGenericPicker;
 | 
					  @query("ha-generic-picker") private _picker?: HaGenericPicker;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected firstUpdated(changedProperties: PropertyValues): void {
 | 
					  protected firstUpdated(changedProperties: PropertyValues): void {
 | 
				
			||||||
@@ -250,10 +255,8 @@ export class HaEntityPicker extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getEntitiesMemoized = memoizeOne(getEntities);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _getItems = () =>
 | 
					  private _getItems = () =>
 | 
				
			||||||
    this._getEntitiesMemoized(
 | 
					    this._getEntities(
 | 
				
			||||||
      this.hass,
 | 
					      this.hass,
 | 
				
			||||||
      this.includeDomains,
 | 
					      this.includeDomains,
 | 
				
			||||||
      this.excludeDomains,
 | 
					      this.excludeDomains,
 | 
				
			||||||
@@ -261,10 +264,128 @@ export class HaEntityPicker extends LitElement {
 | 
				
			|||||||
      this.includeDeviceClasses,
 | 
					      this.includeDeviceClasses,
 | 
				
			||||||
      this.includeUnitOfMeasurement,
 | 
					      this.includeUnitOfMeasurement,
 | 
				
			||||||
      this.includeEntities,
 | 
					      this.includeEntities,
 | 
				
			||||||
      this.excludeEntities,
 | 
					      this.excludeEntities
 | 
				
			||||||
      this.value
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _getEntities = memoizeOne(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					      hass: this["hass"],
 | 
				
			||||||
 | 
					      includeDomains: this["includeDomains"],
 | 
				
			||||||
 | 
					      excludeDomains: this["excludeDomains"],
 | 
				
			||||||
 | 
					      entityFilter: this["entityFilter"],
 | 
				
			||||||
 | 
					      includeDeviceClasses: this["includeDeviceClasses"],
 | 
				
			||||||
 | 
					      includeUnitOfMeasurement: this["includeUnitOfMeasurement"],
 | 
				
			||||||
 | 
					      includeEntities: this["includeEntities"],
 | 
				
			||||||
 | 
					      excludeEntities: this["excludeEntities"]
 | 
				
			||||||
 | 
					    ): EntityComboBoxItem[] => {
 | 
				
			||||||
 | 
					      let items: EntityComboBoxItem[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let entityIds = Object.keys(hass.states);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (includeEntities) {
 | 
				
			||||||
 | 
					        entityIds = entityIds.filter((entityId) =>
 | 
				
			||||||
 | 
					          includeEntities.includes(entityId)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (excludeEntities) {
 | 
				
			||||||
 | 
					        entityIds = entityIds.filter(
 | 
				
			||||||
 | 
					          (entityId) => !excludeEntities.includes(entityId)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (includeDomains) {
 | 
				
			||||||
 | 
					        entityIds = entityIds.filter((eid) =>
 | 
				
			||||||
 | 
					          includeDomains.includes(computeDomain(eid))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (excludeDomains) {
 | 
				
			||||||
 | 
					        entityIds = entityIds.filter(
 | 
				
			||||||
 | 
					          (eid) => !excludeDomains.includes(computeDomain(eid))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const isRTL = computeRTL(hass);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      items = entityIds.map<EntityComboBoxItem>((entityId) => {
 | 
				
			||||||
 | 
					        const stateObj = hass.states[entityId];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const friendlyName = computeStateName(stateObj); // Keep this for search
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const [entityName, deviceName, areaName] = computeEntityNameList(
 | 
				
			||||||
 | 
					          stateObj,
 | 
				
			||||||
 | 
					          [{ type: "entity" }, { type: "device" }, { type: "area" }],
 | 
				
			||||||
 | 
					          hass.entities,
 | 
				
			||||||
 | 
					          hass.devices,
 | 
				
			||||||
 | 
					          hass.areas,
 | 
				
			||||||
 | 
					          hass.floors
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const domainName = domainToName(hass.localize, computeDomain(entityId));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        const primary = entityName || deviceName || entityId;
 | 
				
			||||||
 | 
					        const secondary = [areaName, entityName ? deviceName : undefined]
 | 
				
			||||||
 | 
					          .filter(Boolean)
 | 
				
			||||||
 | 
					          .join(isRTL ? " ◂ " : " ▸ ");
 | 
				
			||||||
 | 
					        const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        return {
 | 
				
			||||||
 | 
					          id: entityId,
 | 
				
			||||||
 | 
					          primary: primary,
 | 
				
			||||||
 | 
					          secondary: secondary,
 | 
				
			||||||
 | 
					          domain_name: domainName,
 | 
				
			||||||
 | 
					          sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
 | 
				
			||||||
 | 
					          search_labels: [
 | 
				
			||||||
 | 
					            entityName,
 | 
				
			||||||
 | 
					            deviceName,
 | 
				
			||||||
 | 
					            areaName,
 | 
				
			||||||
 | 
					            domainName,
 | 
				
			||||||
 | 
					            friendlyName,
 | 
				
			||||||
 | 
					            entityId,
 | 
				
			||||||
 | 
					          ].filter(Boolean) as string[],
 | 
				
			||||||
 | 
					          a11y_label: a11yLabel,
 | 
				
			||||||
 | 
					          stateObj: stateObj,
 | 
				
			||||||
 | 
					        };
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (includeDeviceClasses) {
 | 
				
			||||||
 | 
					        items = items.filter(
 | 
				
			||||||
 | 
					          (item) =>
 | 
				
			||||||
 | 
					            // We always want to include the entity of the current value
 | 
				
			||||||
 | 
					            item.id === this.value ||
 | 
				
			||||||
 | 
					            (item.stateObj?.attributes.device_class &&
 | 
				
			||||||
 | 
					              includeDeviceClasses.includes(
 | 
				
			||||||
 | 
					                item.stateObj.attributes.device_class
 | 
				
			||||||
 | 
					              ))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (includeUnitOfMeasurement) {
 | 
				
			||||||
 | 
					        items = items.filter(
 | 
				
			||||||
 | 
					          (item) =>
 | 
				
			||||||
 | 
					            // We always want to include the entity of the current value
 | 
				
			||||||
 | 
					            item.id === this.value ||
 | 
				
			||||||
 | 
					            (item.stateObj?.attributes.unit_of_measurement &&
 | 
				
			||||||
 | 
					              includeUnitOfMeasurement.includes(
 | 
				
			||||||
 | 
					                item.stateObj.attributes.unit_of_measurement
 | 
				
			||||||
 | 
					              ))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (entityFilter) {
 | 
				
			||||||
 | 
					        items = items.filter(
 | 
				
			||||||
 | 
					          (item) =>
 | 
				
			||||||
 | 
					            // We always want to include the entity of the current value
 | 
				
			||||||
 | 
					            item.id === this.value ||
 | 
				
			||||||
 | 
					            (item.stateObj && entityFilter!(item.stateObj))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return items;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected render() {
 | 
					  protected render() {
 | 
				
			||||||
    const placeholder =
 | 
					    const placeholder =
 | 
				
			||||||
      this.placeholder ??
 | 
					      this.placeholder ??
 | 
				
			||||||
@@ -284,7 +405,7 @@ export class HaEntityPicker extends LitElement {
 | 
				
			|||||||
        .searchLabel=${this.searchLabel}
 | 
					        .searchLabel=${this.searchLabel}
 | 
				
			||||||
        .notFoundLabel=${notFoundLabel}
 | 
					        .notFoundLabel=${notFoundLabel}
 | 
				
			||||||
        .placeholder=${placeholder}
 | 
					        .placeholder=${placeholder}
 | 
				
			||||||
        .value=${this.addButton ? undefined : this.value}
 | 
					        .value=${this.value}
 | 
				
			||||||
        .rowRenderer=${this._rowRenderer}
 | 
					        .rowRenderer=${this._rowRenderer}
 | 
				
			||||||
        .getItems=${this._getItems}
 | 
					        .getItems=${this._getItems}
 | 
				
			||||||
        .getAdditionalItems=${this._getAdditionalItems}
 | 
					        .getAdditionalItems=${this._getAdditionalItems}
 | 
				
			||||||
@@ -292,9 +413,6 @@ export class HaEntityPicker extends LitElement {
 | 
				
			|||||||
        .searchFn=${this._searchFn}
 | 
					        .searchFn=${this._searchFn}
 | 
				
			||||||
        .valueRenderer=${this._valueRenderer}
 | 
					        .valueRenderer=${this._valueRenderer}
 | 
				
			||||||
        @value-changed=${this._valueChanged}
 | 
					        @value-changed=${this._valueChanged}
 | 
				
			||||||
        .addButtonLabel=${this.addButton
 | 
					 | 
				
			||||||
          ? this.hass.localize("ui.components.entity.entity-picker.add")
 | 
					 | 
				
			||||||
          : undefined}
 | 
					 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
      </ha-generic-picker>
 | 
					      </ha-generic-picker>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,39 +1,23 @@
 | 
				
			|||||||
import "@material/mwc-menu/mwc-menu-surface";
 | 
					import { mdiDrag } from "@mdi/js";
 | 
				
			||||||
import { mdiDragHorizontalVariant, mdiPlus } from "@mdi/js";
 | 
					 | 
				
			||||||
import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
					 | 
				
			||||||
import type { IFuseOptions } from "fuse.js";
 | 
					 | 
				
			||||||
import Fuse from "fuse.js";
 | 
					 | 
				
			||||||
import type { HassEntity } from "home-assistant-js-websocket";
 | 
					import type { HassEntity } from "home-assistant-js-websocket";
 | 
				
			||||||
import { css, html, LitElement, nothing } from "lit";
 | 
					import type { PropertyValues } from "lit";
 | 
				
			||||||
 | 
					import { LitElement, css, html, nothing } from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
import { repeat } from "lit/directives/repeat";
 | 
					import { repeat } from "lit/directives/repeat";
 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
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 { stopPropagation } from "../../common/dom/stop_propagation";
 | 
					 | 
				
			||||||
import { computeDomain } from "../../common/entity/compute_domain";
 | 
					import { computeDomain } from "../../common/entity/compute_domain";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  STATE_DISPLAY_SPECIAL_CONTENT,
 | 
					  STATE_DISPLAY_SPECIAL_CONTENT,
 | 
				
			||||||
  STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
 | 
					  STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS,
 | 
				
			||||||
} from "../../state-display/state-display";
 | 
					} from "../../state-display/state-display";
 | 
				
			||||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
					import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
				
			||||||
import "../chips/ha-assist-chip";
 | 
					 | 
				
			||||||
import "../chips/ha-chip-set";
 | 
					 | 
				
			||||||
import "../chips/ha-input-chip";
 | 
					 | 
				
			||||||
import "../ha-combo-box";
 | 
					import "../ha-combo-box";
 | 
				
			||||||
import type { HaComboBox } from "../ha-combo-box";
 | 
					 | 
				
			||||||
import "../ha-sortable";
 | 
					import "../ha-sortable";
 | 
				
			||||||
 | 
					import "../chips/ha-input-chip";
 | 
				
			||||||
interface StateContentOption {
 | 
					import "../chips/ha-chip-set";
 | 
				
			||||||
  primary: string;
 | 
					import type { HaComboBox } from "../ha-combo-box";
 | 
				
			||||||
  value: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
const rowRenderer: ComboBoxLitRenderer<StateContentOption> = (item) => html`
 | 
					 | 
				
			||||||
  <ha-combo-box-item type="button">
 | 
					 | 
				
			||||||
    <span slot="headline">${item.primary}</span>
 | 
					 | 
				
			||||||
  </ha-combo-box-item>
 | 
					 | 
				
			||||||
`;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
const HIDDEN_ATTRIBUTES = [
 | 
					const HIDDEN_ATTRIBUTES = [
 | 
				
			||||||
  "access_token",
 | 
					  "access_token",
 | 
				
			||||||
@@ -90,7 +74,7 @@ const HIDDEN_ATTRIBUTES = [
 | 
				
			|||||||
];
 | 
					];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-entity-state-content-picker")
 | 
					@customElement("ha-entity-state-content-picker")
 | 
				
			||||||
export class HaStateContentPicker extends LitElement {
 | 
					class HaEntityStatePicker extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false }) public entityId?: string;
 | 
					  @property({ attribute: false }) public entityId?: string;
 | 
				
			||||||
@@ -111,28 +95,26 @@ export class HaStateContentPicker extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property() public helper?: string;
 | 
					  @property() public helper?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query(".container", true) private _container?: HTMLDivElement;
 | 
					  @state() private _opened = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("ha-combo-box", true) private _comboBox!: HaComboBox;
 | 
					  @query("ha-combo-box", true) private _comboBox!: HaComboBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _opened = false;
 | 
					  protected shouldUpdate(changedProps: PropertyValues) {
 | 
				
			||||||
 | 
					    return !(!changedProps.has("_opened") && this._opened);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _editIndex?: number;
 | 
					  private options = memoizeOne(
 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _options = memoizeOne(
 | 
					 | 
				
			||||||
    (entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
 | 
					    (entityId?: string, stateObj?: HassEntity, allowName?: boolean) => {
 | 
				
			||||||
      const domain = entityId ? computeDomain(entityId) : undefined;
 | 
					      const domain = entityId ? computeDomain(entityId) : undefined;
 | 
				
			||||||
      return [
 | 
					      return [
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          primary: this.hass.localize(
 | 
					          label: this.hass.localize("ui.components.state-content-picker.state"),
 | 
				
			||||||
            "ui.components.state-content-picker.state"
 | 
					 | 
				
			||||||
          ),
 | 
					 | 
				
			||||||
          value: "state",
 | 
					          value: "state",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        ...(allowName
 | 
					        ...(allowName
 | 
				
			||||||
          ? [
 | 
					          ? [
 | 
				
			||||||
              {
 | 
					              {
 | 
				
			||||||
                primary: this.hass.localize(
 | 
					                label: this.hass.localize(
 | 
				
			||||||
                  "ui.components.state-content-picker.name"
 | 
					                  "ui.components.state-content-picker.name"
 | 
				
			||||||
                ),
 | 
					                ),
 | 
				
			||||||
                value: "name",
 | 
					                value: "name",
 | 
				
			||||||
@@ -140,13 +122,13 @@ export class HaStateContentPicker extends LitElement {
 | 
				
			|||||||
            ]
 | 
					            ]
 | 
				
			||||||
          : []),
 | 
					          : []),
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          primary: this.hass.localize(
 | 
					          label: this.hass.localize(
 | 
				
			||||||
            "ui.components.state-content-picker.last_changed"
 | 
					            "ui.components.state-content-picker.last_changed"
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          value: "last_changed",
 | 
					          value: "last_changed",
 | 
				
			||||||
        },
 | 
					        },
 | 
				
			||||||
        {
 | 
					        {
 | 
				
			||||||
          primary: this.hass.localize(
 | 
					          label: this.hass.localize(
 | 
				
			||||||
            "ui.components.state-content-picker.last_updated"
 | 
					            "ui.components.state-content-picker.last_updated"
 | 
				
			||||||
          ),
 | 
					          ),
 | 
				
			||||||
          value: "last_updated",
 | 
					          value: "last_updated",
 | 
				
			||||||
@@ -155,7 +137,7 @@ export class HaStateContentPicker extends LitElement {
 | 
				
			|||||||
          ? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
 | 
					          ? STATE_DISPLAY_SPECIAL_CONTENT.filter((content) =>
 | 
				
			||||||
              STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
 | 
					              STATE_DISPLAY_SPECIAL_CONTENT_DOMAINS[domain]?.includes(content)
 | 
				
			||||||
            ).map((content) => ({
 | 
					            ).map((content) => ({
 | 
				
			||||||
              primary: this.hass.localize(
 | 
					              label: this.hass.localize(
 | 
				
			||||||
                `ui.components.state-content-picker.${content}`
 | 
					                `ui.components.state-content-picker.${content}`
 | 
				
			||||||
              ),
 | 
					              ),
 | 
				
			||||||
              value: content,
 | 
					              value: content,
 | 
				
			||||||
@@ -164,201 +146,105 @@ export class HaStateContentPicker extends LitElement {
 | 
				
			|||||||
        ...Object.keys(stateObj?.attributes ?? {})
 | 
					        ...Object.keys(stateObj?.attributes ?? {})
 | 
				
			||||||
          .filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
 | 
					          .filter((a) => !HIDDEN_ATTRIBUTES.includes(a))
 | 
				
			||||||
          .map((attribute) => ({
 | 
					          .map((attribute) => ({
 | 
				
			||||||
            primary: this.hass.formatEntityAttributeName(stateObj!, attribute),
 | 
					 | 
				
			||||||
            value: attribute,
 | 
					            value: attribute,
 | 
				
			||||||
 | 
					            label: this.hass.formatEntityAttributeName(stateObj!, attribute),
 | 
				
			||||||
          })),
 | 
					          })),
 | 
				
			||||||
      ] satisfies StateContentOption[];
 | 
					      ];
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _filter = "";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected render() {
 | 
					  protected render() {
 | 
				
			||||||
 | 
					    if (!this.hass) {
 | 
				
			||||||
 | 
					      return nothing;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const value = this._value;
 | 
					    const value = this._value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const stateObj = this.entityId
 | 
					    const stateObj = this.entityId
 | 
				
			||||||
      ? this.hass.states[this.entityId]
 | 
					      ? this.hass.states[this.entityId]
 | 
				
			||||||
      : undefined;
 | 
					      : undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const options = this._options(this.entityId, stateObj, this.allowName);
 | 
					    const options = this.options(this.entityId, stateObj, this.allowName);
 | 
				
			||||||
 | 
					    const optionItems = options.filter(
 | 
				
			||||||
 | 
					      (option) => !this._value.includes(option.value)
 | 
				
			||||||
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
					      ${value?.length
 | 
				
			||||||
      <div class="container ${this.disabled ? "disabled" : ""}">
 | 
					        ? html`
 | 
				
			||||||
        <ha-sortable
 | 
					            <ha-sortable
 | 
				
			||||||
          no-style
 | 
					              no-style
 | 
				
			||||||
          @item-moved=${this._moveItem}
 | 
					              @item-moved=${this._moveItem}
 | 
				
			||||||
          .disabled=${this.disabled}
 | 
					              .disabled=${this.disabled}
 | 
				
			||||||
          handle-selector="button.primary.action"
 | 
					              handle-selector="button.primary.action"
 | 
				
			||||||
          filter=".add"
 | 
					            >
 | 
				
			||||||
        >
 | 
					              <ha-chip-set>
 | 
				
			||||||
          <ha-chip-set>
 | 
					                ${repeat(
 | 
				
			||||||
            ${repeat(
 | 
					                  this._value,
 | 
				
			||||||
              this._value,
 | 
					                  (item) => item,
 | 
				
			||||||
              (item) => item,
 | 
					                  (item, idx) => {
 | 
				
			||||||
              (item: string, idx) => {
 | 
					                    const label =
 | 
				
			||||||
                const label = options.find((o) => o.value === item)?.primary;
 | 
					                      options.find((option) => option.value === item)?.label ||
 | 
				
			||||||
                const isValid = !!label;
 | 
					                      item;
 | 
				
			||||||
                return html`
 | 
					                    return html`
 | 
				
			||||||
                  <ha-input-chip
 | 
					                      <ha-input-chip
 | 
				
			||||||
                    data-idx=${idx}
 | 
					                        .idx=${idx}
 | 
				
			||||||
                    @remove=${this._removeItem}
 | 
					                        @remove=${this._removeItem}
 | 
				
			||||||
                    @click=${this._editItem}
 | 
					                        .label=${label}
 | 
				
			||||||
                    .label=${label || item}
 | 
					                        selected
 | 
				
			||||||
                    .selected=${!this.disabled}
 | 
					                      >
 | 
				
			||||||
                    .disabled=${this.disabled}
 | 
					                        <ha-svg-icon slot="icon" .path=${mdiDrag}></ha-svg-icon>
 | 
				
			||||||
                    class=${!isValid ? "invalid" : ""}
 | 
					                        ${label}
 | 
				
			||||||
                  >
 | 
					                      </ha-input-chip>
 | 
				
			||||||
                    <ha-svg-icon
 | 
					                    `;
 | 
				
			||||||
                      slot="icon"
 | 
					                  }
 | 
				
			||||||
                      .path=${mdiDragHorizontalVariant}
 | 
					                )}
 | 
				
			||||||
                    ></ha-svg-icon>
 | 
					              </ha-chip-set>
 | 
				
			||||||
                  </ha-input-chip>
 | 
					            </ha-sortable>
 | 
				
			||||||
                `;
 | 
					          `
 | 
				
			||||||
              }
 | 
					        : nothing}
 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
            ${this.disabled
 | 
					 | 
				
			||||||
              ? nothing
 | 
					 | 
				
			||||||
              : html`
 | 
					 | 
				
			||||||
                  <ha-assist-chip
 | 
					 | 
				
			||||||
                    @click=${this._addItem}
 | 
					 | 
				
			||||||
                    .disabled=${this.disabled}
 | 
					 | 
				
			||||||
                    label=${this.hass.localize(
 | 
					 | 
				
			||||||
                      "ui.components.entity.entity-state-content-picker.add"
 | 
					 | 
				
			||||||
                    )}
 | 
					 | 
				
			||||||
                    class="add"
 | 
					 | 
				
			||||||
                  >
 | 
					 | 
				
			||||||
                    <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>
 | 
					 | 
				
			||||||
                  </ha-assist-chip>
 | 
					 | 
				
			||||||
                `}
 | 
					 | 
				
			||||||
          </ha-chip-set>
 | 
					 | 
				
			||||||
        </ha-sortable>
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        <mwc-menu-surface
 | 
					      <ha-combo-box
 | 
				
			||||||
          .open=${this._opened}
 | 
					        item-value-path="value"
 | 
				
			||||||
          @closed=${this._onClosed}
 | 
					        item-label-path="label"
 | 
				
			||||||
          @opened=${this._onOpened}
 | 
					        .hass=${this.hass}
 | 
				
			||||||
          @input=${stopPropagation}
 | 
					        .label=${this.label}
 | 
				
			||||||
          .anchor=${this._container}
 | 
					        .helper=${this.helper}
 | 
				
			||||||
        >
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
          <ha-combo-box
 | 
					        .required=${this.required && !value.length}
 | 
				
			||||||
            .hass=${this.hass}
 | 
					        .value=${""}
 | 
				
			||||||
            .value=${""}
 | 
					        .items=${optionItems}
 | 
				
			||||||
            .autofocus=${this.autofocus}
 | 
					        allow-custom-value
 | 
				
			||||||
            .disabled=${this.disabled || !this.entityId}
 | 
					        @filter-changed=${this._filterChanged}
 | 
				
			||||||
            .required=${this.required && !value.length}
 | 
					        @value-changed=${this._comboBoxValueChanged}
 | 
				
			||||||
            .helper=${this.helper}
 | 
					        @opened-changed=${this._openedChanged}
 | 
				
			||||||
            .items=${options}
 | 
					      ></ha-combo-box>
 | 
				
			||||||
            allow-custom-value
 | 
					 | 
				
			||||||
            item-id-path="value"
 | 
					 | 
				
			||||||
            item-value-path="value"
 | 
					 | 
				
			||||||
            item-label-path="primary"
 | 
					 | 
				
			||||||
            .renderer=${rowRenderer}
 | 
					 | 
				
			||||||
            @opened-changed=${this._openedChanged}
 | 
					 | 
				
			||||||
            @value-changed=${this._comboBoxValueChanged}
 | 
					 | 
				
			||||||
            @filter-changed=${this._filterChanged}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
          </ha-combo-box>
 | 
					 | 
				
			||||||
        </mwc-menu-surface>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _onClosed(ev) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    this._opened = false;
 | 
					 | 
				
			||||||
    this._editIndex = undefined;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _onOpened(ev) {
 | 
					 | 
				
			||||||
    if (!this._opened) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    this._opened = true;
 | 
					 | 
				
			||||||
    await this._comboBox?.focus();
 | 
					 | 
				
			||||||
    await this._comboBox?.open();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _addItem(ev) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    this._opened = true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _editItem(ev) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    const idx = parseInt(ev.currentTarget.dataset.idx, 10);
 | 
					 | 
				
			||||||
    this._editIndex = idx;
 | 
					 | 
				
			||||||
    this._opened = true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private get _value() {
 | 
					  private get _value() {
 | 
				
			||||||
    return !this.value ? [] : ensureArray(this.value);
 | 
					    return !this.value ? [] : ensureArray(this.value);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _toValue = memoizeOne((value: string[]): typeof this.value => {
 | 
					 | 
				
			||||||
    if (value.length === 0) {
 | 
					 | 
				
			||||||
      return undefined;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (value.length === 1) {
 | 
					 | 
				
			||||||
      return value[0];
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return value;
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _openedChanged(ev: ValueChangedEvent<boolean>) {
 | 
					  private _openedChanged(ev: ValueChangedEvent<boolean>) {
 | 
				
			||||||
    const open = ev.detail.value;
 | 
					    this._opened = ev.detail.value;
 | 
				
			||||||
    if (open) {
 | 
					    this._comboBox.filteredItems = this._comboBox.items;
 | 
				
			||||||
      const options = this._comboBox.items || [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const initialValue =
 | 
					 | 
				
			||||||
        this._editIndex != null ? this._value[this._editIndex] : "";
 | 
					 | 
				
			||||||
      const filteredItems = this._filterSelectedOptions(options, initialValue);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this._comboBox.filteredItems = filteredItems;
 | 
					 | 
				
			||||||
      this._comboBox.setInputValue(initialValue);
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this._opened = false;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _filterSelectedOptions = (
 | 
					  private _filterChanged(ev?: CustomEvent): void {
 | 
				
			||||||
    options: StateContentOption[],
 | 
					    this._filter = ev?.detail.value || "";
 | 
				
			||||||
    current?: string
 | 
					 | 
				
			||||||
  ) => {
 | 
					 | 
				
			||||||
    const value = this._value;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return options.filter(
 | 
					    const filteredItems = this._comboBox.items?.filter((item) => {
 | 
				
			||||||
      (option) => !value.includes(option.value) || option.value === current
 | 
					      const label = item.label || item.value;
 | 
				
			||||||
    );
 | 
					      return label.toLowerCase().includes(this._filter?.toLowerCase());
 | 
				
			||||||
  };
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _filterChanged(ev: ValueChangedEvent<string>) {
 | 
					    if (this._filter) {
 | 
				
			||||||
    const input = ev.detail.value;
 | 
					      filteredItems?.unshift({ label: this._filter, value: this._filter });
 | 
				
			||||||
    const filter = input?.toLowerCase() || "";
 | 
					 | 
				
			||||||
    const options = this._comboBox.items || [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const currentValue =
 | 
					 | 
				
			||||||
      this._editIndex != null ? this._value[this._editIndex] : "";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._comboBox.filteredItems = this._filterSelectedOptions(
 | 
					 | 
				
			||||||
      options,
 | 
					 | 
				
			||||||
      currentValue
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!filter) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const fuseOptions: IFuseOptions<StateContentOption> = {
 | 
					 | 
				
			||||||
      keys: ["primary", "secondary", "value"],
 | 
					 | 
				
			||||||
      isCaseSensitive: false,
 | 
					 | 
				
			||||||
      minMatchCharLength: Math.min(filter.length, 2),
 | 
					 | 
				
			||||||
      threshold: 0.2,
 | 
					 | 
				
			||||||
      ignoreDiacritics: true,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions);
 | 
					 | 
				
			||||||
    const filteredItems = fuse.search(filter).map((result) => result.item);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._comboBox.filteredItems = filteredItems;
 | 
					    this._comboBox.filteredItems = filteredItems;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -371,40 +257,43 @@ export class HaStateContentPicker extends LitElement {
 | 
				
			|||||||
    newValue.splice(newIndex, 0, element);
 | 
					    newValue.splice(newIndex, 0, element);
 | 
				
			||||||
    this._setValue(newValue);
 | 
					    this._setValue(newValue);
 | 
				
			||||||
    await this.updateComplete;
 | 
					    await this.updateComplete;
 | 
				
			||||||
    this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
 | 
					    this._filterChanged();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private async _removeItem(ev) {
 | 
					  private async _removeItem(ev) {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    const value = [...this._value];
 | 
					    const value: string[] = [...this._value];
 | 
				
			||||||
    const idx = parseInt(ev.target.dataset.idx, 10);
 | 
					    value.splice(ev.target.idx, 1);
 | 
				
			||||||
    value.splice(idx, 1);
 | 
					 | 
				
			||||||
    this._setValue(value);
 | 
					    this._setValue(value);
 | 
				
			||||||
    await this.updateComplete;
 | 
					    await this.updateComplete;
 | 
				
			||||||
    this._filterChanged({ detail: { value: "" } } as ValueChangedEvent<string>);
 | 
					    this._filterChanged();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _comboBoxValueChanged(ev: ValueChangedEvent<string>): void {
 | 
					  private _comboBoxValueChanged(ev: CustomEvent): void {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    const value = ev.detail.value;
 | 
					    const newValue = ev.detail.value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.disabled || value === "") {
 | 
					    if (this.disabled || newValue === "") {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const newValue = [...this._value];
 | 
					    const currentValue = this._value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this._editIndex != null) {
 | 
					    if (currentValue.includes(newValue)) {
 | 
				
			||||||
      newValue[this._editIndex] = value;
 | 
					      return;
 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      newValue.push(value);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._setValue(newValue);
 | 
					    setTimeout(() => {
 | 
				
			||||||
 | 
					      this._filterChanged();
 | 
				
			||||||
 | 
					      this._comboBox.setInputValue("");
 | 
				
			||||||
 | 
					    }, 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    this._setValue([...currentValue, newValue]);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _setValue(value: string[]) {
 | 
					  private _setValue(value: string[]) {
 | 
				
			||||||
    const newValue = this._toValue(value);
 | 
					    const newValue =
 | 
				
			||||||
 | 
					      value.length === 0 ? undefined : value.length === 1 ? value[0] : value;
 | 
				
			||||||
    this.value = newValue;
 | 
					    this.value = newValue;
 | 
				
			||||||
    fireEvent(this, "value-changed", {
 | 
					    fireEvent(this, "value-changed", {
 | 
				
			||||||
      value: newValue,
 | 
					      value: newValue,
 | 
				
			||||||
@@ -414,64 +303,10 @@ export class HaStateContentPicker extends LitElement {
 | 
				
			|||||||
  static styles = css`
 | 
					  static styles = css`
 | 
				
			||||||
    :host {
 | 
					    :host {
 | 
				
			||||||
      position: relative;
 | 
					      position: relative;
 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .container {
 | 
					 | 
				
			||||||
      position: relative;
 | 
					 | 
				
			||||||
      background-color: var(--mdc-text-field-fill-color, whitesmoke);
 | 
					 | 
				
			||||||
      border-radius: var(--ha-border-radius-sm);
 | 
					 | 
				
			||||||
      border-end-end-radius: var(--ha-border-radius-square);
 | 
					 | 
				
			||||||
      border-end-start-radius: var(--ha-border-radius-square);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .container:after {
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
      content: "";
 | 
					 | 
				
			||||||
      position: absolute;
 | 
					 | 
				
			||||||
      pointer-events: none;
 | 
					 | 
				
			||||||
      bottom: 0;
 | 
					 | 
				
			||||||
      left: 0;
 | 
					 | 
				
			||||||
      right: 0;
 | 
					 | 
				
			||||||
      height: 1px;
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
      background-color: var(
 | 
					 | 
				
			||||||
        --mdc-text-field-idle-line-color,
 | 
					 | 
				
			||||||
        rgba(0, 0, 0, 0.42)
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      transform:
 | 
					 | 
				
			||||||
        height 180ms ease-in-out,
 | 
					 | 
				
			||||||
        background-color 180ms ease-in-out;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .container.disabled:after {
 | 
					 | 
				
			||||||
      background-color: var(
 | 
					 | 
				
			||||||
        --mdc-text-field-disabled-line-color,
 | 
					 | 
				
			||||||
        rgba(0, 0, 0, 0.42)
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .container:focus-within:after {
 | 
					 | 
				
			||||||
      height: 2px;
 | 
					 | 
				
			||||||
      background-color: var(--mdc-theme-primary);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    label {
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
      margin: 0 0 var(--ha-space-2);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .add {
 | 
					 | 
				
			||||||
      order: 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    mwc-menu-surface {
 | 
					 | 
				
			||||||
      --mdc-menu-min-width: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    ha-chip-set {
 | 
					    ha-chip-set {
 | 
				
			||||||
      padding: var(--ha-space-2) var(--ha-space-2);
 | 
					      padding: 8px 0;
 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    .invalid {
 | 
					 | 
				
			||||||
      text-decoration: line-through;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    .sortable-fallback {
 | 
					    .sortable-fallback {
 | 
				
			||||||
@@ -491,6 +326,6 @@ export class HaStateContentPicker extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
    "ha-entity-state-content-picker": HaStateContentPicker;
 | 
					    "ha-entity-state-content-picker": HaEntityStatePicker;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,3 +1,4 @@
 | 
				
			|||||||
 | 
					import type { HassEntity } from "home-assistant-js-websocket";
 | 
				
			||||||
import type { PropertyValues } from "lit";
 | 
					import type { PropertyValues } from "lit";
 | 
				
			||||||
import { LitElement, html, nothing } from "lit";
 | 
					import { LitElement, html, nothing } from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
@@ -8,6 +9,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
				
			|||||||
import "../ha-combo-box";
 | 
					import "../ha-combo-box";
 | 
				
			||||||
import type { HaComboBox } from "../ha-combo-box";
 | 
					import type { HaComboBox } from "../ha-combo-box";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface StateOption {
 | 
					interface StateOption {
 | 
				
			||||||
  value: string;
 | 
					  value: string;
 | 
				
			||||||
  label: string;
 | 
					  label: string;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -4,7 +4,6 @@ import { customElement, property } from "lit/decorators";
 | 
				
			|||||||
import { keyed } from "lit/directives/keyed";
 | 
					import { keyed } from "lit/directives/keyed";
 | 
				
			||||||
import { repeat } from "lit/directives/repeat";
 | 
					import { repeat } from "lit/directives/repeat";
 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
import { ANY_STATE_VALUE } from "./const";
 | 
					 | 
				
			||||||
import { ensureArray } from "../../common/array/ensure-array";
 | 
					import { ensureArray } from "../../common/array/ensure-array";
 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
import "./ha-entity-state-picker";
 | 
					import "./ha-entity-state-picker";
 | 
				
			||||||
@@ -58,7 +57,6 @@ export class HaEntityStatesPicker extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    const value = this.value || [];
 | 
					    const value = this.value || [];
 | 
				
			||||||
    const hide = [...(this.hideStates || []), ...value];
 | 
					    const hide = [...(this.hideStates || []), ...value];
 | 
				
			||||||
    const hideValue = value.includes(ANY_STATE_VALUE);
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      ${repeat(
 | 
					      ${repeat(
 | 
				
			||||||
@@ -86,7 +84,7 @@ export class HaEntityStatesPicker extends LitElement {
 | 
				
			|||||||
        `
 | 
					        `
 | 
				
			||||||
      )}
 | 
					      )}
 | 
				
			||||||
      <div>
 | 
					      <div>
 | 
				
			||||||
        ${(this.disabled && value.length) || hideValue
 | 
					        ${this.disabled && value.length
 | 
				
			||||||
          ? nothing
 | 
					          ? nothing
 | 
				
			||||||
          : keyed(
 | 
					          : keyed(
 | 
				
			||||||
              value.length,
 | 
					              value.length,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -46,7 +46,7 @@ export class HaAnalytics extends LitElement {
 | 
				
			|||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <ha-switch
 | 
					        <ha-switch
 | 
				
			||||||
          @change=${this._handleRowClick}
 | 
					          @change=${this._handleRowClick}
 | 
				
			||||||
          .checked=${!!baseEnabled}
 | 
					          .checked=${baseEnabled}
 | 
				
			||||||
          .preference=${"base"}
 | 
					          .preference=${"base"}
 | 
				
			||||||
          .disabled=${loading}
 | 
					          .disabled=${loading}
 | 
				
			||||||
          name="base"
 | 
					          name="base"
 | 
				
			||||||
@@ -70,7 +70,7 @@ export class HaAnalytics extends LitElement {
 | 
				
			|||||||
              <ha-switch
 | 
					              <ha-switch
 | 
				
			||||||
                .id="switch-${preference}"
 | 
					                .id="switch-${preference}"
 | 
				
			||||||
                @change=${this._handleRowClick}
 | 
					                @change=${this._handleRowClick}
 | 
				
			||||||
                .checked=${!!this.analytics?.preferences[preference]}
 | 
					                .checked=${this.analytics?.preferences[preference]}
 | 
				
			||||||
                .preference=${preference}
 | 
					                .preference=${preference}
 | 
				
			||||||
                name=${preference}
 | 
					                name=${preference}
 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
@@ -102,7 +102,7 @@ export class HaAnalytics extends LitElement {
 | 
				
			|||||||
        </span>
 | 
					        </span>
 | 
				
			||||||
        <ha-switch
 | 
					        <ha-switch
 | 
				
			||||||
          @change=${this._handleRowClick}
 | 
					          @change=${this._handleRowClick}
 | 
				
			||||||
          .checked=${!!this.analytics?.preferences.diagnostics}
 | 
					          .checked=${this.analytics?.preferences.diagnostics}
 | 
				
			||||||
          .preference=${"diagnostics"}
 | 
					          .preference=${"diagnostics"}
 | 
				
			||||||
          .disabled=${loading}
 | 
					          .disabled=${loading}
 | 
				
			||||||
          name="diagnostics"
 | 
					          name="diagnostics"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -8,13 +8,21 @@ import { styleMap } from "lit/directives/style-map";
 | 
				
			|||||||
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 { computeAreaName } from "../common/entity/compute_area_name";
 | 
					import { computeAreaName } from "../common/entity/compute_area_name";
 | 
				
			||||||
 | 
					import { computeDomain } from "../common/entity/compute_domain";
 | 
				
			||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
 | 
					import { computeFloorName } from "../common/entity/compute_floor_name";
 | 
				
			||||||
 | 
					import { stringCompare } from "../common/string/compare";
 | 
				
			||||||
import { computeRTL } from "../common/util/compute_rtl";
 | 
					import { computeRTL } from "../common/util/compute_rtl";
 | 
				
			||||||
 | 
					import type { AreaRegistryEntry } from "../data/area_registry";
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  DeviceEntityDisplayLookup,
 | 
				
			||||||
 | 
					  DeviceRegistryEntry,
 | 
				
			||||||
 | 
					} from "../data/device_registry";
 | 
				
			||||||
 | 
					import { getDeviceEntityDisplayLookup } from "../data/device_registry";
 | 
				
			||||||
 | 
					import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  getAreasAndFloors,
 | 
					  getFloorAreaLookup,
 | 
				
			||||||
  type AreaFloorValue,
 | 
					  type FloorRegistryEntry,
 | 
				
			||||||
  type FloorComboBoxItem,
 | 
					} from "../data/floor_registry";
 | 
				
			||||||
} from "../data/area_floor";
 | 
					 | 
				
			||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
 | 
					import type { HomeAssistant, ValueChangedEvent } from "../types";
 | 
				
			||||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
					import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
				
			||||||
import "./ha-combo-box-item";
 | 
					import "./ha-combo-box-item";
 | 
				
			||||||
@@ -22,12 +30,24 @@ import "./ha-floor-icon";
 | 
				
			|||||||
import "./ha-generic-picker";
 | 
					import "./ha-generic-picker";
 | 
				
			||||||
import type { HaGenericPicker } from "./ha-generic-picker";
 | 
					import type { HaGenericPicker } from "./ha-generic-picker";
 | 
				
			||||||
import "./ha-icon-button";
 | 
					import "./ha-icon-button";
 | 
				
			||||||
 | 
					import type { PickerComboBoxItem } from "./ha-picker-combo-box";
 | 
				
			||||||
import type { PickerValueRenderer } from "./ha-picker-field";
 | 
					import type { PickerValueRenderer } from "./ha-picker-field";
 | 
				
			||||||
import "./ha-svg-icon";
 | 
					import "./ha-svg-icon";
 | 
				
			||||||
import "./ha-tree-indicator";
 | 
					import "./ha-tree-indicator";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const SEPARATOR = "________";
 | 
					const SEPARATOR = "________";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface FloorComboBoxItem extends PickerComboBoxItem {
 | 
				
			||||||
 | 
					  type: "floor" | "area";
 | 
				
			||||||
 | 
					  floor?: FloorRegistryEntry;
 | 
				
			||||||
 | 
					  area?: AreaRegistryEntry;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					interface AreaFloorValue {
 | 
				
			||||||
 | 
					  id: string;
 | 
				
			||||||
 | 
					  type: "floor" | "area";
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-area-floor-picker")
 | 
					@customElement("ha-area-floor-picker")
 | 
				
			||||||
export class HaAreaFloorPicker extends LitElement {
 | 
					export class HaAreaFloorPicker extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
@@ -134,6 +154,243 @@ export class HaAreaFloorPicker extends LitElement {
 | 
				
			|||||||
    `;
 | 
					    `;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _getAreasAndFloors = memoizeOne(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					      haFloors: HomeAssistant["floors"],
 | 
				
			||||||
 | 
					      haAreas: HomeAssistant["areas"],
 | 
				
			||||||
 | 
					      haDevices: HomeAssistant["devices"],
 | 
				
			||||||
 | 
					      haEntities: HomeAssistant["entities"],
 | 
				
			||||||
 | 
					      includeDomains: this["includeDomains"],
 | 
				
			||||||
 | 
					      excludeDomains: this["excludeDomains"],
 | 
				
			||||||
 | 
					      includeDeviceClasses: this["includeDeviceClasses"],
 | 
				
			||||||
 | 
					      deviceFilter: this["deviceFilter"],
 | 
				
			||||||
 | 
					      entityFilter: this["entityFilter"],
 | 
				
			||||||
 | 
					      excludeAreas: this["excludeAreas"],
 | 
				
			||||||
 | 
					      excludeFloors: this["excludeFloors"]
 | 
				
			||||||
 | 
					    ): FloorComboBoxItem[] => {
 | 
				
			||||||
 | 
					      const floors = Object.values(haFloors);
 | 
				
			||||||
 | 
					      const areas = Object.values(haAreas);
 | 
				
			||||||
 | 
					      const devices = Object.values(haDevices);
 | 
				
			||||||
 | 
					      const entities = Object.values(haEntities);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let deviceEntityLookup: DeviceEntityDisplayLookup = {};
 | 
				
			||||||
 | 
					      let inputDevices: DeviceRegistryEntry[] | undefined;
 | 
				
			||||||
 | 
					      let inputEntities: EntityRegistryDisplayEntry[] | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (
 | 
				
			||||||
 | 
					        includeDomains ||
 | 
				
			||||||
 | 
					        excludeDomains ||
 | 
				
			||||||
 | 
					        includeDeviceClasses ||
 | 
				
			||||||
 | 
					        deviceFilter ||
 | 
				
			||||||
 | 
					        entityFilter
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
 | 
				
			||||||
 | 
					        inputDevices = devices;
 | 
				
			||||||
 | 
					        inputEntities = entities.filter((entity) => entity.area_id);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (includeDomains) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) => {
 | 
				
			||||||
 | 
					            const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					            if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return deviceEntityLookup[device.id].some((entity) =>
 | 
				
			||||||
 | 
					              includeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          inputEntities = inputEntities!.filter((entity) =>
 | 
				
			||||||
 | 
					            includeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (excludeDomains) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) => {
 | 
				
			||||||
 | 
					            const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					            if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					              return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return entities.every(
 | 
				
			||||||
 | 
					              (entity) =>
 | 
				
			||||||
 | 
					                !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          inputEntities = inputEntities!.filter(
 | 
				
			||||||
 | 
					            (entity) =>
 | 
				
			||||||
 | 
					              !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (includeDeviceClasses) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) => {
 | 
				
			||||||
 | 
					            const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					            if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return deviceEntityLookup[device.id].some((entity) => {
 | 
				
			||||||
 | 
					              const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					              if (!stateObj) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              return (
 | 
				
			||||||
 | 
					                stateObj.attributes.device_class &&
 | 
				
			||||||
 | 
					                includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          inputEntities = inputEntities!.filter((entity) => {
 | 
				
			||||||
 | 
					            const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              stateObj.attributes.device_class &&
 | 
				
			||||||
 | 
					              includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (deviceFilter) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) =>
 | 
				
			||||||
 | 
					            deviceFilter!(device)
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (entityFilter) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) => {
 | 
				
			||||||
 | 
					            const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					            if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return deviceEntityLookup[device.id].some((entity) => {
 | 
				
			||||||
 | 
					              const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					              if (!stateObj) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              return entityFilter(stateObj);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          inputEntities = inputEntities!.filter((entity) => {
 | 
				
			||||||
 | 
					            const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					            if (!stateObj) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return entityFilter!(stateObj);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let outputAreas = areas;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let areaIds: string[] | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (inputDevices) {
 | 
				
			||||||
 | 
					        areaIds = inputDevices
 | 
				
			||||||
 | 
					          .filter((device) => device.area_id)
 | 
				
			||||||
 | 
					          .map((device) => device.area_id!);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (inputEntities) {
 | 
				
			||||||
 | 
					        areaIds = (areaIds ?? []).concat(
 | 
				
			||||||
 | 
					          inputEntities
 | 
				
			||||||
 | 
					            .filter((entity) => entity.area_id)
 | 
				
			||||||
 | 
					            .map((entity) => entity.area_id!)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (areaIds) {
 | 
				
			||||||
 | 
					        outputAreas = outputAreas.filter((area) =>
 | 
				
			||||||
 | 
					          areaIds!.includes(area.area_id)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (excludeAreas) {
 | 
				
			||||||
 | 
					        outputAreas = outputAreas.filter(
 | 
				
			||||||
 | 
					          (area) => !excludeAreas!.includes(area.area_id)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (excludeFloors) {
 | 
				
			||||||
 | 
					        outputAreas = outputAreas.filter(
 | 
				
			||||||
 | 
					          (area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const floorAreaLookup = getFloorAreaLookup(outputAreas);
 | 
				
			||||||
 | 
					      const unassisgnedAreas = Object.values(outputAreas).filter(
 | 
				
			||||||
 | 
					        (area) => !area.floor_id || !floorAreaLookup[area.floor_id]
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      // @ts-ignore
 | 
				
			||||||
 | 
					      const floorAreaEntries: [
 | 
				
			||||||
 | 
					        FloorRegistryEntry | undefined,
 | 
				
			||||||
 | 
					        AreaRegistryEntry[],
 | 
				
			||||||
 | 
					      ][] = Object.entries(floorAreaLookup)
 | 
				
			||||||
 | 
					        .map(([floorId, floorAreas]) => {
 | 
				
			||||||
 | 
					          const floor = floors.find((fl) => fl.floor_id === floorId)!;
 | 
				
			||||||
 | 
					          return [floor, floorAreas] as const;
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					        .sort(([floorA], [floorB]) => {
 | 
				
			||||||
 | 
					          if (floorA.level !== floorB.level) {
 | 
				
			||||||
 | 
					            return (floorA.level ?? 0) - (floorB.level ?? 0);
 | 
				
			||||||
 | 
					          }
 | 
				
			||||||
 | 
					          return stringCompare(floorA.name, floorB.name);
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const items: FloorComboBoxItem[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      floorAreaEntries.forEach(([floor, floorAreas]) => {
 | 
				
			||||||
 | 
					        if (floor) {
 | 
				
			||||||
 | 
					          const floorName = computeFloorName(floor);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          const areaSearchLabels = floorAreas
 | 
				
			||||||
 | 
					            .map((area) => {
 | 
				
			||||||
 | 
					              const areaName = computeAreaName(area) || area.area_id;
 | 
				
			||||||
 | 
					              return [area.area_id, areaName, ...area.aliases];
 | 
				
			||||||
 | 
					            })
 | 
				
			||||||
 | 
					            .flat();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					          items.push({
 | 
				
			||||||
 | 
					            id: this._formatValue({ id: floor.floor_id, type: "floor" }),
 | 
				
			||||||
 | 
					            type: "floor",
 | 
				
			||||||
 | 
					            primary: floorName,
 | 
				
			||||||
 | 
					            floor: floor,
 | 
				
			||||||
 | 
					            search_labels: [
 | 
				
			||||||
 | 
					              floor.floor_id,
 | 
				
			||||||
 | 
					              floorName,
 | 
				
			||||||
 | 
					              ...floor.aliases,
 | 
				
			||||||
 | 
					              ...areaSearchLabels,
 | 
				
			||||||
 | 
					            ],
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        items.push(
 | 
				
			||||||
 | 
					          ...floorAreas.map((area) => {
 | 
				
			||||||
 | 
					            const areaName = computeAreaName(area) || area.area_id;
 | 
				
			||||||
 | 
					            return {
 | 
				
			||||||
 | 
					              id: this._formatValue({ id: area.area_id, type: "area" }),
 | 
				
			||||||
 | 
					              type: "area" as const,
 | 
				
			||||||
 | 
					              primary: areaName,
 | 
				
			||||||
 | 
					              area: area,
 | 
				
			||||||
 | 
					              icon: area.icon || undefined,
 | 
				
			||||||
 | 
					              search_labels: [area.area_id, areaName, ...area.aliases],
 | 
				
			||||||
 | 
					            };
 | 
				
			||||||
 | 
					          })
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      items.push(
 | 
				
			||||||
 | 
					        ...unassisgnedAreas.map((area) => {
 | 
				
			||||||
 | 
					          const areaName = computeAreaName(area) || area.area_id;
 | 
				
			||||||
 | 
					          return {
 | 
				
			||||||
 | 
					            id: this._formatValue({ id: area.area_id, type: "area" }),
 | 
				
			||||||
 | 
					            type: "area" as const,
 | 
				
			||||||
 | 
					            primary: areaName,
 | 
				
			||||||
 | 
					            icon: area.icon || undefined,
 | 
				
			||||||
 | 
					            search_labels: [area.area_id, areaName, ...area.aliases],
 | 
				
			||||||
 | 
					          };
 | 
				
			||||||
 | 
					        })
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return items;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
 | 
					  private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = (
 | 
				
			||||||
    item,
 | 
					    item,
 | 
				
			||||||
    { index },
 | 
					    { index },
 | 
				
			||||||
@@ -188,16 +445,12 @@ export class HaAreaFloorPicker extends LitElement {
 | 
				
			|||||||
    `;
 | 
					    `;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _getItems = () =>
 | 
					  private _getItems = () =>
 | 
				
			||||||
    this._getAreasAndFloorsMemoized(
 | 
					    this._getAreasAndFloors(
 | 
				
			||||||
      this.hass.states,
 | 
					 | 
				
			||||||
      this.hass.floors,
 | 
					      this.hass.floors,
 | 
				
			||||||
      this.hass.areas,
 | 
					      this.hass.areas,
 | 
				
			||||||
      this.hass.devices,
 | 
					      this.hass.devices,
 | 
				
			||||||
      this.hass.entities,
 | 
					      this.hass.entities,
 | 
				
			||||||
      this._formatValue,
 | 
					 | 
				
			||||||
      this.includeDomains,
 | 
					      this.includeDomains,
 | 
				
			||||||
      this.excludeDomains,
 | 
					      this.excludeDomains,
 | 
				
			||||||
      this.includeDeviceClasses,
 | 
					      this.includeDeviceClasses,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -107,7 +107,7 @@ export class HaAreaPicker extends LitElement {
 | 
				
			|||||||
          `;
 | 
					          `;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const { floor } = getAreaContext(area, this.hass.floors);
 | 
					        const { floor } = getAreaContext(area, this.hass);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const areaName = area ? computeAreaName(area) : undefined;
 | 
					        const areaName = area ? computeAreaName(area) : undefined;
 | 
				
			||||||
        const floorName = floor ? computeFloorName(floor) : undefined;
 | 
					        const floorName = floor ? computeFloorName(floor) : undefined;
 | 
				
			||||||
@@ -279,7 +279,7 @@ export class HaAreaPicker extends LitElement {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      const items = outputAreas.map<PickerComboBoxItem>((area) => {
 | 
					      const items = outputAreas.map<PickerComboBoxItem>((area) => {
 | 
				
			||||||
        const { floor } = getAreaContext(area, this.hass.floors);
 | 
					        const { floor } = getAreaContext(area, this.hass);
 | 
				
			||||||
        const floorName = floor ? computeFloorName(floor) : undefined;
 | 
					        const floorName = floor ? computeFloorName(floor) : undefined;
 | 
				
			||||||
        const areaName = computeAreaName(area);
 | 
					        const areaName = computeAreaName(area);
 | 
				
			||||||
        return {
 | 
					        return {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const items: DisplayItem[] = areas.map((area) => {
 | 
					    const items: DisplayItem[] = areas.map((area) => {
 | 
				
			||||||
      const { floor } = getAreaContext(area, this.hass.floors);
 | 
					      const { floor } = getAreaContext(area, this.hass!);
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        value: area.area_id,
 | 
					        value: area.area_id,
 | 
				
			||||||
        label: area.name,
 | 
					        label: area.name,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { mdiDragHorizontalVariant, mdiTextureBox } from "@mdi/js";
 | 
					import { mdiDrag, mdiTextureBox } from "@mdi/js";
 | 
				
			||||||
import type { TemplateResult } from "lit";
 | 
					import type { TemplateResult } from "lit";
 | 
				
			||||||
import { LitElement, css, html, nothing } from "lit";
 | 
					import { LitElement, css, html, nothing } from "lit";
 | 
				
			||||||
import { customElement, property } from "lit/decorators";
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
@@ -105,7 +105,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
 | 
				
			|||||||
                      <ha-svg-icon
 | 
					                      <ha-svg-icon
 | 
				
			||||||
                        class="handle"
 | 
					                        class="handle"
 | 
				
			||||||
                        slot="icons"
 | 
					                        slot="icons"
 | 
				
			||||||
                        .path=${mdiDragHorizontalVariant}
 | 
					                        .path=${mdiDrag}
 | 
				
			||||||
                      ></ha-svg-icon>
 | 
					                      ></ha-svg-icon>
 | 
				
			||||||
                    `}
 | 
					                    `}
 | 
				
			||||||
                <ha-items-display-editor
 | 
					                <ha-items-display-editor
 | 
				
			||||||
@@ -138,7 +138,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement {
 | 
				
			|||||||
      );
 | 
					      );
 | 
				
			||||||
      const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
 | 
					      const groupedItems: Record<string, DisplayItem[]> = areas.reduce(
 | 
				
			||||||
        (acc, area) => {
 | 
					        (acc, area) => {
 | 
				
			||||||
          const { floor } = getAreaContext(area, this.hass.floors);
 | 
					          const { floor } = getAreaContext(area, this.hass!);
 | 
				
			||||||
          const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
 | 
					          const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
          if (!acc[floorId]) {
 | 
					          if (!acc[floorId]) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -118,7 +118,7 @@ export class HaAutomationRow extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    .row {
 | 
					    .row {
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
      padding: var(--ha-space-0) var(--ha-space-2);
 | 
					      padding: 0 8px;
 | 
				
			||||||
      min-height: 48px;
 | 
					      min-height: 48px;
 | 
				
			||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
      cursor: pointer;
 | 
					      cursor: pointer;
 | 
				
			||||||
@@ -134,12 +134,12 @@ export class HaAutomationRow extends LitElement {
 | 
				
			|||||||
    .expand-button {
 | 
					    .expand-button {
 | 
				
			||||||
      transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
 | 
					      transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1);
 | 
				
			||||||
      color: var(--ha-color-on-neutral-quiet);
 | 
					      color: var(--ha-color-on-neutral-quiet);
 | 
				
			||||||
      margin-left: calc(var(--ha-space-2) * -1);
 | 
					      margin-left: -8px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    :host([building-block]) .leading-icon-wrapper {
 | 
					    :host([building-block]) .leading-icon-wrapper {
 | 
				
			||||||
      background-color: var(--ha-color-fill-neutral-loud-resting);
 | 
					      background-color: var(--ha-color-fill-neutral-loud-resting);
 | 
				
			||||||
      border-radius: var(--ha-border-radius-md);
 | 
					      border-radius: var(--ha-border-radius-md);
 | 
				
			||||||
      padding: var(--ha-space-1);
 | 
					      padding: 4px;
 | 
				
			||||||
      display: flex;
 | 
					      display: flex;
 | 
				
			||||||
      justify-content: center;
 | 
					      justify-content: center;
 | 
				
			||||||
      align-items: center;
 | 
					      align-items: center;
 | 
				
			||||||
@@ -149,7 +149,7 @@ export class HaAutomationRow extends LitElement {
 | 
				
			|||||||
      color: var(--ha-color-on-neutral-quiet);
 | 
					      color: var(--ha-color-on-neutral-quiet);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    :host([building-block]) ::slotted([slot="leading-icon"]) {
 | 
					    :host([building-block]) ::slotted([slot="leading-icon"]) {
 | 
				
			||||||
      --mdc-icon-size: var(--ha-space-5);
 | 
					      --mdc-icon-size: 20px;
 | 
				
			||||||
      color: var(--white-color);
 | 
					      color: var(--white-color);
 | 
				
			||||||
      transform: rotate(-45deg);
 | 
					      transform: rotate(-45deg);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
@@ -170,7 +170,7 @@ export class HaAutomationRow extends LitElement {
 | 
				
			|||||||
    ::slotted([slot="header"]) {
 | 
					    ::slotted([slot="header"]) {
 | 
				
			||||||
      flex: 1;
 | 
					      flex: 1;
 | 
				
			||||||
      overflow-wrap: anywhere;
 | 
					      overflow-wrap: anywhere;
 | 
				
			||||||
      margin: var(--ha-space-0) var(--ha-space-3);
 | 
					      margin: 0 12px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    :host([sort-selected]) .row {
 | 
					    :host([sort-selected]) .row {
 | 
				
			||||||
      outline: solid;
 | 
					      outline: solid;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,8 +1,6 @@
 | 
				
			|||||||
import "@home-assistant/webawesome/dist/components/drawer/drawer";
 | 
					 | 
				
			||||||
import { css, html, LitElement, type PropertyValues } from "lit";
 | 
					import { css, html, LitElement, type PropertyValues } from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import "@home-assistant/webawesome/dist/components/drawer/drawer";
 | 
				
			||||||
import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer";
 | 
					import { customElement, property, state } from "lit/decorators";
 | 
				
			||||||
import { haStyleScrollbar } from "../resources/styles";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
 | 
					export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -10,17 +8,8 @@ export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300;
 | 
				
			|||||||
export class HaBottomSheet extends LitElement {
 | 
					export class HaBottomSheet extends LitElement {
 | 
				
			||||||
  @property({ type: Boolean }) public open = false;
 | 
					  @property({ type: Boolean }) public open = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ type: Boolean, reflect: true, attribute: "flexcontent" })
 | 
					 | 
				
			||||||
  public flexContent = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _drawerOpen = false;
 | 
					  @state() private _drawerOpen = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("#drawer") private _drawer!: HTMLElement;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _gestureRecognizer = new SwipeGestureRecognizer();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _isDragging = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleAfterHide() {
 | 
					  private _handleAfterHide() {
 | 
				
			||||||
    this.open = false;
 | 
					    this.open = false;
 | 
				
			||||||
    const ev = new Event("closed", {
 | 
					    const ev = new Event("closed", {
 | 
				
			||||||
@@ -40,186 +29,42 @@ export class HaBottomSheet extends LitElement {
 | 
				
			|||||||
  render() {
 | 
					  render() {
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <wa-drawer
 | 
					      <wa-drawer
 | 
				
			||||||
        id="drawer"
 | 
					 | 
				
			||||||
        placement="bottom"
 | 
					        placement="bottom"
 | 
				
			||||||
        .open=${this._drawerOpen}
 | 
					        .open=${this._drawerOpen}
 | 
				
			||||||
        @wa-after-hide=${this._handleAfterHide}
 | 
					        @wa-after-hide=${this._handleAfterHide}
 | 
				
			||||||
        without-header
 | 
					        without-header
 | 
				
			||||||
        @touchstart=${this._handleTouchStart}
 | 
					 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <slot name="header"></slot>
 | 
					        <slot></slot>
 | 
				
			||||||
        <div id="body" class="body ha-scrollbar">
 | 
					 | 
				
			||||||
          <slot></slot>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
      </wa-drawer>
 | 
					      </wa-drawer>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _handleTouchStart = (ev: TouchEvent) => {
 | 
					  static styles = css`
 | 
				
			||||||
    // Check if any element inside drawer in the composed path has scrollTop > 0
 | 
					    wa-drawer {
 | 
				
			||||||
    for (const path of ev.composedPath()) {
 | 
					      --wa-color-surface-raised: var(
 | 
				
			||||||
      const el = path as HTMLElement;
 | 
					        --ha-bottom-sheet-surface-background,
 | 
				
			||||||
      if (el === this._drawer) {
 | 
					        var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
 | 
				
			||||||
        break;
 | 
					      );
 | 
				
			||||||
      }
 | 
					      --spacing: 0;
 | 
				
			||||||
      if (el.scrollTop > 0) {
 | 
					      --size: auto;
 | 
				
			||||||
        return;
 | 
					      --show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
 | 
				
			||||||
      }
 | 
					      --hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					    wa-drawer::part(dialog) {
 | 
				
			||||||
    this._startResizing(ev.touches[0].clientY);
 | 
					      border-top-left-radius: var(
 | 
				
			||||||
  };
 | 
					        --ha-bottom-sheet-border-radius,
 | 
				
			||||||
 | 
					        var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
 | 
				
			||||||
  private _startResizing(clientY: number) {
 | 
					      );
 | 
				
			||||||
    // register event listeners for drag handling
 | 
					      border-top-right-radius: var(
 | 
				
			||||||
    document.addEventListener("touchmove", this._handleTouchMove, {
 | 
					        --ha-bottom-sheet-border-radius,
 | 
				
			||||||
      passive: false,
 | 
					        var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
 | 
				
			||||||
    });
 | 
					      );
 | 
				
			||||||
    document.addEventListener("touchend", this._handleTouchEnd);
 | 
					      max-height: 90vh;
 | 
				
			||||||
    document.addEventListener("touchcancel", this._handleTouchEnd);
 | 
					      padding-bottom: var(--safe-area-inset-bottom);
 | 
				
			||||||
 | 
					      padding-left: var(--safe-area-inset-left);
 | 
				
			||||||
    this._gestureRecognizer.start(clientY);
 | 
					      padding-right: var(--safe-area-inset-right);
 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleTouchMove = (ev: TouchEvent) => {
 | 
					 | 
				
			||||||
    const currentY = ev.touches[0].clientY;
 | 
					 | 
				
			||||||
    const delta = this._gestureRecognizer.move(currentY);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (delta < 0) {
 | 
					 | 
				
			||||||
      ev.preventDefault();
 | 
					 | 
				
			||||||
      this._isDragging = true;
 | 
					 | 
				
			||||||
      requestAnimationFrame(() => {
 | 
					 | 
				
			||||||
        if (this._isDragging) {
 | 
					 | 
				
			||||||
          this.style.setProperty(
 | 
					 | 
				
			||||||
            "--dialog-transform",
 | 
					 | 
				
			||||||
            `translateY(${delta * -1}px)`
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  `;
 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _animateSnapBack() {
 | 
					 | 
				
			||||||
    // Add transition for smooth animation
 | 
					 | 
				
			||||||
    this.style.setProperty(
 | 
					 | 
				
			||||||
      "--dialog-transition",
 | 
					 | 
				
			||||||
      `transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease-out`
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Reset transform to snap back
 | 
					 | 
				
			||||||
    this.style.removeProperty("--dialog-transform");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Remove transition after animation completes
 | 
					 | 
				
			||||||
    setTimeout(() => {
 | 
					 | 
				
			||||||
      this.style.removeProperty("--dialog-transition");
 | 
					 | 
				
			||||||
    }, BOTTOM_SHEET_ANIMATION_DURATION_MS);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleTouchEnd = () => {
 | 
					 | 
				
			||||||
    this._unregisterResizeHandlers();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._isDragging = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const result = this._gestureRecognizer.end();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // If velocity exceeds threshold, use velocity direction to determine action
 | 
					 | 
				
			||||||
    if (result.isSwipe) {
 | 
					 | 
				
			||||||
      if (result.isDownwardSwipe) {
 | 
					 | 
				
			||||||
        // Downward swipe - close the bottom sheet
 | 
					 | 
				
			||||||
        this._drawerOpen = false;
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        // Upward swipe - keep open and animate back
 | 
					 | 
				
			||||||
        this._animateSnapBack();
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // If velocity is below threshold, use position-based logic
 | 
					 | 
				
			||||||
    // Get the drawer height to calculate 50% threshold
 | 
					 | 
				
			||||||
    const drawerBody = this._drawer.shadowRoot?.querySelector(
 | 
					 | 
				
			||||||
      '[part="body"]'
 | 
					 | 
				
			||||||
    ) as HTMLElement;
 | 
					 | 
				
			||||||
    const drawerHeight = drawerBody?.offsetHeight || 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // delta is negative when dragging down
 | 
					 | 
				
			||||||
    // Close if dragged down past 50% of the drawer height
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      drawerHeight > 0 &&
 | 
					 | 
				
			||||||
      result.delta < 0 &&
 | 
					 | 
				
			||||||
      Math.abs(result.delta) > drawerHeight * 0.5
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      this._drawerOpen = false;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this._animateSnapBack();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _unregisterResizeHandlers = () => {
 | 
					 | 
				
			||||||
    document.removeEventListener("touchmove", this._handleTouchMove);
 | 
					 | 
				
			||||||
    document.removeEventListener("touchend", this._handleTouchEnd);
 | 
					 | 
				
			||||||
    document.removeEventListener("touchcancel", this._handleTouchEnd);
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  disconnectedCallback() {
 | 
					 | 
				
			||||||
    super.disconnectedCallback();
 | 
					 | 
				
			||||||
    this._unregisterResizeHandlers();
 | 
					 | 
				
			||||||
    this._isDragging = false;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static styles = [
 | 
					 | 
				
			||||||
    haStyleScrollbar,
 | 
					 | 
				
			||||||
    css`
 | 
					 | 
				
			||||||
      wa-drawer {
 | 
					 | 
				
			||||||
        --wa-color-surface-raised: transparent;
 | 
					 | 
				
			||||||
        --spacing: 0;
 | 
					 | 
				
			||||||
        --size: var(--ha-bottom-sheet-height, auto);
 | 
					 | 
				
			||||||
        --show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
 | 
					 | 
				
			||||||
        --hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      wa-drawer::part(dialog) {
 | 
					 | 
				
			||||||
        max-height: var(--ha-bottom-sheet-max-height, 90vh);
 | 
					 | 
				
			||||||
        align-items: center;
 | 
					 | 
				
			||||||
        transform: var(--dialog-transform);
 | 
					 | 
				
			||||||
        transition: var(--dialog-transition);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      wa-drawer::part(body) {
 | 
					 | 
				
			||||||
        max-width: var(--ha-bottom-sheet-max-width);
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
        border-top-left-radius: var(
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-border-radius,
 | 
					 | 
				
			||||||
          var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        border-top-right-radius: var(
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-border-radius,
 | 
					 | 
				
			||||||
          var(--ha-dialog-border-radius, var(--ha-border-radius-2xl))
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        background-color: var(
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-surface-background,
 | 
					 | 
				
			||||||
          var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)),
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
        padding: var(
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-padding,
 | 
					 | 
				
			||||||
          0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
 | 
					 | 
				
			||||||
            var(--safe-area-inset-left)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      :host([flexcontent]) wa-drawer::part(body) {
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        flex-direction: column;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      :host([flexcontent]) .body {
 | 
					 | 
				
			||||||
        flex: 1;
 | 
					 | 
				
			||||||
        max-width: 100%;
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        flex-direction: column;
 | 
					 | 
				
			||||||
        padding: var(
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-padding,
 | 
					 | 
				
			||||||
          0 var(--safe-area-inset-right) var(--safe-area-inset-bottom)
 | 
					 | 
				
			||||||
            var(--safe-area-inset-left)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    `,
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -31,9 +31,6 @@ export class HaButtonToggleGroup extends LitElement {
 | 
				
			|||||||
  @property({ type: Boolean, reflect: true, attribute: "no-wrap" })
 | 
					  @property({ type: Boolean, reflect: true, attribute: "no-wrap" })
 | 
				
			||||||
  public nowrap = false;
 | 
					  public nowrap = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ type: Boolean, reflect: true, attribute: "full-width" })
 | 
					 | 
				
			||||||
  public fullWidth = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property() public variant:
 | 
					  @property() public variant:
 | 
				
			||||||
    | "brand"
 | 
					    | "brand"
 | 
				
			||||||
    | "neutral"
 | 
					    | "neutral"
 | 
				
			||||||
@@ -41,13 +38,6 @@ export class HaButtonToggleGroup extends LitElement {
 | 
				
			|||||||
    | "warning"
 | 
					    | "warning"
 | 
				
			||||||
    | "danger" = "brand";
 | 
					    | "danger" = "brand";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: "active-variant" }) public activeVariant?:
 | 
					 | 
				
			||||||
    | "brand"
 | 
					 | 
				
			||||||
    | "neutral"
 | 
					 | 
				
			||||||
    | "success"
 | 
					 | 
				
			||||||
    | "warning"
 | 
					 | 
				
			||||||
    | "danger";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render(): TemplateResult {
 | 
					  protected render(): TemplateResult {
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <wa-button-group childSelector="ha-button">
 | 
					      <wa-button-group childSelector="ha-button">
 | 
				
			||||||
@@ -56,9 +46,7 @@ export class HaButtonToggleGroup extends LitElement {
 | 
				
			|||||||
            html`<ha-button
 | 
					            html`<ha-button
 | 
				
			||||||
              iconTag="ha-svg-icon"
 | 
					              iconTag="ha-svg-icon"
 | 
				
			||||||
              class="icon"
 | 
					              class="icon"
 | 
				
			||||||
              .variant=${this.active !== button.value || !this.activeVariant
 | 
					              .variant=${this.variant}
 | 
				
			||||||
                ? this.variant
 | 
					 | 
				
			||||||
                : this.activeVariant}
 | 
					 | 
				
			||||||
              .size=${this.size}
 | 
					              .size=${this.size}
 | 
				
			||||||
              .value=${button.value}
 | 
					              .value=${button.value}
 | 
				
			||||||
              @click=${this._handleClick}
 | 
					              @click=${this._handleClick}
 | 
				
			||||||
@@ -90,19 +78,6 @@ export class HaButtonToggleGroup extends LitElement {
 | 
				
			|||||||
    :host([no-wrap]) wa-button-group::part(base) {
 | 
					    :host([no-wrap]) wa-button-group::part(base) {
 | 
				
			||||||
      flex-wrap: nowrap;
 | 
					      flex-wrap: nowrap;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					 | 
				
			||||||
    wa-button-group {
 | 
					 | 
				
			||||||
      padding: var(--ha-button-toggle-group-padding);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :host([full-width]) wa-button-group,
 | 
					 | 
				
			||||||
    :host([full-width]) wa-button-group::part(base) {
 | 
					 | 
				
			||||||
      width: 100%;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    :host([full-width]) ha-button {
 | 
					 | 
				
			||||||
      flex: 1;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -86,8 +86,7 @@ export class HaCameraStream extends LitElement {
 | 
				
			|||||||
    const streams = this._streams(
 | 
					    const streams = this._streams(
 | 
				
			||||||
      this._capabilities?.frontend_stream_types,
 | 
					      this._capabilities?.frontend_stream_types,
 | 
				
			||||||
      this._hlsStreams,
 | 
					      this._hlsStreams,
 | 
				
			||||||
      this._webRtcStreams,
 | 
					      this._webRtcStreams
 | 
				
			||||||
      this.muted
 | 
					 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
    return html`${repeat(
 | 
					    return html`${repeat(
 | 
				
			||||||
      streams,
 | 
					      streams,
 | 
				
			||||||
@@ -191,8 +190,7 @@ export class HaCameraStream extends LitElement {
 | 
				
			|||||||
    (
 | 
					    (
 | 
				
			||||||
      supportedTypes?: StreamType[],
 | 
					      supportedTypes?: StreamType[],
 | 
				
			||||||
      hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
 | 
					      hlsStreams?: { hasAudio: boolean; hasVideo: boolean },
 | 
				
			||||||
      webRtcStreams?: { hasAudio: boolean; hasVideo: boolean },
 | 
					      webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }
 | 
				
			||||||
      muted?: boolean
 | 
					 | 
				
			||||||
    ): Stream[] => {
 | 
					    ): Stream[] => {
 | 
				
			||||||
      if (__DEMO__) {
 | 
					      if (__DEMO__) {
 | 
				
			||||||
        return [{ type: MJPEG_STREAM, visible: true }];
 | 
					        return [{ type: MJPEG_STREAM, visible: true }];
 | 
				
			||||||
@@ -222,10 +220,9 @@ export class HaCameraStream extends LitElement {
 | 
				
			|||||||
        if (
 | 
					        if (
 | 
				
			||||||
          hlsStreams.hasVideo &&
 | 
					          hlsStreams.hasVideo &&
 | 
				
			||||||
          hlsStreams.hasAudio &&
 | 
					          hlsStreams.hasAudio &&
 | 
				
			||||||
          !webRtcStreams.hasAudio &&
 | 
					          !webRtcStreams.hasAudio
 | 
				
			||||||
          !muted
 | 
					 | 
				
			||||||
        ) {
 | 
					        ) {
 | 
				
			||||||
          // webRTC stream is missing audio and audio is not muted, use HLS
 | 
					          // webRTC stream is missing audio, use HLS
 | 
				
			||||||
          return [{ type: STREAM_TYPE_HLS, visible: true }];
 | 
					          return [{ type: STREAM_TYPE_HLS, visible: true }];
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        if (webRtcStreams.hasVideo) {
 | 
					        if (webRtcStreams.hasVideo) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -44,26 +44,26 @@ export class HaCard extends LitElement {
 | 
				
			|||||||
      font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
 | 
					      font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl));
 | 
				
			||||||
      letter-spacing: -0.012em;
 | 
					      letter-spacing: -0.012em;
 | 
				
			||||||
      line-height: var(--ha-line-height-expanded);
 | 
					      line-height: var(--ha-line-height-expanded);
 | 
				
			||||||
      padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4);
 | 
					      padding: 12px 16px 16px;
 | 
				
			||||||
      display: block;
 | 
					      display: block;
 | 
				
			||||||
      margin-block-start: var(--ha-space-0);
 | 
					      margin-block-start: 0px;
 | 
				
			||||||
      margin-block-end: var(--ha-space-0);
 | 
					      margin-block-end: 0px;
 | 
				
			||||||
      font-weight: var(--ha-font-weight-normal);
 | 
					      font-weight: var(--ha-font-weight-normal);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :host ::slotted(.card-content:not(:first-child)),
 | 
					    :host ::slotted(.card-content:not(:first-child)),
 | 
				
			||||||
    slot:not(:first-child)::slotted(.card-content) {
 | 
					    slot:not(:first-child)::slotted(.card-content) {
 | 
				
			||||||
      padding-top: var(--ha-space-0);
 | 
					      padding-top: 0px;
 | 
				
			||||||
      margin-top: calc(var(--ha-space-2) * -1);
 | 
					      margin-top: -8px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :host ::slotted(.card-content) {
 | 
					    :host ::slotted(.card-content) {
 | 
				
			||||||
      padding: var(--ha-space-4);
 | 
					      padding: 16px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :host ::slotted(.card-actions) {
 | 
					    :host ::slotted(.card-actions) {
 | 
				
			||||||
      border-top: 1px solid var(--divider-color, #e8e8e8);
 | 
					      border-top: 1px solid var(--divider-color, #e8e8e8);
 | 
				
			||||||
      padding: var(--ha-space-2);
 | 
					      padding: 8px;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -239,7 +239,6 @@ export class HaCodeEditor extends ReactiveElement {
 | 
				
			|||||||
      this._loadedCodeMirror.crosshairCursor(),
 | 
					      this._loadedCodeMirror.crosshairCursor(),
 | 
				
			||||||
      this._loadedCodeMirror.highlightSelectionMatches(),
 | 
					      this._loadedCodeMirror.highlightSelectionMatches(),
 | 
				
			||||||
      this._loadedCodeMirror.highlightActiveLine(),
 | 
					      this._loadedCodeMirror.highlightActiveLine(),
 | 
				
			||||||
      this._loadedCodeMirror.dropCursor(),
 | 
					 | 
				
			||||||
      this._loadedCodeMirror.indentationMarkers({
 | 
					      this._loadedCodeMirror.indentationMarkers({
 | 
				
			||||||
        thickness: 0,
 | 
					        thickness: 0,
 | 
				
			||||||
        activeThickness: 1,
 | 
					        activeThickness: 1,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -6,9 +6,6 @@ export class HaDialogHeader extends LitElement {
 | 
				
			|||||||
  @property({ type: String, attribute: "subtitle-position" })
 | 
					  @property({ type: String, attribute: "subtitle-position" })
 | 
				
			||||||
  public subtitlePosition: "above" | "below" = "below";
 | 
					  public subtitlePosition: "above" | "below" = "below";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ type: Boolean, reflect: true, attribute: "show-border" })
 | 
					 | 
				
			||||||
  public showBorder = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render() {
 | 
					  protected render() {
 | 
				
			||||||
    const titleSlot = html`<div class="header-title">
 | 
					    const titleSlot = html`<div class="header-title">
 | 
				
			||||||
      <slot name="title"></slot>
 | 
					      <slot name="title"></slot>
 | 
				
			||||||
@@ -52,16 +49,12 @@ export class HaDialogHeader extends LitElement {
 | 
				
			|||||||
          display: flex;
 | 
					          display: flex;
 | 
				
			||||||
          flex-direction: row;
 | 
					          flex-direction: row;
 | 
				
			||||||
          align-items: center;
 | 
					          align-items: center;
 | 
				
			||||||
          padding: 0 var(--ha-space-1);
 | 
					          padding: 4px;
 | 
				
			||||||
          box-sizing: border-box;
 | 
					          box-sizing: border-box;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .header-content {
 | 
					        .header-content {
 | 
				
			||||||
          flex: 1;
 | 
					          flex: 1;
 | 
				
			||||||
          padding: 10px var(--ha-space-1);
 | 
					          padding: 10px 4px;
 | 
				
			||||||
          display: flex;
 | 
					 | 
				
			||||||
          flex-direction: column;
 | 
					 | 
				
			||||||
          justify-content: center;
 | 
					 | 
				
			||||||
          min-height: var(--ha-space-12);
 | 
					 | 
				
			||||||
          min-width: 0;
 | 
					          min-width: 0;
 | 
				
			||||||
          overflow: hidden;
 | 
					          overflow: hidden;
 | 
				
			||||||
          text-overflow: ellipsis;
 | 
					          text-overflow: ellipsis;
 | 
				
			||||||
@@ -70,7 +63,7 @@ export class HaDialogHeader extends LitElement {
 | 
				
			|||||||
        .header-title {
 | 
					        .header-title {
 | 
				
			||||||
          height: var(
 | 
					          height: var(
 | 
				
			||||||
            --ha-dialog-header-title-height,
 | 
					            --ha-dialog-header-title-height,
 | 
				
			||||||
            calc(var(--ha-font-size-xl) + var(--ha-space-1))
 | 
					            calc(var(--ha-font-size-xl) + 4px)
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
          font-size: var(--ha-font-size-xl);
 | 
					          font-size: var(--ha-font-size-xl);
 | 
				
			||||||
          line-height: var(--ha-line-height-condensed);
 | 
					          line-height: var(--ha-line-height-condensed);
 | 
				
			||||||
@@ -83,19 +76,19 @@ export class HaDialogHeader extends LitElement {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        @media all and (min-width: 450px) and (min-height: 500px) {
 | 
					        @media all and (min-width: 450px) and (min-height: 500px) {
 | 
				
			||||||
          .header-bar {
 | 
					          .header-bar {
 | 
				
			||||||
            padding: 0 var(--ha-space-2);
 | 
					            padding: 16px;
 | 
				
			||||||
          }
 | 
					          }
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .header-navigation-icon {
 | 
					        .header-navigation-icon {
 | 
				
			||||||
          flex: none;
 | 
					          flex: none;
 | 
				
			||||||
          min-width: var(--ha-space-2);
 | 
					          min-width: 8px;
 | 
				
			||||||
          height: 100%;
 | 
					          height: 100%;
 | 
				
			||||||
          display: flex;
 | 
					          display: flex;
 | 
				
			||||||
          flex-direction: row;
 | 
					          flex-direction: row;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .header-action-items {
 | 
					        .header-action-items {
 | 
				
			||||||
          flex: none;
 | 
					          flex: none;
 | 
				
			||||||
          min-width: var(--ha-space-2);
 | 
					          min-width: 8px;
 | 
				
			||||||
          height: 100%;
 | 
					          height: 100%;
 | 
				
			||||||
          display: flex;
 | 
					          display: flex;
 | 
				
			||||||
          flex-direction: row;
 | 
					          flex-direction: row;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -49,7 +49,6 @@ export class HaExpansionPanel extends LitElement {
 | 
				
			|||||||
          tabindex=${this.noCollapse ? -1 : 0}
 | 
					          tabindex=${this.noCollapse ? -1 : 0}
 | 
				
			||||||
          aria-expanded=${this.expanded}
 | 
					          aria-expanded=${this.expanded}
 | 
				
			||||||
          aria-controls="sect1"
 | 
					          aria-controls="sect1"
 | 
				
			||||||
          part="summary"
 | 
					 | 
				
			||||||
        >
 | 
					        >
 | 
				
			||||||
          ${this.leftChevron ? chevronIcon : nothing}
 | 
					          ${this.leftChevron ? chevronIcon : nothing}
 | 
				
			||||||
          <slot name="leading-icon"></slot>
 | 
					          <slot name="leading-icon"></slot>
 | 
				
			||||||
@@ -171,11 +170,6 @@ export class HaExpansionPanel extends LitElement {
 | 
				
			|||||||
      margin-left: 8px;
 | 
					      margin-left: 8px;
 | 
				
			||||||
      margin-inline-start: 8px;
 | 
					      margin-inline-start: 8px;
 | 
				
			||||||
      margin-inline-end: initial;
 | 
					      margin-inline-end: initial;
 | 
				
			||||||
      border-radius: var(--ha-border-radius-circle);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    #summary:focus-visible ha-svg-icon.summary-icon {
 | 
					 | 
				
			||||||
      background-color: var(--ha-color-fill-neutral-normal-active);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    :host([left-chevron]) .summary-icon,
 | 
					    :host([left-chevron]) .summary-icon,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -248,7 +248,7 @@ export class HaFilterDevices extends LitElement {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        search-input-outlined {
 | 
					        search-input-outlined {
 | 
				
			||||||
          display: block;
 | 
					          display: block;
 | 
				
			||||||
          padding: var(--ha-space-1) var(--ha-space-2) 0;
 | 
					          padding: 0 8px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -199,7 +199,7 @@ export class HaFilterDomains extends LitElement {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        search-input-outlined {
 | 
					        search-input-outlined {
 | 
				
			||||||
          display: block;
 | 
					          display: block;
 | 
				
			||||||
          padding: var(--ha-space-1) var(--ha-space-2) 0;
 | 
					          padding: 0 8px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -264,7 +264,7 @@ export class HaFilterEntities extends LitElement {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        search-input-outlined {
 | 
					        search-input-outlined {
 | 
				
			||||||
          display: block;
 | 
					          display: block;
 | 
				
			||||||
          padding: var(--ha-space-1) var(--ha-space-2) 0;
 | 
					          padding: 0 8px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -217,7 +217,7 @@ export class HaFilterIntegrations extends LitElement {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        search-input-outlined {
 | 
					        search-input-outlined {
 | 
				
			||||||
          display: block;
 | 
					          display: block;
 | 
				
			||||||
          padding: var(--ha-space-1) var(--ha-space-2) 0;
 | 
					          padding: 0 8px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -256,7 +256,7 @@ export class HaFilterLabels extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        search-input-outlined {
 | 
					        search-input-outlined {
 | 
				
			||||||
          display: block;
 | 
					          display: block;
 | 
				
			||||||
          padding: var(--ha-space-1) var(--ha-space-2) 0;
 | 
					          padding: 0 8px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -148,7 +148,7 @@ export class HaForm extends LitElement implements HaFormElement {
 | 
				
			|||||||
                  .value=${getValue(this.data, item)}
 | 
					                  .value=${getValue(this.data, item)}
 | 
				
			||||||
                  .label=${this._computeLabel(item, this.data)}
 | 
					                  .label=${this._computeLabel(item, this.data)}
 | 
				
			||||||
                  .disabled=${item.disabled || this.disabled || false}
 | 
					                  .disabled=${item.disabled || this.disabled || false}
 | 
				
			||||||
                  .placeholder=${item.required ? undefined : item.default}
 | 
					                  .placeholder=${item.required ? "" : item.default}
 | 
				
			||||||
                  .helper=${this._computeHelper(item)}
 | 
					                  .helper=${this._computeHelper(item)}
 | 
				
			||||||
                  .localizeValue=${this.localizeValue}
 | 
					                  .localizeValue=${this.localizeValue}
 | 
				
			||||||
                  .required=${item.required || false}
 | 
					                  .required=${item.required || false}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,14 +1,10 @@
 | 
				
			|||||||
import "@home-assistant/webawesome/dist/components/popover/popover";
 | 
					import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
				
			||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
 | 
					import type { ComboBoxLightOpenedChangedEvent } from "@vaadin/combo-box/vaadin-combo-box-light";
 | 
				
			||||||
import { mdiPlaylistPlus } from "@mdi/js";
 | 
					 | 
				
			||||||
import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
 | 
					import { css, html, LitElement, nothing, type CSSResultGroup } from "lit";
 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
import { ifDefined } from "lit/directives/if-defined";
 | 
					import { ifDefined } from "lit/directives/if-defined";
 | 
				
			||||||
import { tinykeys } from "tinykeys";
 | 
					 | 
				
			||||||
import { fireEvent } from "../common/dom/fire_event";
 | 
					import { fireEvent } from "../common/dom/fire_event";
 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
import "./ha-bottom-sheet";
 | 
					 | 
				
			||||||
import "./ha-button";
 | 
					 | 
				
			||||||
import "./ha-combo-box-item";
 | 
					import "./ha-combo-box-item";
 | 
				
			||||||
import "./ha-icon-button";
 | 
					import "./ha-icon-button";
 | 
				
			||||||
import "./ha-input-helper-text";
 | 
					import "./ha-input-helper-text";
 | 
				
			||||||
@@ -19,12 +15,12 @@ import type {
 | 
				
			|||||||
  PickerComboBoxSearchFn,
 | 
					  PickerComboBoxSearchFn,
 | 
				
			||||||
} from "./ha-picker-combo-box";
 | 
					} from "./ha-picker-combo-box";
 | 
				
			||||||
import "./ha-picker-field";
 | 
					import "./ha-picker-field";
 | 
				
			||||||
import type { PickerValueRenderer } from "./ha-picker-field";
 | 
					import type { HaPickerField, PickerValueRenderer } from "./ha-picker-field";
 | 
				
			||||||
import "./ha-svg-icon";
 | 
					import "./ha-svg-icon";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@customElement("ha-generic-picker")
 | 
					@customElement("ha-generic-picker")
 | 
				
			||||||
export class HaGenericPicker extends LitElement {
 | 
					export class HaGenericPicker extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass?: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // eslint-disable-next-line lit/no-native-attributes
 | 
					  // eslint-disable-next-line lit/no-native-attributes
 | 
				
			||||||
  @property({ type: Boolean }) public autofocus = false;
 | 
					  @property({ type: Boolean }) public autofocus = false;
 | 
				
			||||||
@@ -57,7 +53,7 @@ export class HaGenericPicker extends LitElement {
 | 
				
			|||||||
  public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
 | 
					  public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false })
 | 
					  @property({ attribute: false })
 | 
				
			||||||
  public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
 | 
					  public rowRenderer?: ComboBoxLitRenderer<PickerComboBoxItem>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false })
 | 
					  @property({ attribute: false })
 | 
				
			||||||
  public valueRenderer?: PickerValueRenderer;
 | 
					  public valueRenderer?: PickerValueRenderer;
 | 
				
			||||||
@@ -68,142 +64,58 @@ export class HaGenericPicker extends LitElement {
 | 
				
			|||||||
  @property({ attribute: "not-found-label", type: String })
 | 
					  @property({ attribute: "not-found-label", type: String })
 | 
				
			||||||
  public notFoundLabel?: string;
 | 
					  public notFoundLabel?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: "popover-placement" })
 | 
					  @query("ha-picker-field") private _field?: HaPickerField;
 | 
				
			||||||
  public popoverPlacement:
 | 
					 | 
				
			||||||
    | "bottom"
 | 
					 | 
				
			||||||
    | "top"
 | 
					 | 
				
			||||||
    | "left"
 | 
					 | 
				
			||||||
    | "right"
 | 
					 | 
				
			||||||
    | "top-start"
 | 
					 | 
				
			||||||
    | "top-end"
 | 
					 | 
				
			||||||
    | "right-start"
 | 
					 | 
				
			||||||
    | "right-end"
 | 
					 | 
				
			||||||
    | "bottom-start"
 | 
					 | 
				
			||||||
    | "bottom-end"
 | 
					 | 
				
			||||||
    | "left-start"
 | 
					 | 
				
			||||||
    | "left-end" = "bottom-start";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /** If set picker shows an add button instead of textbox when value isn't set */
 | 
					 | 
				
			||||||
  @property({ attribute: "add-button-label" }) public addButtonLabel?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @query(".container") private _containerElement?: HTMLDivElement;
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
 | 
					  @query("ha-picker-combo-box") private _comboBox?: HaPickerComboBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _opened = false;
 | 
					  @state() private _opened = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _pickerWrapperOpen = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _popoverWidth = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _openedNarrow = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _narrow = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // helper to set new value after closing picker, to avoid flicker
 | 
					 | 
				
			||||||
  private _newValue?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _unsubscribeTinyKeys?: () => void;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render() {
 | 
					  protected render() {
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      ${this.label
 | 
					      ${this.label
 | 
				
			||||||
        ? html`<label ?disabled=${this.disabled}>${this.label}</label>`
 | 
					        ? html`<label ?disabled=${this.disabled}>${this.label}</label>`
 | 
				
			||||||
        : nothing}
 | 
					        : nothing}
 | 
				
			||||||
      <div class="container">
 | 
					      <div class="container">
 | 
				
			||||||
        <div id="picker">
 | 
					        ${!this._opened
 | 
				
			||||||
          <slot name="field">
 | 
					 | 
				
			||||||
            ${this.addButtonLabel && !this.value
 | 
					 | 
				
			||||||
              ? html`<ha-button
 | 
					 | 
				
			||||||
                  size="small"
 | 
					 | 
				
			||||||
                  appearance="filled"
 | 
					 | 
				
			||||||
                  @click=${this.open}
 | 
					 | 
				
			||||||
                  .disabled=${this.disabled}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <ha-svg-icon
 | 
					 | 
				
			||||||
                    .path=${mdiPlaylistPlus}
 | 
					 | 
				
			||||||
                    slot="start"
 | 
					 | 
				
			||||||
                  ></ha-svg-icon>
 | 
					 | 
				
			||||||
                  ${this.addButtonLabel}
 | 
					 | 
				
			||||||
                </ha-button>`
 | 
					 | 
				
			||||||
              : html`<ha-picker-field
 | 
					 | 
				
			||||||
                  type="button"
 | 
					 | 
				
			||||||
                  class=${this._opened ? "opened" : ""}
 | 
					 | 
				
			||||||
                  compact
 | 
					 | 
				
			||||||
                  aria-label=${ifDefined(this.label)}
 | 
					 | 
				
			||||||
                  @click=${this.open}
 | 
					 | 
				
			||||||
                  @clear=${this._clear}
 | 
					 | 
				
			||||||
                  .placeholder=${this.placeholder}
 | 
					 | 
				
			||||||
                  .value=${this.value}
 | 
					 | 
				
			||||||
                  .required=${this.required}
 | 
					 | 
				
			||||||
                  .disabled=${this.disabled}
 | 
					 | 
				
			||||||
                  .hideClearIcon=${this.hideClearIcon}
 | 
					 | 
				
			||||||
                  .valueRenderer=${this.valueRenderer}
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                </ha-picker-field>`}
 | 
					 | 
				
			||||||
          </slot>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        ${!this._openedNarrow && (this._pickerWrapperOpen || this._opened)
 | 
					 | 
				
			||||||
          ? html`
 | 
					          ? html`
 | 
				
			||||||
              <wa-popover
 | 
					              <ha-picker-field
 | 
				
			||||||
                .open=${this._pickerWrapperOpen}
 | 
					                type="button"
 | 
				
			||||||
                style="--body-width: ${this._popoverWidth}px;"
 | 
					                compact
 | 
				
			||||||
                without-arrow
 | 
					                aria-label=${ifDefined(this.label)}
 | 
				
			||||||
                distance="-4"
 | 
					                @click=${this.open}
 | 
				
			||||||
                .placement=${this.popoverPlacement}
 | 
					                @clear=${this._clear}
 | 
				
			||||||
                for="picker"
 | 
					                .placeholder=${this.placeholder}
 | 
				
			||||||
                auto-size="vertical"
 | 
					                .value=${this.value}
 | 
				
			||||||
                auto-size-padding="16"
 | 
					                .required=${this.required}
 | 
				
			||||||
                @wa-after-show=${this._dialogOpened}
 | 
					                .disabled=${this.disabled}
 | 
				
			||||||
                @wa-after-hide=${this._hidePicker}
 | 
					                .hideClearIcon=${this.hideClearIcon}
 | 
				
			||||||
                trap-focus
 | 
					                .valueRenderer=${this.valueRenderer}
 | 
				
			||||||
                role="dialog"
 | 
					 | 
				
			||||||
                aria-modal="true"
 | 
					 | 
				
			||||||
                aria-label=${this.label || "Select option"}
 | 
					 | 
				
			||||||
              >
 | 
					              >
 | 
				
			||||||
                ${this._renderComboBox()}
 | 
					              </ha-picker-field>
 | 
				
			||||||
              </wa-popover>
 | 
					 | 
				
			||||||
            `
 | 
					            `
 | 
				
			||||||
          : this._pickerWrapperOpen || this._opened
 | 
					          : html`
 | 
				
			||||||
            ? html`<ha-bottom-sheet
 | 
					              <ha-picker-combo-box
 | 
				
			||||||
                flexcontent
 | 
					                .hass=${this.hass}
 | 
				
			||||||
                .open=${this._pickerWrapperOpen}
 | 
					                .autofocus=${this.autofocus}
 | 
				
			||||||
                @wa-after-show=${this._dialogOpened}
 | 
					                .allowCustomValue=${this.allowCustomValue}
 | 
				
			||||||
                @closed=${this._hidePicker}
 | 
					                .label=${this.searchLabel ??
 | 
				
			||||||
                role="dialog"
 | 
					                this.hass.localize("ui.common.search")}
 | 
				
			||||||
                aria-modal="true"
 | 
					                .value=${this.value}
 | 
				
			||||||
                aria-label=${this.label || "Select option"}
 | 
					                hide-clear-icon
 | 
				
			||||||
              >
 | 
					                @opened-changed=${this._openedChanged}
 | 
				
			||||||
                ${this._renderComboBox(true)}
 | 
					                @value-changed=${this._valueChanged}
 | 
				
			||||||
              </ha-bottom-sheet>`
 | 
					                .rowRenderer=${this.rowRenderer}
 | 
				
			||||||
            : nothing}
 | 
					                .notFoundLabel=${this.notFoundLabel}
 | 
				
			||||||
 | 
					                .getItems=${this.getItems}
 | 
				
			||||||
 | 
					                .getAdditionalItems=${this.getAdditionalItems}
 | 
				
			||||||
 | 
					                .searchFn=${this.searchFn}
 | 
				
			||||||
 | 
					              ></ha-picker-combo-box>
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
      ${this._renderHelper()}
 | 
					      ${this._renderHelper()}
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _renderComboBox(dialogMode = false) {
 | 
					 | 
				
			||||||
    if (!this._opened) {
 | 
					 | 
				
			||||||
      return nothing;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return html`
 | 
					 | 
				
			||||||
      <ha-picker-combo-box
 | 
					 | 
				
			||||||
        .hass=${this.hass}
 | 
					 | 
				
			||||||
        .allowCustomValue=${this.allowCustomValue}
 | 
					 | 
				
			||||||
        .label=${this.searchLabel ??
 | 
					 | 
				
			||||||
        (this.hass?.localize("ui.common.search") || "Search")}
 | 
					 | 
				
			||||||
        .value=${this.value}
 | 
					 | 
				
			||||||
        @value-changed=${this._valueChanged}
 | 
					 | 
				
			||||||
        .rowRenderer=${this.rowRenderer}
 | 
					 | 
				
			||||||
        .notFoundLabel=${this.notFoundLabel}
 | 
					 | 
				
			||||||
        .getItems=${this.getItems}
 | 
					 | 
				
			||||||
        .getAdditionalItems=${this.getAdditionalItems}
 | 
					 | 
				
			||||||
        .searchFn=${this.searchFn}
 | 
					 | 
				
			||||||
        .mode=${dialogMode ? "dialog" : "popover"}
 | 
					 | 
				
			||||||
      ></ha-picker-combo-box>
 | 
					 | 
				
			||||||
    `;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _renderHelper() {
 | 
					  private _renderHelper() {
 | 
				
			||||||
    return this.helper
 | 
					    return this.helper
 | 
				
			||||||
      ? html`<ha-input-helper-text .disabled=${this.disabled}
 | 
					      ? html`<ha-input-helper-text .disabled=${this.disabled}
 | 
				
			||||||
@@ -212,33 +124,13 @@ export class HaGenericPicker extends LitElement {
 | 
				
			|||||||
      : nothing;
 | 
					      : nothing;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _dialogOpened = () => {
 | 
					 | 
				
			||||||
    this._opened = true;
 | 
					 | 
				
			||||||
    requestAnimationFrame(() => {
 | 
					 | 
				
			||||||
      this._comboBox?.focus();
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _hidePicker(ev) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    if (this._newValue) {
 | 
					 | 
				
			||||||
      fireEvent(this, "value-changed", { value: this._newValue });
 | 
					 | 
				
			||||||
      this._newValue = undefined;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._opened = false;
 | 
					 | 
				
			||||||
    this._pickerWrapperOpen = false;
 | 
					 | 
				
			||||||
    this._unsubscribeTinyKeys?.();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _valueChanged(ev: CustomEvent) {
 | 
					  private _valueChanged(ev: CustomEvent) {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    const value = ev.detail.value;
 | 
					    const value = ev.detail.value;
 | 
				
			||||||
    if (!value) {
 | 
					    if (!value) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this._pickerWrapperOpen = false;
 | 
					    fireEvent(this, "value-changed", { value });
 | 
				
			||||||
    this._newValue = value;
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _clear(e) {
 | 
					  private _clear(e) {
 | 
				
			||||||
@@ -251,44 +143,24 @@ export class HaGenericPicker extends LitElement {
 | 
				
			|||||||
    fireEvent(this, "value-changed", { value });
 | 
					    fireEvent(this, "value-changed", { value });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async open(ev?: Event) {
 | 
					  public async open() {
 | 
				
			||||||
    ev?.stopPropagation();
 | 
					 | 
				
			||||||
    if (this.disabled) {
 | 
					    if (this.disabled) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    this._openedNarrow = this._narrow;
 | 
					    this._opened = true;
 | 
				
			||||||
    this._popoverWidth = this._containerElement?.offsetWidth || 250;
 | 
					    await this.updateComplete;
 | 
				
			||||||
    this._pickerWrapperOpen = true;
 | 
					    this._comboBox?.focus();
 | 
				
			||||||
    this._unsubscribeTinyKeys = tinykeys(this, {
 | 
					    this._comboBox?.open();
 | 
				
			||||||
      Escape: this._handleEscClose,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  connectedCallback() {
 | 
					  private async _openedChanged(ev: ComboBoxLightOpenedChangedEvent) {
 | 
				
			||||||
    super.connectedCallback();
 | 
					    const opened = ev.detail.value;
 | 
				
			||||||
    this._handleResize();
 | 
					    if (this._opened && !opened) {
 | 
				
			||||||
    window.addEventListener("resize", this._handleResize);
 | 
					      this._opened = false;
 | 
				
			||||||
  }
 | 
					      await this.updateComplete;
 | 
				
			||||||
 | 
					      this._field?.focus();
 | 
				
			||||||
  public disconnectedCallback() {
 | 
					 | 
				
			||||||
    super.disconnectedCallback();
 | 
					 | 
				
			||||||
    window.removeEventListener("resize", this._handleResize);
 | 
					 | 
				
			||||||
    this._unsubscribeTinyKeys?.();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleResize = () => {
 | 
					 | 
				
			||||||
    this._narrow =
 | 
					 | 
				
			||||||
      window.matchMedia("(max-width: 870px)").matches ||
 | 
					 | 
				
			||||||
      window.matchMedia("(max-height: 500px)").matches;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!this._openedNarrow && this._pickerWrapperOpen) {
 | 
					 | 
				
			||||||
      this._popoverWidth = this._containerElement?.offsetWidth || 250;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  };
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleEscClose = (ev: KeyboardEvent) => {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static get styles(): CSSResultGroup {
 | 
					  static get styles(): CSSResultGroup {
 | 
				
			||||||
    return [
 | 
					    return [
 | 
				
			||||||
@@ -306,45 +178,7 @@ export class HaGenericPicker extends LitElement {
 | 
				
			|||||||
        }
 | 
					        }
 | 
				
			||||||
        ha-input-helper-text {
 | 
					        ha-input-helper-text {
 | 
				
			||||||
          display: block;
 | 
					          display: block;
 | 
				
			||||||
          margin: var(--ha-space-2) 0 0;
 | 
					          margin: 8px 0 0;
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        wa-popover {
 | 
					 | 
				
			||||||
          --wa-space-l: var(--ha-space-0);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        wa-popover::part(body) {
 | 
					 | 
				
			||||||
          width: max(var(--body-width), 250px);
 | 
					 | 
				
			||||||
          max-width: max(var(--body-width), 250px);
 | 
					 | 
				
			||||||
          max-height: 500px;
 | 
					 | 
				
			||||||
          height: 70vh;
 | 
					 | 
				
			||||||
          overflow: hidden;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @media (max-height: 1000px) {
 | 
					 | 
				
			||||||
          wa-popover::part(body) {
 | 
					 | 
				
			||||||
            max-height: 400px;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        @media (max-height: 1000px) {
 | 
					 | 
				
			||||||
          wa-popover::part(body) {
 | 
					 | 
				
			||||||
            max-height: 400px;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ha-bottom-sheet {
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-height: 90vh;
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-height: calc(100dvh - var(--ha-space-12));
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-max-height: var(--ha-bottom-sheet-height);
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-max-width: 600px;
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-padding: var(--ha-space-0);
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-surface-background: var(--card-background-color);
 | 
					 | 
				
			||||||
          --ha-bottom-sheet-border-radius: var(--ha-border-radius-2xl);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ha-picker-field.opened {
 | 
					 | 
				
			||||||
          --mdc-text-field-idle-line-color: var(--primary-color);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      `,
 | 
					      `,
 | 
				
			||||||
    ];
 | 
					    ];
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,5 +1,5 @@
 | 
				
			|||||||
import { ResizeController } from "@lit-labs/observers/resize-controller";
 | 
					import { ResizeController } from "@lit-labs/observers/resize-controller";
 | 
				
			||||||
import { mdiDragHorizontalVariant, mdiEye, mdiEyeOff } from "@mdi/js";
 | 
					import { mdiDrag, mdiEye, mdiEyeOff } from "@mdi/js";
 | 
				
			||||||
import type { TemplateResult } from "lit";
 | 
					import type { TemplateResult } from "lit";
 | 
				
			||||||
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";
 | 
				
			||||||
@@ -178,7 +178,7 @@ export class HaItemDisplayEditor extends LitElement {
 | 
				
			|||||||
                            ? this._dragHandleKeydown
 | 
					                            ? this._dragHandleKeydown
 | 
				
			||||||
                            : undefined}
 | 
					                            : undefined}
 | 
				
			||||||
                          class="handle"
 | 
					                          class="handle"
 | 
				
			||||||
                          .path=${mdiDragHorizontalVariant}
 | 
					                          .path=${mdiDrag}
 | 
				
			||||||
                          slot="end"
 | 
					                          slot="end"
 | 
				
			||||||
                        ></ha-svg-icon>
 | 
					                        ></ha-svg-icon>
 | 
				
			||||||
                      `
 | 
					                      `
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -2,19 +2,19 @@ import { mdiLabel, mdiPlus } from "@mdi/js";
 | 
				
			|||||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
					import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
				
			||||||
import type { TemplateResult } from "lit";
 | 
					import type { TemplateResult } from "lit";
 | 
				
			||||||
import { LitElement, html } from "lit";
 | 
					import { LitElement, html } from "lit";
 | 
				
			||||||
import {
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
  customElement,
 | 
					 | 
				
			||||||
  property,
 | 
					 | 
				
			||||||
  query,
 | 
					 | 
				
			||||||
  queryAssignedElements,
 | 
					 | 
				
			||||||
  state,
 | 
					 | 
				
			||||||
} from "lit/decorators";
 | 
					 | 
				
			||||||
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 { computeDomain } from "../common/entity/compute_domain";
 | 
				
			||||||
 | 
					import type {
 | 
				
			||||||
 | 
					  DeviceEntityDisplayLookup,
 | 
				
			||||||
 | 
					  DeviceRegistryEntry,
 | 
				
			||||||
 | 
					} from "../data/device_registry";
 | 
				
			||||||
 | 
					import { getDeviceEntityDisplayLookup } from "../data/device_registry";
 | 
				
			||||||
 | 
					import type { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
				
			||||||
import type { LabelRegistryEntry } from "../data/label_registry";
 | 
					import type { LabelRegistryEntry } from "../data/label_registry";
 | 
				
			||||||
import {
 | 
					import {
 | 
				
			||||||
  createLabelRegistryEntry,
 | 
					  createLabelRegistryEntry,
 | 
				
			||||||
  getLabels,
 | 
					 | 
				
			||||||
  subscribeLabelRegistry,
 | 
					  subscribeLabelRegistry,
 | 
				
			||||||
} from "../data/label_registry";
 | 
					} from "../data/label_registry";
 | 
				
			||||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
 | 
					import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
 | 
				
			||||||
@@ -90,9 +90,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() private _labels?: LabelRegistryEntry[];
 | 
					  @state() private _labels?: LabelRegistryEntry[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @queryAssignedElements({ flatten: true })
 | 
					 | 
				
			||||||
  private _slotNodes?: NodeListOf<HTMLElement>;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @query("ha-generic-picker") private _picker?: HaGenericPicker;
 | 
					  @query("ha-generic-picker") private _picker?: HaGenericPicker;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public async open() {
 | 
					  public async open() {
 | 
				
			||||||
@@ -140,22 +137,201 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getLabelsMemoized = memoizeOne(getLabels);
 | 
					  private _getLabels = memoizeOne(
 | 
				
			||||||
 | 
					    (
 | 
				
			||||||
 | 
					      labels: LabelRegistryEntry[] | undefined,
 | 
				
			||||||
 | 
					      haAreas: HomeAssistant["areas"],
 | 
				
			||||||
 | 
					      haDevices: HomeAssistant["devices"],
 | 
				
			||||||
 | 
					      haEntities: HomeAssistant["entities"],
 | 
				
			||||||
 | 
					      includeDomains: this["includeDomains"],
 | 
				
			||||||
 | 
					      excludeDomains: this["excludeDomains"],
 | 
				
			||||||
 | 
					      includeDeviceClasses: this["includeDeviceClasses"],
 | 
				
			||||||
 | 
					      deviceFilter: this["deviceFilter"],
 | 
				
			||||||
 | 
					      entityFilter: this["entityFilter"],
 | 
				
			||||||
 | 
					      excludeLabels: this["excludeLabels"]
 | 
				
			||||||
 | 
					    ): PickerComboBoxItem[] => {
 | 
				
			||||||
 | 
					      if (!labels || labels.length === 0) {
 | 
				
			||||||
 | 
					        return [
 | 
				
			||||||
 | 
					          {
 | 
				
			||||||
 | 
					            id: NO_LABELS,
 | 
				
			||||||
 | 
					            primary: this.hass.localize("ui.components.label-picker.no_labels"),
 | 
				
			||||||
 | 
					            icon_path: mdiLabel,
 | 
				
			||||||
 | 
					          },
 | 
				
			||||||
 | 
					        ];
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getItems = () => {
 | 
					      const devices = Object.values(haDevices);
 | 
				
			||||||
    if (!this._labels || this._labels.length === 0) {
 | 
					      const entities = Object.values(haEntities);
 | 
				
			||||||
      return [
 | 
					
 | 
				
			||||||
        {
 | 
					      let deviceEntityLookup: DeviceEntityDisplayLookup = {};
 | 
				
			||||||
          id: NO_LABELS,
 | 
					      let inputDevices: DeviceRegistryEntry[] | undefined;
 | 
				
			||||||
          primary: this.hass.localize("ui.components.label-picker.no_labels"),
 | 
					      let inputEntities: EntityRegistryDisplayEntry[] | undefined;
 | 
				
			||||||
          icon_path: mdiLabel,
 | 
					
 | 
				
			||||||
        },
 | 
					      if (
 | 
				
			||||||
      ];
 | 
					        includeDomains ||
 | 
				
			||||||
 | 
					        excludeDomains ||
 | 
				
			||||||
 | 
					        includeDeviceClasses ||
 | 
				
			||||||
 | 
					        deviceFilter ||
 | 
				
			||||||
 | 
					        entityFilter
 | 
				
			||||||
 | 
					      ) {
 | 
				
			||||||
 | 
					        deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
 | 
				
			||||||
 | 
					        inputDevices = devices;
 | 
				
			||||||
 | 
					        inputEntities = entities.filter((entity) => entity.labels.length > 0);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (includeDomains) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) => {
 | 
				
			||||||
 | 
					            const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					            if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return deviceEntityLookup[device.id].some((entity) =>
 | 
				
			||||||
 | 
					              includeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          inputEntities = inputEntities!.filter((entity) =>
 | 
				
			||||||
 | 
					            includeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (excludeDomains) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) => {
 | 
				
			||||||
 | 
					            const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					            if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					              return true;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return entities.every(
 | 
				
			||||||
 | 
					              (entity) =>
 | 
				
			||||||
 | 
					                !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          inputEntities = inputEntities!.filter(
 | 
				
			||||||
 | 
					            (entity) =>
 | 
				
			||||||
 | 
					              !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (includeDeviceClasses) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) => {
 | 
				
			||||||
 | 
					            const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					            if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return deviceEntityLookup[device.id].some((entity) => {
 | 
				
			||||||
 | 
					              const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					              if (!stateObj) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              return (
 | 
				
			||||||
 | 
					                stateObj.attributes.device_class &&
 | 
				
			||||||
 | 
					                includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          inputEntities = inputEntities!.filter((entity) => {
 | 
				
			||||||
 | 
					            const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					            return (
 | 
				
			||||||
 | 
					              stateObj.attributes.device_class &&
 | 
				
			||||||
 | 
					              includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (deviceFilter) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) =>
 | 
				
			||||||
 | 
					            deviceFilter!(device)
 | 
				
			||||||
 | 
					          );
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        if (entityFilter) {
 | 
				
			||||||
 | 
					          inputDevices = inputDevices!.filter((device) => {
 | 
				
			||||||
 | 
					            const devEntities = deviceEntityLookup[device.id];
 | 
				
			||||||
 | 
					            if (!devEntities || !devEntities.length) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return deviceEntityLookup[device.id].some((entity) => {
 | 
				
			||||||
 | 
					              const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					              if (!stateObj) {
 | 
				
			||||||
 | 
					                return false;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					              return entityFilter(stateObj);
 | 
				
			||||||
 | 
					            });
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					          inputEntities = inputEntities!.filter((entity) => {
 | 
				
			||||||
 | 
					            const stateObj = this.hass.states[entity.entity_id];
 | 
				
			||||||
 | 
					            if (!stateObj) {
 | 
				
			||||||
 | 
					              return false;
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            return entityFilter!(stateObj);
 | 
				
			||||||
 | 
					          });
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let outputLabels = labels;
 | 
				
			||||||
 | 
					      const usedLabels = new Set<string>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      let areaIds: string[] | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (inputDevices) {
 | 
				
			||||||
 | 
					        areaIds = inputDevices
 | 
				
			||||||
 | 
					          .filter((device) => device.area_id)
 | 
				
			||||||
 | 
					          .map((device) => device.area_id!);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					        inputDevices.forEach((device) => {
 | 
				
			||||||
 | 
					          device.labels.forEach((label) => usedLabels.add(label));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (inputEntities) {
 | 
				
			||||||
 | 
					        areaIds = (areaIds ?? []).concat(
 | 
				
			||||||
 | 
					          inputEntities
 | 
				
			||||||
 | 
					            .filter((entity) => entity.area_id)
 | 
				
			||||||
 | 
					            .map((entity) => entity.area_id!)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					        inputEntities.forEach((entity) => {
 | 
				
			||||||
 | 
					          entity.labels.forEach((label) => usedLabels.add(label));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (areaIds) {
 | 
				
			||||||
 | 
					        areaIds.forEach((areaId) => {
 | 
				
			||||||
 | 
					          const area = haAreas[areaId];
 | 
				
			||||||
 | 
					          area.labels.forEach((label) => usedLabels.add(label));
 | 
				
			||||||
 | 
					        });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (excludeLabels) {
 | 
				
			||||||
 | 
					        outputLabels = outputLabels.filter(
 | 
				
			||||||
 | 
					          (label) => !excludeLabels!.includes(label.label_id)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      if (inputDevices || inputEntities) {
 | 
				
			||||||
 | 
					        outputLabels = outputLabels.filter((label) =>
 | 
				
			||||||
 | 
					          usedLabels.has(label.label_id)
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      const items = outputLabels.map<PickerComboBoxItem>((label) => ({
 | 
				
			||||||
 | 
					        id: label.label_id,
 | 
				
			||||||
 | 
					        primary: label.name,
 | 
				
			||||||
 | 
					        icon: label.icon || undefined,
 | 
				
			||||||
 | 
					        icon_path: label.icon ? undefined : mdiLabel,
 | 
				
			||||||
 | 
					        sorting_label: label.name,
 | 
				
			||||||
 | 
					        search_labels: [label.name, label.label_id, label.description].filter(
 | 
				
			||||||
 | 
					          (v): v is string => Boolean(v)
 | 
				
			||||||
 | 
					        ),
 | 
				
			||||||
 | 
					      }));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					      return items;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return this._getLabelsMemoized(
 | 
					  private _getItems = () =>
 | 
				
			||||||
      this.hass,
 | 
					    this._getLabels(
 | 
				
			||||||
      this._labels,
 | 
					      this._labels,
 | 
				
			||||||
 | 
					      this.hass.areas,
 | 
				
			||||||
 | 
					      this.hass.devices,
 | 
				
			||||||
 | 
					      this.hass.entities,
 | 
				
			||||||
      this.includeDomains,
 | 
					      this.includeDomains,
 | 
				
			||||||
      this.excludeDomains,
 | 
					      this.excludeDomains,
 | 
				
			||||||
      this.includeDeviceClasses,
 | 
					      this.includeDeviceClasses,
 | 
				
			||||||
@@ -163,7 +339,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
      this.entityFilter,
 | 
					      this.entityFilter,
 | 
				
			||||||
      this.excludeLabels
 | 
					      this.excludeLabels
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
 | 
					  private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => {
 | 
				
			||||||
    if (!labels) {
 | 
					    if (!labels) {
 | 
				
			||||||
@@ -220,14 +395,12 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-generic-picker
 | 
					      <ha-generic-picker
 | 
				
			||||||
        .disabled=${this.disabled}
 | 
					 | 
				
			||||||
        .hass=${this.hass}
 | 
					        .hass=${this.hass}
 | 
				
			||||||
        .autofocus=${this.autofocus}
 | 
					        .autofocus=${this.autofocus}
 | 
				
			||||||
        .label=${this.label}
 | 
					        .label=${this.label}
 | 
				
			||||||
        .notFoundLabel=${this.hass.localize(
 | 
					        .notFoundLabel=${this.hass.localize(
 | 
				
			||||||
          "ui.components.label-picker.no_match"
 | 
					          "ui.components.label-picker.no_match"
 | 
				
			||||||
        )}
 | 
					        )}
 | 
				
			||||||
        .addButtonLabel=${this.hass.localize("ui.components.label-picker.add")}
 | 
					 | 
				
			||||||
        .placeholder=${placeholder}
 | 
					        .placeholder=${placeholder}
 | 
				
			||||||
        .value=${this.value}
 | 
					        .value=${this.value}
 | 
				
			||||||
        .getItems=${this._getItems}
 | 
					        .getItems=${this._getItems}
 | 
				
			||||||
@@ -235,7 +408,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
        .valueRenderer=${valueRenderer}
 | 
					        .valueRenderer=${valueRenderer}
 | 
				
			||||||
        @value-changed=${this._valueChanged}
 | 
					        @value-changed=${this._valueChanged}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <slot .slot=${this._slotNodes?.length ? "field" : undefined}></slot>
 | 
					 | 
				
			||||||
      </ha-generic-picker>
 | 
					      </ha-generic-picker>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import { mdiPlaylistPlus } from "@mdi/js";
 | 
					 | 
				
			||||||
import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
					import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
				
			||||||
import type { TemplateResult } from "lit";
 | 
					import type { TemplateResult } from "lit";
 | 
				
			||||||
import { LitElement, css, html, nothing } from "lit";
 | 
					import { LitElement, css, html, nothing } from "lit";
 | 
				
			||||||
@@ -124,6 +123,36 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
					      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
				
			||||||
 | 
					      ${labels?.length
 | 
				
			||||||
 | 
					        ? html`<ha-chip-set>
 | 
				
			||||||
 | 
					            ${repeat(
 | 
				
			||||||
 | 
					              labels,
 | 
				
			||||||
 | 
					              (label) => label?.label_id,
 | 
				
			||||||
 | 
					              (label) => {
 | 
				
			||||||
 | 
					                const color = label?.color
 | 
				
			||||||
 | 
					                  ? computeCssColor(label.color)
 | 
				
			||||||
 | 
					                  : undefined;
 | 
				
			||||||
 | 
					                return html`
 | 
				
			||||||
 | 
					                  <ha-input-chip
 | 
				
			||||||
 | 
					                    .item=${label}
 | 
				
			||||||
 | 
					                    @remove=${this._removeItem}
 | 
				
			||||||
 | 
					                    @click=${this._openDetail}
 | 
				
			||||||
 | 
					                    .label=${label?.name}
 | 
				
			||||||
 | 
					                    selected
 | 
				
			||||||
 | 
					                    style=${color ? `--color: ${color}` : ""}
 | 
				
			||||||
 | 
					                  >
 | 
				
			||||||
 | 
					                    ${label?.icon
 | 
				
			||||||
 | 
					                      ? html`<ha-icon
 | 
				
			||||||
 | 
					                          slot="icon"
 | 
				
			||||||
 | 
					                          .icon=${label.icon}
 | 
				
			||||||
 | 
					                        ></ha-icon>`
 | 
				
			||||||
 | 
					                      : nothing}
 | 
				
			||||||
 | 
					                  </ha-input-chip>
 | 
				
			||||||
 | 
					                `;
 | 
				
			||||||
 | 
					              }
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					          </ha-chip-set>`
 | 
				
			||||||
 | 
					        : nothing}
 | 
				
			||||||
      <ha-label-picker
 | 
					      <ha-label-picker
 | 
				
			||||||
        .hass=${this.hass}
 | 
					        .hass=${this.hass}
 | 
				
			||||||
        .helper=${this.helper}
 | 
					        .helper=${this.helper}
 | 
				
			||||||
@@ -133,47 +162,6 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
        .excludeLabels=${this.value}
 | 
					        .excludeLabels=${this.value}
 | 
				
			||||||
        @value-changed=${this._labelChanged}
 | 
					        @value-changed=${this._labelChanged}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <ha-chip-set>
 | 
					 | 
				
			||||||
          ${labels?.length
 | 
					 | 
				
			||||||
            ? repeat(
 | 
					 | 
				
			||||||
                labels,
 | 
					 | 
				
			||||||
                (label) => label?.label_id,
 | 
					 | 
				
			||||||
                (label) => {
 | 
					 | 
				
			||||||
                  const color = label?.color
 | 
					 | 
				
			||||||
                    ? computeCssColor(label.color)
 | 
					 | 
				
			||||||
                    : undefined;
 | 
					 | 
				
			||||||
                  return html`
 | 
					 | 
				
			||||||
                    <ha-input-chip
 | 
					 | 
				
			||||||
                      .item=${label}
 | 
					 | 
				
			||||||
                      @remove=${this._removeItem}
 | 
					 | 
				
			||||||
                      @click=${this._openDetail}
 | 
					 | 
				
			||||||
                      .disabled=${this.disabled}
 | 
					 | 
				
			||||||
                      .label=${label?.name}
 | 
					 | 
				
			||||||
                      selected
 | 
					 | 
				
			||||||
                      style=${color ? `--color: ${color}` : ""}
 | 
					 | 
				
			||||||
                    >
 | 
					 | 
				
			||||||
                      ${label?.icon
 | 
					 | 
				
			||||||
                        ? html`<ha-icon
 | 
					 | 
				
			||||||
                            slot="icon"
 | 
					 | 
				
			||||||
                            .icon=${label.icon}
 | 
					 | 
				
			||||||
                          ></ha-icon>`
 | 
					 | 
				
			||||||
                        : nothing}
 | 
					 | 
				
			||||||
                    </ha-input-chip>
 | 
					 | 
				
			||||||
                  `;
 | 
					 | 
				
			||||||
                }
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
            : nothing}
 | 
					 | 
				
			||||||
          <ha-button
 | 
					 | 
				
			||||||
            id="picker"
 | 
					 | 
				
			||||||
            size="small"
 | 
					 | 
				
			||||||
            appearance="filled"
 | 
					 | 
				
			||||||
            @click=${this._openPicker}
 | 
					 | 
				
			||||||
            .disabled=${this.disabled}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <ha-svg-icon .path=${mdiPlaylistPlus} slot="start"></ha-svg-icon>
 | 
					 | 
				
			||||||
            ${this.hass.localize("ui.components.label-picker.add")}
 | 
					 | 
				
			||||||
          </ha-button>
 | 
					 | 
				
			||||||
        </ha-chip-set>
 | 
					 | 
				
			||||||
      </ha-label-picker>
 | 
					      </ha-label-picker>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
@@ -215,25 +203,9 @@ export class HaLabelsPicker extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
    }, 0);
 | 
					    }, 0);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _openPicker(ev: Event) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    this.labelPicker.open();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static styles = css`
 | 
					  static styles = css`
 | 
				
			||||||
    ha-chip-set {
 | 
					    ha-chip-set {
 | 
				
			||||||
      margin-bottom: 8px;
 | 
					      margin-bottom: 8px;
 | 
				
			||||||
      background-color: var(--mdc-text-field-fill-color);
 | 
					 | 
				
			||||||
      border-bottom: 1px solid var(--ha-color-border-neutral-normal);
 | 
					 | 
				
			||||||
      border-top-right-radius: var(--ha-border-radius-sm);
 | 
					 | 
				
			||||||
      border-top-left-radius: var(--ha-border-radius-sm);
 | 
					 | 
				
			||||||
      padding: var(--ha-space-3);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .placeholder {
 | 
					 | 
				
			||||||
      color: var(--mdc-text-field-label-ink-color);
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      height: var(--ha-space-8);
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    ha-input-chip {
 | 
					    ha-input-chip {
 | 
				
			||||||
      --md-input-chip-selected-container-color: var(--color, var(--grey-color));
 | 
					      --md-input-chip-selected-container-color: var(--color, var(--grey-color));
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,58 +1,56 @@
 | 
				
			|||||||
import type { PropertyValues } from "lit";
 | 
					import type { PropertyValues } from "lit";
 | 
				
			||||||
import { css, html, LitElement } from "lit";
 | 
					import { css, html, LitElement } from "lit";
 | 
				
			||||||
import { customElement, property, state } from "lit/decorators";
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
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 { stopPropagation } from "../common/dom/stop_propagation";
 | 
				
			||||||
import { formatLanguageCode } from "../common/language/format_language";
 | 
					import { formatLanguageCode } from "../common/language/format_language";
 | 
				
			||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
 | 
					import { caseInsensitiveStringCompare } from "../common/string/compare";
 | 
				
			||||||
import type { FrontendLocaleData } from "../data/translation";
 | 
					import type { FrontendLocaleData } from "../data/translation";
 | 
				
			||||||
import { translationMetadata } from "../resources/translations-metadata";
 | 
					import { translationMetadata } from "../resources/translations-metadata";
 | 
				
			||||||
import type { HomeAssistant, ValueChangedEvent } from "../types";
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
import "./ha-generic-picker";
 | 
					 | 
				
			||||||
import "./ha-list-item";
 | 
					import "./ha-list-item";
 | 
				
			||||||
import type { PickerComboBoxItem } from "./ha-picker-combo-box";
 | 
					 | 
				
			||||||
import "./ha-select";
 | 
					import "./ha-select";
 | 
				
			||||||
 | 
					import type { HaSelect } from "./ha-select";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getLanguageOptions = (
 | 
					export const getLanguageOptions = (
 | 
				
			||||||
  languages: string[],
 | 
					  languages: string[],
 | 
				
			||||||
  nativeName: boolean,
 | 
					  nativeName: boolean,
 | 
				
			||||||
  noSort: boolean,
 | 
					  noSort: boolean,
 | 
				
			||||||
  locale?: FrontendLocaleData
 | 
					  locale?: FrontendLocaleData
 | 
				
			||||||
): PickerComboBoxItem[] => {
 | 
					) => {
 | 
				
			||||||
  let options: PickerComboBoxItem[] = [];
 | 
					  let options: { label: string; value: string }[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (nativeName) {
 | 
					  if (nativeName) {
 | 
				
			||||||
    const translations = translationMetadata.translations;
 | 
					    const translations = translationMetadata.translations;
 | 
				
			||||||
    options = languages.map((lang) => {
 | 
					    options = languages.map((lang) => {
 | 
				
			||||||
      let primary = translations[lang]?.nativeName;
 | 
					      let label = translations[lang]?.nativeName;
 | 
				
			||||||
      if (!primary) {
 | 
					      if (!label) {
 | 
				
			||||||
        try {
 | 
					        try {
 | 
				
			||||||
          // this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
 | 
					          // this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user
 | 
				
			||||||
          primary = new Intl.DisplayNames(lang, {
 | 
					          label = new Intl.DisplayNames(lang, {
 | 
				
			||||||
            type: "language",
 | 
					            type: "language",
 | 
				
			||||||
            fallback: "code",
 | 
					            fallback: "code",
 | 
				
			||||||
          }).of(lang)!;
 | 
					          }).of(lang)!;
 | 
				
			||||||
        } catch (_err) {
 | 
					        } catch (_err) {
 | 
				
			||||||
          primary = lang;
 | 
					          label = lang;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      return {
 | 
					      return {
 | 
				
			||||||
        id: lang,
 | 
					        value: lang,
 | 
				
			||||||
        primary,
 | 
					        label,
 | 
				
			||||||
        search_labels: [primary],
 | 
					 | 
				
			||||||
      };
 | 
					      };
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
  } else if (locale) {
 | 
					  } else if (locale) {
 | 
				
			||||||
    options = languages.map((lang) => ({
 | 
					    options = languages.map((lang) => ({
 | 
				
			||||||
      id: lang,
 | 
					      value: lang,
 | 
				
			||||||
      primary: formatLanguageCode(lang, locale),
 | 
					      label: formatLanguageCode(lang, locale),
 | 
				
			||||||
      search_labels: [formatLanguageCode(lang, locale)],
 | 
					 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  if (!noSort && locale) {
 | 
					  if (!noSort && locale) {
 | 
				
			||||||
    options.sort((a, b) =>
 | 
					    options.sort((a, b) =>
 | 
				
			||||||
      caseInsensitiveStringCompare(a.primary, b.primary, locale.language)
 | 
					      caseInsensitiveStringCompare(a.label, b.label, locale.language)
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  return options;
 | 
					  return options;
 | 
				
			||||||
@@ -82,69 +80,115 @@ export class HaLanguagePicker extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @state() _defaultLanguages: string[] = [];
 | 
					  @state() _defaultLanguages: string[] = [];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @query("ha-select") private _select!: HaSelect;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected firstUpdated(changedProps: PropertyValues) {
 | 
					  protected firstUpdated(changedProps: PropertyValues) {
 | 
				
			||||||
    super.firstUpdated(changedProps);
 | 
					    super.firstUpdated(changedProps);
 | 
				
			||||||
    this._computeDefaultLanguageOptions();
 | 
					    this._computeDefaultLanguageOptions();
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected updated(changedProperties: PropertyValues) {
 | 
				
			||||||
 | 
					    super.updated(changedProperties);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const localeChanged =
 | 
				
			||||||
 | 
					      changedProperties.has("hass") &&
 | 
				
			||||||
 | 
					      this.hass &&
 | 
				
			||||||
 | 
					      changedProperties.get("hass") &&
 | 
				
			||||||
 | 
					      changedProperties.get("hass").locale.language !==
 | 
				
			||||||
 | 
					        this.hass.locale.language;
 | 
				
			||||||
 | 
					    if (
 | 
				
			||||||
 | 
					      changedProperties.has("languages") ||
 | 
				
			||||||
 | 
					      changedProperties.has("value") ||
 | 
				
			||||||
 | 
					      localeChanged
 | 
				
			||||||
 | 
					    ) {
 | 
				
			||||||
 | 
					      this._select.layoutOptions();
 | 
				
			||||||
 | 
					      if (!this.disabled && this._select.value !== this.value) {
 | 
				
			||||||
 | 
					        fireEvent(this, "value-changed", { value: this._select.value });
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (!this.value) {
 | 
				
			||||||
 | 
					        return;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      const languageOptions = this._getLanguagesOptions(
 | 
				
			||||||
 | 
					        this.languages ?? this._defaultLanguages,
 | 
				
			||||||
 | 
					        this.nativeName,
 | 
				
			||||||
 | 
					        this.noSort,
 | 
				
			||||||
 | 
					        this.hass?.locale
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      const selectedItemIndex = languageOptions.findIndex(
 | 
				
			||||||
 | 
					        (option) => option.value === this.value
 | 
				
			||||||
 | 
					      );
 | 
				
			||||||
 | 
					      if (selectedItemIndex === -1) {
 | 
				
			||||||
 | 
					        this.value = undefined;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      if (localeChanged) {
 | 
				
			||||||
 | 
					        this._select.select(selectedItemIndex);
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getLanguagesOptions = memoizeOne(getLanguageOptions);
 | 
					  private _getLanguagesOptions = memoizeOne(getLanguageOptions);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _computeDefaultLanguageOptions() {
 | 
					  private _computeDefaultLanguageOptions() {
 | 
				
			||||||
    this._defaultLanguages = Object.keys(translationMetadata.translations);
 | 
					    this._defaultLanguages = Object.keys(translationMetadata.translations);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getItems = () =>
 | 
					  protected render() {
 | 
				
			||||||
    this._getLanguagesOptions(
 | 
					    const languageOptions = this._getLanguagesOptions(
 | 
				
			||||||
      this.languages ?? this._defaultLanguages,
 | 
					      this.languages ?? this._defaultLanguages,
 | 
				
			||||||
      this.nativeName,
 | 
					      this.nativeName,
 | 
				
			||||||
      this.noSort,
 | 
					      this.noSort,
 | 
				
			||||||
      this.hass?.locale
 | 
					      this.hass?.locale
 | 
				
			||||||
    );
 | 
					    );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _valueRenderer = (value) => {
 | 
					 | 
				
			||||||
    const language = this._getItems().find(
 | 
					 | 
				
			||||||
      (lang) => lang.id === value
 | 
					 | 
				
			||||||
    )?.primary;
 | 
					 | 
				
			||||||
    return html`<span slot="headline">${language ?? value}</span> `;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render() {
 | 
					 | 
				
			||||||
    const value =
 | 
					    const value =
 | 
				
			||||||
      this.value ??
 | 
					      this.value ??
 | 
				
			||||||
      (this.required && !this.disabled ? this._getItems()[0].id : this.value);
 | 
					      (this.required && !this.disabled
 | 
				
			||||||
 | 
					        ? languageOptions[0]?.value
 | 
				
			||||||
 | 
					        : this.value);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-generic-picker
 | 
					      <ha-select
 | 
				
			||||||
        .hass=${this.hass}
 | 
					        .label=${this.label ??
 | 
				
			||||||
        .autofocus=${this.autofocus}
 | 
					 | 
				
			||||||
        popover-placement="bottom-end"
 | 
					 | 
				
			||||||
        .notFoundLabel=${this.hass?.localize(
 | 
					 | 
				
			||||||
          "ui.components.language-picker.no_match"
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        .placeholder=${this.label ??
 | 
					 | 
				
			||||||
        (this.hass?.localize("ui.components.language-picker.language") ||
 | 
					        (this.hass?.localize("ui.components.language-picker.language") ||
 | 
				
			||||||
          "Language")}
 | 
					          "Language")}
 | 
				
			||||||
        .value=${value}
 | 
					        .value=${value || ""}
 | 
				
			||||||
        .valueRenderer=${this._valueRenderer}
 | 
					        .required=${this.required}
 | 
				
			||||||
        .disabled=${this.disabled}
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
        .getItems=${this._getItems}
 | 
					        @selected=${this._changed}
 | 
				
			||||||
        @value-changed=${this._changed}
 | 
					        @closed=${stopPropagation}
 | 
				
			||||||
        hide-clear-icon
 | 
					        fixedMenuPosition
 | 
				
			||||||
      ></ha-generic-picker>
 | 
					        naturalMenuWidth
 | 
				
			||||||
 | 
					        .inlineArrow=${this.inlineArrow}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					        ${languageOptions.length === 0
 | 
				
			||||||
 | 
					          ? html`<ha-list-item value=""
 | 
				
			||||||
 | 
					              >${this.hass?.localize(
 | 
				
			||||||
 | 
					                "ui.components.language-picker.no_languages"
 | 
				
			||||||
 | 
					              ) || "No languages"}</ha-list-item
 | 
				
			||||||
 | 
					            >`
 | 
				
			||||||
 | 
					          : languageOptions.map(
 | 
				
			||||||
 | 
					              (option) => html`
 | 
				
			||||||
 | 
					                <ha-list-item .value=${option.value}
 | 
				
			||||||
 | 
					                  >${option.label}</ha-list-item
 | 
				
			||||||
 | 
					                >
 | 
				
			||||||
 | 
					              `
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					      </ha-select>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  static styles = css`
 | 
					  static styles = css`
 | 
				
			||||||
    ha-generic-picker {
 | 
					    ha-select {
 | 
				
			||||||
      width: 100%;
 | 
					      width: 100%;
 | 
				
			||||||
      min-width: 200px;
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _changed(ev: ValueChangedEvent<string>): void {
 | 
					  private _changed(ev): void {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    const target = ev.target as HaSelect;
 | 
				
			||||||
    this.value = ev.detail.value;
 | 
					    if (this.disabled || target.value === "" || target.value === this.value) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    this.value = target.value;
 | 
				
			||||||
    fireEvent(this, "value-changed", { value: this.value });
 | 
					    fireEvent(this, "value-changed", { value: this.value });
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -50,7 +50,7 @@ export class HaMarkdown extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
    ha-alert {
 | 
					    ha-alert {
 | 
				
			||||||
      display: block;
 | 
					      display: block;
 | 
				
			||||||
      margin: var(--ha-space-1) 0;
 | 
					      margin: 4px 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    a {
 | 
					    a {
 | 
				
			||||||
      color: var(--primary-color);
 | 
					      color: var(--primary-color);
 | 
				
			||||||
@@ -75,7 +75,7 @@ export class HaMarkdown extends LitElement {
 | 
				
			|||||||
      padding: 0;
 | 
					      padding: 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    pre {
 | 
					    pre {
 | 
				
			||||||
      padding: var(--ha-space-4);
 | 
					      padding: 16px;
 | 
				
			||||||
      overflow: auto;
 | 
					      overflow: auto;
 | 
				
			||||||
      line-height: var(--ha-line-height-condensed);
 | 
					      line-height: var(--ha-line-height-condensed);
 | 
				
			||||||
      font-family: var(--ha-font-family-code);
 | 
					      font-family: var(--ha-font-family-code);
 | 
				
			||||||
@@ -95,7 +95,7 @@ export class HaMarkdown extends LitElement {
 | 
				
			|||||||
    hr {
 | 
					    hr {
 | 
				
			||||||
      border-color: var(--divider-color);
 | 
					      border-color: var(--divider-color);
 | 
				
			||||||
      border-bottom: none;
 | 
					      border-bottom: none;
 | 
				
			||||||
      margin: var(--ha-space-4) 0;
 | 
					      margin: 16px 0;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
  ` as CSSResultGroup;
 | 
					  ` as CSSResultGroup;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,28 +1,19 @@
 | 
				
			|||||||
import type { LitVirtualizer } from "@lit-labs/virtualizer";
 | 
					 | 
				
			||||||
import type { RenderItemFunction } from "@lit-labs/virtualizer/virtualize";
 | 
					 | 
				
			||||||
import { mdiMagnify } from "@mdi/js";
 | 
					import { mdiMagnify } from "@mdi/js";
 | 
				
			||||||
 | 
					import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
				
			||||||
import Fuse from "fuse.js";
 | 
					import Fuse from "fuse.js";
 | 
				
			||||||
import { css, html, LitElement, nothing } from "lit";
 | 
					import type { PropertyValues, TemplateResult } from "lit";
 | 
				
			||||||
import {
 | 
					import { html, LitElement, nothing } from "lit";
 | 
				
			||||||
  customElement,
 | 
					import { customElement, property, query, state } from "lit/decorators";
 | 
				
			||||||
  eventOptions,
 | 
					 | 
				
			||||||
  property,
 | 
					 | 
				
			||||||
  query,
 | 
					 | 
				
			||||||
  state,
 | 
					 | 
				
			||||||
} from "lit/decorators";
 | 
					 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
import { tinykeys } from "tinykeys";
 | 
					 | 
				
			||||||
import { fireEvent } from "../common/dom/fire_event";
 | 
					import { fireEvent } from "../common/dom/fire_event";
 | 
				
			||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
 | 
					import { caseInsensitiveStringCompare } from "../common/string/compare";
 | 
				
			||||||
import type { LocalizeFunc } from "../common/translations/localize";
 | 
					import type { LocalizeFunc } from "../common/translations/localize";
 | 
				
			||||||
import { HaFuse } from "../resources/fuse";
 | 
					import { HaFuse } from "../resources/fuse";
 | 
				
			||||||
import { haStyleScrollbar } from "../resources/styles";
 | 
					import type { HomeAssistant, ValueChangedEvent } from "../types";
 | 
				
			||||||
import { loadVirtualizer } from "../resources/virtualizer";
 | 
					import "./ha-combo-box";
 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					import type { HaComboBox } from "./ha-combo-box";
 | 
				
			||||||
import "./ha-combo-box-item";
 | 
					import "./ha-combo-box-item";
 | 
				
			||||||
import "./ha-icon";
 | 
					import "./ha-icon";
 | 
				
			||||||
import "./ha-textfield";
 | 
					 | 
				
			||||||
import type { HaTextField } from "./ha-textfield";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface PickerComboBoxItem {
 | 
					export interface PickerComboBoxItem {
 | 
				
			||||||
  id: string;
 | 
					  id: string;
 | 
				
			||||||
@@ -42,13 +33,10 @@ export interface PickerComboBoxItemWithLabel extends PickerComboBoxItem {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
 | 
					const NO_MATCHING_ITEMS_FOUND_ID = "___no_matching_items_found___";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
const DEFAULT_ROW_RENDERER: RenderItemFunction<PickerComboBoxItem> = (
 | 
					const DEFAULT_ROW_RENDERER: ComboBoxLitRenderer<PickerComboBoxItem> = (
 | 
				
			||||||
  item
 | 
					  item
 | 
				
			||||||
) => html`
 | 
					) => html`
 | 
				
			||||||
  <ha-combo-box-item
 | 
					  <ha-combo-box-item type="button" compact>
 | 
				
			||||||
    .type=${item.id === NO_MATCHING_ITEMS_FOUND_ID ? "text" : "button"}
 | 
					 | 
				
			||||||
    compact
 | 
					 | 
				
			||||||
  >
 | 
					 | 
				
			||||||
    ${item.icon
 | 
					    ${item.icon
 | 
				
			||||||
      ? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
 | 
					      ? html`<ha-icon slot="start" .icon=${item.icon}></ha-icon>`
 | 
				
			||||||
      : item.icon_path
 | 
					      : item.icon_path
 | 
				
			||||||
@@ -69,7 +57,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = (
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
@customElement("ha-picker-combo-box")
 | 
					@customElement("ha-picker-combo-box")
 | 
				
			||||||
export class HaPickerComboBox extends LitElement {
 | 
					export class HaPickerComboBox extends LitElement {
 | 
				
			||||||
  @property({ attribute: false }) public hass?: HomeAssistant;
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  // eslint-disable-next-line lit/no-native-attributes
 | 
					  // eslint-disable-next-line lit/no-native-attributes
 | 
				
			||||||
  @property({ type: Boolean }) public autofocus = false;
 | 
					  @property({ type: Boolean }) public autofocus = false;
 | 
				
			||||||
@@ -85,7 +73,7 @@ export class HaPickerComboBox extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property() public value?: string;
 | 
					  @property() public value?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _listScrolled = false;
 | 
					  @property() public helper?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false, type: Array })
 | 
					  @property({ attribute: false, type: Array })
 | 
				
			||||||
  public getItems?: () => PickerComboBoxItem[];
 | 
					  public getItems?: () => PickerComboBoxItem[];
 | 
				
			||||||
@@ -94,7 +82,10 @@ export class HaPickerComboBox extends LitElement {
 | 
				
			|||||||
  public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
 | 
					  public getAdditionalItems?: (searchString?: string) => PickerComboBoxItem[];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: false })
 | 
					  @property({ attribute: false })
 | 
				
			||||||
  public rowRenderer?: RenderItemFunction<PickerComboBoxItem>;
 | 
					  public rowRenderer?: ComboBoxLitRenderer<PickerComboBoxItem>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ attribute: "hide-clear-icon", type: Boolean })
 | 
				
			||||||
 | 
					  public hideClearIcon = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: "not-found-label", type: String })
 | 
					  @property({ attribute: "not-found-label", type: String })
 | 
				
			||||||
  public notFoundLabel?: string;
 | 
					  public notFoundLabel?: string;
 | 
				
			||||||
@@ -102,77 +93,33 @@ export class HaPickerComboBox extends LitElement {
 | 
				
			|||||||
  @property({ attribute: false })
 | 
					  @property({ attribute: false })
 | 
				
			||||||
  public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
 | 
					  public searchFn?: PickerComboBoxSearchFn<PickerComboBoxItem>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ reflect: true }) public mode: "popover" | "dialog" = "popover";
 | 
					  @state() private _opened = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("lit-virtualizer") private _virtualizerElement?: LitVirtualizer;
 | 
					  @query("ha-combo-box", true) public comboBox!: HaComboBox;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query("ha-textfield") private _searchFieldElement?: HaTextField;
 | 
					  public async open() {
 | 
				
			||||||
 | 
					    await this.updateComplete;
 | 
				
			||||||
  @state() private _items: PickerComboBoxItemWithLabel[] = [];
 | 
					    await this.comboBox?.open();
 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _allItems: PickerComboBoxItemWithLabel[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _selectedItemIndex = -1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static shadowRootOptions = {
 | 
					 | 
				
			||||||
    ...LitElement.shadowRootOptions,
 | 
					 | 
				
			||||||
    delegatesFocus: true,
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _removeKeyboardShortcuts?: () => void;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected firstUpdated() {
 | 
					 | 
				
			||||||
    this._registerKeyboardShortcuts();
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  public willUpdate() {
 | 
					  public async focus() {
 | 
				
			||||||
    if (!this.hasUpdated) {
 | 
					    await this.updateComplete;
 | 
				
			||||||
      loadVirtualizer();
 | 
					    await this.comboBox?.focus();
 | 
				
			||||||
      this._allItems = this._getItems();
 | 
					 | 
				
			||||||
      this._items = this._allItems;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  disconnectedCallback() {
 | 
					  private _initialItems = false;
 | 
				
			||||||
    super.disconnectedCallback();
 | 
					 | 
				
			||||||
    this._removeKeyboardShortcuts?.();
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected render() {
 | 
					  private _items: PickerComboBoxItemWithLabel[] = [];
 | 
				
			||||||
    return html`<ha-textfield
 | 
					 | 
				
			||||||
        .label=${this.label ??
 | 
					 | 
				
			||||||
        this.hass?.localize("ui.common.search") ??
 | 
					 | 
				
			||||||
        "Search"}
 | 
					 | 
				
			||||||
        @input=${this._filterChanged}
 | 
					 | 
				
			||||||
      ></ha-textfield>
 | 
					 | 
				
			||||||
      <lit-virtualizer
 | 
					 | 
				
			||||||
        @scroll=${this._onScrollList}
 | 
					 | 
				
			||||||
        tabindex="0"
 | 
					 | 
				
			||||||
        scroller
 | 
					 | 
				
			||||||
        .items=${this._items}
 | 
					 | 
				
			||||||
        .renderItem=${this._renderItem}
 | 
					 | 
				
			||||||
        style="min-height: 36px;"
 | 
					 | 
				
			||||||
        class=${this._listScrolled ? "scrolled" : ""}
 | 
					 | 
				
			||||||
        @focus=${this._focusList}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
      </lit-virtualizer> `;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _defaultNotFoundItem = memoizeOne(
 | 
					  private _defaultNotFoundItem = memoizeOne(
 | 
				
			||||||
    (
 | 
					    (
 | 
				
			||||||
      label: this["notFoundLabel"],
 | 
					      label: this["notFoundLabel"],
 | 
				
			||||||
      localize?: LocalizeFunc
 | 
					      localize: LocalizeFunc
 | 
				
			||||||
    ): PickerComboBoxItemWithLabel => ({
 | 
					    ): PickerComboBoxItemWithLabel => ({
 | 
				
			||||||
      id: NO_MATCHING_ITEMS_FOUND_ID,
 | 
					      id: NO_MATCHING_ITEMS_FOUND_ID,
 | 
				
			||||||
      primary:
 | 
					      primary: label || localize("ui.components.combo-box.no_match"),
 | 
				
			||||||
        label ||
 | 
					 | 
				
			||||||
        (localize && localize("ui.components.combo-box.no_match")) ||
 | 
					 | 
				
			||||||
        "No matching items found",
 | 
					 | 
				
			||||||
      icon_path: mdiMagnify,
 | 
					      icon_path: mdiMagnify,
 | 
				
			||||||
      a11y_label:
 | 
					      a11y_label: label || localize("ui.components.combo-box.no_match"),
 | 
				
			||||||
        label ||
 | 
					 | 
				
			||||||
        (localize && localize("ui.components.combo-box.no_match")) ||
 | 
					 | 
				
			||||||
        "No matching items found",
 | 
					 | 
				
			||||||
    })
 | 
					    })
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -197,13 +144,13 @@ export class HaPickerComboBox extends LitElement {
 | 
				
			|||||||
        caseInsensitiveStringCompare(
 | 
					        caseInsensitiveStringCompare(
 | 
				
			||||||
          entityA.sorting_label!,
 | 
					          entityA.sorting_label!,
 | 
				
			||||||
          entityB.sorting_label!,
 | 
					          entityB.sorting_label!,
 | 
				
			||||||
          this.hass?.locale.language ?? navigator.language
 | 
					          this.hass.locale.language
 | 
				
			||||||
        )
 | 
					        )
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!sortedItems.length) {
 | 
					    if (!sortedItems.length) {
 | 
				
			||||||
      sortedItems.push(
 | 
					      sortedItems.push(
 | 
				
			||||||
        this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
 | 
					        this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
 | 
				
			||||||
      );
 | 
					      );
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -212,73 +159,99 @@ export class HaPickerComboBox extends LitElement {
 | 
				
			|||||||
    return sortedItems;
 | 
					    return sortedItems;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _renderItem = (item: PickerComboBoxItem, index: number) => {
 | 
					  protected shouldUpdate(changedProps: PropertyValues) {
 | 
				
			||||||
    const renderer = this.rowRenderer || DEFAULT_ROW_RENDERER;
 | 
					    if (
 | 
				
			||||||
    return html`<div
 | 
					      changedProps.has("value") ||
 | 
				
			||||||
      id=${`list-item-${index}`}
 | 
					      changedProps.has("label") ||
 | 
				
			||||||
      class="combo-box-row ${this._value === item.id ? "current-value" : ""}"
 | 
					      changedProps.has("disabled")
 | 
				
			||||||
      .value=${item.id}
 | 
					    ) {
 | 
				
			||||||
      .index=${index}
 | 
					      return true;
 | 
				
			||||||
      @click=${this._valueSelected}
 | 
					    }
 | 
				
			||||||
    >
 | 
					    return !(!changedProps.has("_opened") && this._opened);
 | 
				
			||||||
      ${item.id === NO_MATCHING_ITEMS_FOUND_ID
 | 
					  }
 | 
				
			||||||
        ? DEFAULT_ROW_RENDERER(item, index)
 | 
					 | 
				
			||||||
        : renderer(item, index)}
 | 
					 | 
				
			||||||
    </div>`;
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @eventOptions({ passive: true })
 | 
					  public willUpdate(changedProps: PropertyValues) {
 | 
				
			||||||
  private _onScrollList(ev) {
 | 
					    if (changedProps.has("_opened") && this._opened) {
 | 
				
			||||||
    const top = ev.target.scrollTop ?? 0;
 | 
					      this._items = this._getItems();
 | 
				
			||||||
    this._listScrolled = top > 0;
 | 
					      if (this._initialItems) {
 | 
				
			||||||
 | 
					        this.comboBox.filteredItems = this._items;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      this._initialItems = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected render(): TemplateResult {
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <ha-combo-box
 | 
				
			||||||
 | 
					        item-id-path="id"
 | 
				
			||||||
 | 
					        item-value-path="id"
 | 
				
			||||||
 | 
					        item-label-path="a11y_label"
 | 
				
			||||||
 | 
					        clear-initial-value
 | 
				
			||||||
 | 
					        .hass=${this.hass}
 | 
				
			||||||
 | 
					        .value=${this._value}
 | 
				
			||||||
 | 
					        .label=${this.label}
 | 
				
			||||||
 | 
					        .helper=${this.helper}
 | 
				
			||||||
 | 
					        .allowCustomValue=${this.allowCustomValue}
 | 
				
			||||||
 | 
					        .filteredItems=${this._items}
 | 
				
			||||||
 | 
					        .renderer=${this.rowRenderer || DEFAULT_ROW_RENDERER}
 | 
				
			||||||
 | 
					        .required=${this.required}
 | 
				
			||||||
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
 | 
					        .hideClearIcon=${this.hideClearIcon}
 | 
				
			||||||
 | 
					        @opened-changed=${this._openedChanged}
 | 
				
			||||||
 | 
					        @value-changed=${this._valueChanged}
 | 
				
			||||||
 | 
					        @filter-changed=${this._filterChanged}
 | 
				
			||||||
 | 
					      >
 | 
				
			||||||
 | 
					      </ha-combo-box>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private get _value() {
 | 
					  private get _value() {
 | 
				
			||||||
    return this.value || "";
 | 
					    return this.value || "";
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _valueSelected = (ev: Event) => {
 | 
					  private _openedChanged(ev: ValueChangedEvent<boolean>) {
 | 
				
			||||||
    ev.stopPropagation();
 | 
					    ev.stopPropagation();
 | 
				
			||||||
    const value = (ev.currentTarget as any).value as string;
 | 
					    if (ev.detail.value !== this._opened) {
 | 
				
			||||||
    const newValue = value?.trim();
 | 
					      this._opened = ev.detail.value;
 | 
				
			||||||
 | 
					      fireEvent(this, "opened-changed", { value: this._opened });
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _valueChanged(ev: ValueChangedEvent<string | undefined>) {
 | 
				
			||||||
 | 
					    ev.stopPropagation();
 | 
				
			||||||
 | 
					    // Clear the input field to prevent showing the old value next time
 | 
				
			||||||
 | 
					    this.comboBox.setTextFieldValue("");
 | 
				
			||||||
 | 
					    const newValue = ev.detail.value?.trim();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
 | 
					    if (newValue === NO_MATCHING_ITEMS_FOUND_ID) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    fireEvent(this, "value-changed", { value: newValue });
 | 
					    if (newValue !== this._value) {
 | 
				
			||||||
  };
 | 
					      this._setValue(newValue);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
 | 
					  private _fuseIndex = memoizeOne((states: PickerComboBoxItem[]) =>
 | 
				
			||||||
    Fuse.createIndex(["search_labels"], states)
 | 
					    Fuse.createIndex(["search_labels"], states)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _filterChanged = (ev: Event) => {
 | 
					  private _filterChanged(ev: CustomEvent): void {
 | 
				
			||||||
    const textfield = ev.target as HaTextField;
 | 
					    if (!this._opened) return;
 | 
				
			||||||
    const searchString = textfield.value.trim();
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (!searchString) {
 | 
					    const target = ev.target as HaComboBox;
 | 
				
			||||||
      this._items = this._allItems;
 | 
					    const searchString = ev.detail.value.trim() as string;
 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const index = this._fuseIndex(this._allItems);
 | 
					    const index = this._fuseIndex(this._items);
 | 
				
			||||||
    const fuse = new HaFuse(
 | 
					    const fuse = new HaFuse(this._items, { shouldSort: false }, index);
 | 
				
			||||||
      this._allItems,
 | 
					 | 
				
			||||||
      {
 | 
					 | 
				
			||||||
        shouldSort: false,
 | 
					 | 
				
			||||||
        minMatchCharLength: Math.min(searchString.length, 2),
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
      index
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
    const results = fuse.multiTermsSearch(searchString);
 | 
					    const results = fuse.multiTermsSearch(searchString);
 | 
				
			||||||
    let filteredItems = this._allItems as PickerComboBoxItem[];
 | 
					    let filteredItems = this._items as PickerComboBoxItem[];
 | 
				
			||||||
    if (results) {
 | 
					    if (results) {
 | 
				
			||||||
      const items = results.map((result) => result.item);
 | 
					      const items = results.map((result) => result.item);
 | 
				
			||||||
      if (items.length === 0) {
 | 
					      if (items.length === 0) {
 | 
				
			||||||
        items.push(
 | 
					        items.push(
 | 
				
			||||||
          this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize)
 | 
					          this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize)
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      const additionalItems = this._getAdditionalItems(searchString);
 | 
					      const additionalItems = this._getAdditionalItems(searchString);
 | 
				
			||||||
@@ -287,279 +260,17 @@ export class HaPickerComboBox extends LitElement {
 | 
				
			|||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.searchFn) {
 | 
					    if (this.searchFn) {
 | 
				
			||||||
      filteredItems = this.searchFn(
 | 
					      filteredItems = this.searchFn(searchString, filteredItems, this._items);
 | 
				
			||||||
        searchString,
 | 
					 | 
				
			||||||
        filteredItems,
 | 
					 | 
				
			||||||
        this._allItems
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    this._items = filteredItems as PickerComboBoxItemWithLabel[];
 | 
					    target.filteredItems = filteredItems;
 | 
				
			||||||
    this._selectedItemIndex = -1;
 | 
					 | 
				
			||||||
    if (this._virtualizerElement) {
 | 
					 | 
				
			||||||
      this._virtualizerElement.scrollTo(0, 0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _registerKeyboardShortcuts() {
 | 
					 | 
				
			||||||
    this._removeKeyboardShortcuts = tinykeys(this, {
 | 
					 | 
				
			||||||
      ArrowUp: this._selectPreviousItem,
 | 
					 | 
				
			||||||
      ArrowDown: this._selectNextItem,
 | 
					 | 
				
			||||||
      Home: this._selectFirstItem,
 | 
					 | 
				
			||||||
      End: this._selectLastItem,
 | 
					 | 
				
			||||||
      Enter: this._pickSelectedItem,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _focusList() {
 | 
					  private _setValue(value: string | undefined) {
 | 
				
			||||||
    if (this._selectedItemIndex === -1) {
 | 
					    setTimeout(() => {
 | 
				
			||||||
      this._selectNextItem();
 | 
					      fireEvent(this, "value-changed", { value });
 | 
				
			||||||
    }
 | 
					    }, 0);
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _selectNextItem = (ev?: KeyboardEvent) => {
 | 
					 | 
				
			||||||
    ev?.stopPropagation();
 | 
					 | 
				
			||||||
    ev?.preventDefault();
 | 
					 | 
				
			||||||
    if (!this._virtualizerElement) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._searchFieldElement?.focus();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const items = this._virtualizerElement.items as PickerComboBoxItem[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const maxItems = items.length - 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (maxItems === -1) {
 | 
					 | 
				
			||||||
      this._resetSelectedItem();
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const nextIndex =
 | 
					 | 
				
			||||||
      maxItems === this._selectedItemIndex
 | 
					 | 
				
			||||||
        ? this._selectedItemIndex
 | 
					 | 
				
			||||||
        : this._selectedItemIndex + 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (!items[nextIndex]) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (items[nextIndex].id === NO_MATCHING_ITEMS_FOUND_ID) {
 | 
					 | 
				
			||||||
      // Skip titles, padding and empty search
 | 
					 | 
				
			||||||
      if (nextIndex === maxItems) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      this._selectedItemIndex = nextIndex + 1;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this._selectedItemIndex = nextIndex;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._scrollToSelectedItem();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _selectPreviousItem = (ev: KeyboardEvent) => {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    ev.preventDefault();
 | 
					 | 
				
			||||||
    if (!this._virtualizerElement) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this._selectedItemIndex > 0) {
 | 
					 | 
				
			||||||
      const nextIndex = this._selectedItemIndex - 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const items = this._virtualizerElement.items as PickerComboBoxItem[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (!items[nextIndex]) {
 | 
					 | 
				
			||||||
        return;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (items[nextIndex]?.id === NO_MATCHING_ITEMS_FOUND_ID) {
 | 
					 | 
				
			||||||
        // Skip titles, padding and empty search
 | 
					 | 
				
			||||||
        if (nextIndex === 0) {
 | 
					 | 
				
			||||||
          return;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        this._selectedItemIndex = nextIndex - 1;
 | 
					 | 
				
			||||||
      } else {
 | 
					 | 
				
			||||||
        this._selectedItemIndex = nextIndex;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this._scrollToSelectedItem();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _selectFirstItem = (ev: KeyboardEvent) => {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const nextIndex = 0;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      (this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
 | 
					 | 
				
			||||||
      NO_MATCHING_ITEMS_FOUND_ID
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (typeof this._virtualizerElement.items[nextIndex] === "string") {
 | 
					 | 
				
			||||||
      this._selectedItemIndex = nextIndex + 1;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this._selectedItemIndex = nextIndex;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._scrollToSelectedItem();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _selectLastItem = (ev: KeyboardEvent) => {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    if (!this._virtualizerElement || !this._virtualizerElement.items.length) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const nextIndex = this._virtualizerElement.items.length - 1;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      (this._virtualizerElement.items[nextIndex] as PickerComboBoxItem)?.id ===
 | 
					 | 
				
			||||||
      NO_MATCHING_ITEMS_FOUND_ID
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (typeof this._virtualizerElement.items[nextIndex] === "string") {
 | 
					 | 
				
			||||||
      this._selectedItemIndex = nextIndex - 1;
 | 
					 | 
				
			||||||
    } else {
 | 
					 | 
				
			||||||
      this._selectedItemIndex = nextIndex;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._scrollToSelectedItem();
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _scrollToSelectedItem = () => {
 | 
					 | 
				
			||||||
    this._virtualizerElement
 | 
					 | 
				
			||||||
      ?.querySelector(".selected")
 | 
					 | 
				
			||||||
      ?.classList.remove("selected");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    this._virtualizerElement?.scrollToIndex(this._selectedItemIndex, "end");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    requestAnimationFrame(() => {
 | 
					 | 
				
			||||||
      this._virtualizerElement
 | 
					 | 
				
			||||||
        ?.querySelector(`#list-item-${this._selectedItemIndex}`)
 | 
					 | 
				
			||||||
        ?.classList.add("selected");
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _pickSelectedItem = (ev: KeyboardEvent) => {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      this._virtualizerElement?.items.length === 1 &&
 | 
					 | 
				
			||||||
      firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      fireEvent(this, "value-changed", {
 | 
					 | 
				
			||||||
        value: firstItem.id,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this._selectedItemIndex === -1) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // if filter button is focused
 | 
					 | 
				
			||||||
    ev.preventDefault();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const item = this._virtualizerElement?.items[
 | 
					 | 
				
			||||||
      this._selectedItemIndex
 | 
					 | 
				
			||||||
    ] as PickerComboBoxItem;
 | 
					 | 
				
			||||||
    if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) {
 | 
					 | 
				
			||||||
      fireEvent(this, "value-changed", { value: item.id });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _resetSelectedItem() {
 | 
					 | 
				
			||||||
    this._virtualizerElement
 | 
					 | 
				
			||||||
      ?.querySelector(".selected")
 | 
					 | 
				
			||||||
      ?.classList.remove("selected");
 | 
					 | 
				
			||||||
    this._selectedItemIndex = -1;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static styles = [
 | 
					 | 
				
			||||||
    haStyleScrollbar,
 | 
					 | 
				
			||||||
    css`
 | 
					 | 
				
			||||||
      :host {
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        flex-direction: column;
 | 
					 | 
				
			||||||
        padding-top: var(--ha-space-3);
 | 
					 | 
				
			||||||
        flex: 1;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      ha-textfield {
 | 
					 | 
				
			||||||
        padding: 0 var(--ha-space-3);
 | 
					 | 
				
			||||||
        margin-bottom: var(--ha-space-3);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      :host([mode="dialog"]) ha-textfield {
 | 
					 | 
				
			||||||
        padding: 0 var(--ha-space-4);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      ha-combo-box-item {
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      ha-combo-box-item.selected {
 | 
					 | 
				
			||||||
        background-color: var(--ha-color-fill-neutral-quiet-hover);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      @media (prefers-color-scheme: dark) {
 | 
					 | 
				
			||||||
        ha-combo-box-item.selected {
 | 
					 | 
				
			||||||
          background-color: var(--ha-color-fill-neutral-normal-hover);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      lit-virtualizer {
 | 
					 | 
				
			||||||
        flex: 1;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      lit-virtualizer:focus-visible {
 | 
					 | 
				
			||||||
        outline: none;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      lit-virtualizer.scrolled {
 | 
					 | 
				
			||||||
        border-top: 1px solid var(--ha-color-border-neutral-quiet);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .bottom-padding {
 | 
					 | 
				
			||||||
        height: max(var(--safe-area-inset-bottom, 0px), var(--ha-space-8));
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .empty {
 | 
					 | 
				
			||||||
        text-align: center;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .combo-box-row {
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        width: 100%;
 | 
					 | 
				
			||||||
        align-items: center;
 | 
					 | 
				
			||||||
        box-sizing: border-box;
 | 
					 | 
				
			||||||
        min-height: 36px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .combo-box-row.current-value {
 | 
					 | 
				
			||||||
        background-color: var(--ha-color-fill-primary-quiet-resting);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .combo-box-row.selected {
 | 
					 | 
				
			||||||
        background-color: var(--ha-color-fill-neutral-quiet-hover);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      @media (prefers-color-scheme: dark) {
 | 
					 | 
				
			||||||
        .combo-box-row.selected {
 | 
					 | 
				
			||||||
          background-color: var(--ha-color-fill-neutral-normal-hover);
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    `,
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
declare global {
 | 
					declare global {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -137,7 +137,7 @@ export class HaSelect extends SelectBase {
 | 
				
			|||||||
        height: var(--ha-select-height, 56px);
 | 
					        height: var(--ha-select-height, 56px);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .mdc-select--filled .mdc-floating-label {
 | 
					      .mdc-select--filled .mdc-floating-label {
 | 
				
			||||||
        inset-inline-start: var(--ha-space-4);
 | 
					        inset-inline-start: 12px;
 | 
				
			||||||
        inset-inline-end: initial;
 | 
					        inset-inline-end: initial;
 | 
				
			||||||
        direction: var(--direction);
 | 
					        direction: var(--direction);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -147,7 +147,7 @@ export class HaSelect extends SelectBase {
 | 
				
			|||||||
        direction: var(--direction);
 | 
					        direction: var(--direction);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      .mdc-select .mdc-select__anchor {
 | 
					      .mdc-select .mdc-select__anchor {
 | 
				
			||||||
        padding-inline-start: var(--ha-space-4);
 | 
					        padding-inline-start: 12px;
 | 
				
			||||||
        padding-inline-end: 0px;
 | 
					        padding-inline-end: 0px;
 | 
				
			||||||
        direction: var(--direction);
 | 
					        direction: var(--direction);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -158,10 +158,7 @@ export class HaSelect extends SelectBase {
 | 
				
			|||||||
        padding-inline-end: var(--select-selected-text-padding-end, 0px);
 | 
					        padding-inline-end: var(--select-selected-text-padding-end, 0px);
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      :host([clearable]) .mdc-select__selected-text-container {
 | 
					      :host([clearable]) .mdc-select__selected-text-container {
 | 
				
			||||||
        padding-inline-end: var(
 | 
					        padding-inline-end: var(--select-selected-text-padding-end, 12px);
 | 
				
			||||||
          --select-selected-text-padding-end,
 | 
					 | 
				
			||||||
          var(--ha-space-4)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
      ha-icon-button {
 | 
					      ha-icon-button {
 | 
				
			||||||
        position: absolute;
 | 
					        position: absolute;
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										122
									
								
								src/components/ha-selector/ha-selector-background.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										122
									
								
								src/components/ha-selector/ha-selector-background.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,122 @@
 | 
				
			|||||||
 | 
					import { css, html, LitElement } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property, state } from "lit/decorators";
 | 
				
			||||||
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
 | 
					import type { BackgroundSelector } from "../../data/selector";
 | 
				
			||||||
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
 | 
					import "../ha-picture-upload";
 | 
				
			||||||
 | 
					import "../ha-alert";
 | 
				
			||||||
 | 
					import type { HaPictureUpload } from "../ha-picture-upload";
 | 
				
			||||||
 | 
					import { URL_PREFIX } from "../../data/image_upload";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("ha-selector-background")
 | 
				
			||||||
 | 
					export class HaBackgroundSelector extends LitElement {
 | 
				
			||||||
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public value?: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ attribute: false }) public selector!: BackgroundSelector;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ type: Boolean }) public disabled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ type: Boolean }) public required = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @state() private yamlBackground = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected updated(changedProps) {
 | 
				
			||||||
 | 
					    super.updated(changedProps);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (changedProps.has("value")) {
 | 
				
			||||||
 | 
					      this.yamlBackground = !!this.value && !this.value.startsWith(URL_PREFIX);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected render() {
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        ${this.yamlBackground
 | 
				
			||||||
 | 
					          ? html`
 | 
				
			||||||
 | 
					              <div class="value">
 | 
				
			||||||
 | 
					                <img
 | 
				
			||||||
 | 
					                  src=${this.value}
 | 
				
			||||||
 | 
					                  alt=${this.hass.localize(
 | 
				
			||||||
 | 
					                    "ui.components.picture-upload.current_image_alt"
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                />
 | 
				
			||||||
 | 
					              </div>
 | 
				
			||||||
 | 
					              <ha-alert alert-type="info">
 | 
				
			||||||
 | 
					                ${this.hass.localize(
 | 
				
			||||||
 | 
					                  `ui.components.selectors.background.yaml_info`
 | 
				
			||||||
 | 
					                )}
 | 
				
			||||||
 | 
					                <ha-button slot="action" @click=${this._clearValue}>
 | 
				
			||||||
 | 
					                  ${this.hass.localize(
 | 
				
			||||||
 | 
					                    `ui.components.picture-upload.clear_picture`
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </ha-button>
 | 
				
			||||||
 | 
					              </ha-alert>
 | 
				
			||||||
 | 
					            `
 | 
				
			||||||
 | 
					          : html`
 | 
				
			||||||
 | 
					              <ha-picture-upload
 | 
				
			||||||
 | 
					                .hass=${this.hass}
 | 
				
			||||||
 | 
					                .value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
 | 
				
			||||||
 | 
					                .original=${!!this.selector.background?.original}
 | 
				
			||||||
 | 
					                .cropOptions=${this.selector.background?.crop}
 | 
				
			||||||
 | 
					                select-media
 | 
				
			||||||
 | 
					                @change=${this._pictureChanged}
 | 
				
			||||||
 | 
					              ></ha-picture-upload>
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _pictureChanged(ev) {
 | 
				
			||||||
 | 
					    const value = (ev.target as HaPictureUpload).value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fireEvent(this, "value-changed", { value: value ?? undefined });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _clearValue() {
 | 
				
			||||||
 | 
					    fireEvent(this, "value-changed", { value: undefined });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static styles = css`
 | 
				
			||||||
 | 
					    :host {
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					      position: relative;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ha-picture-upload {
 | 
				
			||||||
 | 
					      background-color: var(--primary-background-color);
 | 
				
			||||||
 | 
					      border-radius: var(--file-upload-image-border-radius);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    div {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ha-button {
 | 
				
			||||||
 | 
					      white-space: nowrap;
 | 
				
			||||||
 | 
					      --mdc-theme-primary: var(--primary-color);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    .value {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					      align-items: center;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    img {
 | 
				
			||||||
 | 
					      max-width: 100%;
 | 
				
			||||||
 | 
					      max-height: 200px;
 | 
				
			||||||
 | 
					      margin-bottom: 4px;
 | 
				
			||||||
 | 
					      border-radius: var(--file-upload-image-border-radius);
 | 
				
			||||||
 | 
					      transition: opacity 0.3s;
 | 
				
			||||||
 | 
					      opacity: var(--picture-opacity, 1);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    img:hover {
 | 
				
			||||||
 | 
					      opacity: 1;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					    "ha-selector-background": HaBackgroundSelector;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -36,8 +36,6 @@ export class HaDeviceSelector extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property() public helper?: string;
 | 
					  @property() public helper?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property() public placeholder?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ type: Boolean }) public disabled = false;
 | 
					  @property({ type: Boolean }) public disabled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ type: Boolean }) public required = true;
 | 
					  @property({ type: Boolean }) public required = true;
 | 
				
			||||||
@@ -104,7 +102,6 @@ export class HaDeviceSelector extends LitElement {
 | 
				
			|||||||
          .entityFilter=${this.selector.device?.entity
 | 
					          .entityFilter=${this.selector.device?.entity
 | 
				
			||||||
            ? this._filterEntities
 | 
					            ? this._filterEntities
 | 
				
			||||||
            : undefined}
 | 
					            : undefined}
 | 
				
			||||||
          .placeholder=${this.placeholder}
 | 
					 | 
				
			||||||
          .disabled=${this.disabled}
 | 
					          .disabled=${this.disabled}
 | 
				
			||||||
          .required=${this.required}
 | 
					          .required=${this.required}
 | 
				
			||||||
          allow-custom-entity
 | 
					          allow-custom-entity
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,8 +29,6 @@ export class HaEntitySelector extends LitElement {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  @property() public helper?: string;
 | 
					  @property() public helper?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property() public placeholder?: any;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ type: Boolean }) public disabled = false;
 | 
					  @property({ type: Boolean }) public disabled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ type: Boolean }) public required = true;
 | 
					  @property({ type: Boolean }) public required = true;
 | 
				
			||||||
@@ -71,7 +69,6 @@ export class HaEntitySelector extends LitElement {
 | 
				
			|||||||
        .excludeEntities=${this.selector.entity?.exclude_entities}
 | 
					        .excludeEntities=${this.selector.entity?.exclude_entities}
 | 
				
			||||||
        .entityFilter=${this._filterEntities}
 | 
					        .entityFilter=${this._filterEntities}
 | 
				
			||||||
        .createDomains=${this._createDomains}
 | 
					        .createDomains=${this._createDomains}
 | 
				
			||||||
        .placeholder=${this.placeholder}
 | 
					 | 
				
			||||||
        .disabled=${this.disabled}
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
        .required=${this.required}
 | 
					        .required=${this.required}
 | 
				
			||||||
        allow-custom-entity
 | 
					        allow-custom-entity
 | 
				
			||||||
@@ -89,7 +86,6 @@ export class HaEntitySelector extends LitElement {
 | 
				
			|||||||
        .reorder=${this.selector.entity.reorder ?? false}
 | 
					        .reorder=${this.selector.entity.reorder ?? false}
 | 
				
			||||||
        .entityFilter=${this._filterEntities}
 | 
					        .entityFilter=${this._filterEntities}
 | 
				
			||||||
        .createDomains=${this._createDomains}
 | 
					        .createDomains=${this._createDomains}
 | 
				
			||||||
        .placeholder=${this.placeholder}
 | 
					 | 
				
			||||||
        .disabled=${this.disabled}
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
        .required=${this.required}
 | 
					        .required=${this.required}
 | 
				
			||||||
      ></ha-entities-picker>
 | 
					      ></ha-entities-picker>
 | 
				
			||||||
 
 | 
				
			|||||||
							
								
								
									
										152
									
								
								src/components/ha-selector/ha-selector-image.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										152
									
								
								src/components/ha-selector/ha-selector-image.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,152 @@
 | 
				
			|||||||
 | 
					import { css, html, LitElement } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property, state } from "lit/decorators";
 | 
				
			||||||
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
 | 
					import type { ImageSelector } from "../../data/selector";
 | 
				
			||||||
 | 
					import type { HomeAssistant } from "../../types";
 | 
				
			||||||
 | 
					import "../ha-icon-button";
 | 
				
			||||||
 | 
					import "../ha-textarea";
 | 
				
			||||||
 | 
					import "../ha-textfield";
 | 
				
			||||||
 | 
					import "../ha-picture-upload";
 | 
				
			||||||
 | 
					import "../ha-radio";
 | 
				
			||||||
 | 
					import "../ha-formfield";
 | 
				
			||||||
 | 
					import type { HaPictureUpload } from "../ha-picture-upload";
 | 
				
			||||||
 | 
					import { URL_PREFIX } from "../../data/image_upload";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("ha-selector-image")
 | 
				
			||||||
 | 
					export class HaImageSelector extends LitElement {
 | 
				
			||||||
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public value?: any;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public name?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public label?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public placeholder?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public helper?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ attribute: false }) public selector!: ImageSelector;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ type: Boolean }) public disabled = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property({ type: Boolean }) public required = true;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @state() private showUpload = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected firstUpdated(changedProps): void {
 | 
				
			||||||
 | 
					    super.firstUpdated(changedProps);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.value || this.value.startsWith(URL_PREFIX)) {
 | 
				
			||||||
 | 
					      this.showUpload = true;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected render() {
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <div>
 | 
				
			||||||
 | 
					        <label>
 | 
				
			||||||
 | 
					          ${this.hass.localize(
 | 
				
			||||||
 | 
					            "ui.components.selectors.image.select_image_with_label",
 | 
				
			||||||
 | 
					            {
 | 
				
			||||||
 | 
					              label:
 | 
				
			||||||
 | 
					                this.label ||
 | 
				
			||||||
 | 
					                this.hass.localize("ui.components.selectors.image.image"),
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					          )}
 | 
				
			||||||
 | 
					          <ha-formfield
 | 
				
			||||||
 | 
					            .label=${this.hass.localize("ui.components.selectors.image.upload")}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <ha-radio
 | 
				
			||||||
 | 
					              name="mode"
 | 
				
			||||||
 | 
					              value="upload"
 | 
				
			||||||
 | 
					              .checked=${this.showUpload}
 | 
				
			||||||
 | 
					              @change=${this._radioGroupPicked}
 | 
				
			||||||
 | 
					            ></ha-radio>
 | 
				
			||||||
 | 
					          </ha-formfield>
 | 
				
			||||||
 | 
					          <ha-formfield
 | 
				
			||||||
 | 
					            .label=${this.hass.localize("ui.components.selectors.image.url")}
 | 
				
			||||||
 | 
					          >
 | 
				
			||||||
 | 
					            <ha-radio
 | 
				
			||||||
 | 
					              name="mode"
 | 
				
			||||||
 | 
					              value="url"
 | 
				
			||||||
 | 
					              .checked=${!this.showUpload}
 | 
				
			||||||
 | 
					              @change=${this._radioGroupPicked}
 | 
				
			||||||
 | 
					            ></ha-radio>
 | 
				
			||||||
 | 
					          </ha-formfield>
 | 
				
			||||||
 | 
					        </label>
 | 
				
			||||||
 | 
					        ${!this.showUpload
 | 
				
			||||||
 | 
					          ? html`
 | 
				
			||||||
 | 
					              <ha-textfield
 | 
				
			||||||
 | 
					                .name=${this.name}
 | 
				
			||||||
 | 
					                .value=${this.value || ""}
 | 
				
			||||||
 | 
					                .placeholder=${this.placeholder || ""}
 | 
				
			||||||
 | 
					                .helper=${this.helper}
 | 
				
			||||||
 | 
					                helperPersistent
 | 
				
			||||||
 | 
					                .disabled=${this.disabled}
 | 
				
			||||||
 | 
					                @input=${this._handleChange}
 | 
				
			||||||
 | 
					                .label=${this.label || ""}
 | 
				
			||||||
 | 
					                .required=${this.required}
 | 
				
			||||||
 | 
					              ></ha-textfield>
 | 
				
			||||||
 | 
					            `
 | 
				
			||||||
 | 
					          : html`
 | 
				
			||||||
 | 
					              <ha-picture-upload
 | 
				
			||||||
 | 
					                .hass=${this.hass}
 | 
				
			||||||
 | 
					                .value=${this.value?.startsWith(URL_PREFIX) ? this.value : null}
 | 
				
			||||||
 | 
					                .original=${this.selector.image?.original}
 | 
				
			||||||
 | 
					                .cropOptions=${this.selector.image?.crop}
 | 
				
			||||||
 | 
					                select-media
 | 
				
			||||||
 | 
					                @change=${this._pictureChanged}
 | 
				
			||||||
 | 
					              ></ha-picture-upload>
 | 
				
			||||||
 | 
					            `}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _radioGroupPicked(ev): void {
 | 
				
			||||||
 | 
					    this.showUpload = ev.target.value === "upload";
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _pictureChanged(ev) {
 | 
				
			||||||
 | 
					    const value = (ev.target as HaPictureUpload).value;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fireEvent(this, "value-changed", { value: value ?? undefined });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _handleChange(ev) {
 | 
				
			||||||
 | 
					    let value = ev.target.value;
 | 
				
			||||||
 | 
					    if (this.value === value) {
 | 
				
			||||||
 | 
					      return;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    if (value === "" && !this.required) {
 | 
				
			||||||
 | 
					      value = undefined;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    fireEvent(this, "value-changed", { value });
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  static styles = css`
 | 
				
			||||||
 | 
					    :host {
 | 
				
			||||||
 | 
					      display: block;
 | 
				
			||||||
 | 
					      position: relative;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    div {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    label {
 | 
				
			||||||
 | 
					      display: flex;
 | 
				
			||||||
 | 
					      flex-direction: column;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    ha-textarea,
 | 
				
			||||||
 | 
					    ha-textfield {
 | 
				
			||||||
 | 
					      width: 100%;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  `;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					    "ha-selector-image": HaImageSelector;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -107,15 +107,14 @@ export class HaMediaSelector extends LitElement {
 | 
				
			|||||||
        supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
 | 
					        supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA));
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.selector.media?.image_upload && !this.value) {
 | 
					    if (this.selector.media?.image_upload && !this.value) {
 | 
				
			||||||
      return html`${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
					      return html`<ha-picture-upload
 | 
				
			||||||
        <ha-picture-upload
 | 
					        .hass=${this.hass}
 | 
				
			||||||
          .hass=${this.hass}
 | 
					        .value=${null}
 | 
				
			||||||
          .value=${null}
 | 
					        .contentIdHelper=${this.selector.media?.content_id_helper}
 | 
				
			||||||
          .contentIdHelper=${this.selector.media?.content_id_helper}
 | 
					        select-media
 | 
				
			||||||
          select-media
 | 
					        full-media
 | 
				
			||||||
          full-media
 | 
					        @media-picked=${this._pictureUploadMediaPicked}
 | 
				
			||||||
          @media-picked=${this._pictureUploadMediaPicked}
 | 
					      ></ha-picture-upload>`;
 | 
				
			||||||
        ></ha-picture-upload>`;
 | 
					 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
@@ -142,7 +141,6 @@ export class HaMediaSelector extends LitElement {
 | 
				
			|||||||
          `}
 | 
					          `}
 | 
				
			||||||
      ${!supportsBrowse
 | 
					      ${!supportsBrowse
 | 
				
			||||||
        ? html`
 | 
					        ? html`
 | 
				
			||||||
            ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
					 | 
				
			||||||
            <ha-alert>
 | 
					            <ha-alert>
 | 
				
			||||||
              ${this.hass.localize(
 | 
					              ${this.hass.localize(
 | 
				
			||||||
                "ui.components.selectors.media.browse_not_supported"
 | 
					                "ui.components.selectors.media.browse_not_supported"
 | 
				
			||||||
@@ -156,8 +154,7 @@ export class HaMediaSelector extends LitElement {
 | 
				
			|||||||
              .computeHelper=${this._computeHelperCallback}
 | 
					              .computeHelper=${this._computeHelperCallback}
 | 
				
			||||||
            ></ha-form>
 | 
					            ></ha-form>
 | 
				
			||||||
          `
 | 
					          `
 | 
				
			||||||
        : html`${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
					        : html`<ha-card
 | 
				
			||||||
            <ha-card
 | 
					 | 
				
			||||||
              outlined
 | 
					              outlined
 | 
				
			||||||
              tabindex="0"
 | 
					              tabindex="0"
 | 
				
			||||||
              role="button"
 | 
					              role="button"
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,9 +1,4 @@
 | 
				
			|||||||
import {
 | 
					import { mdiClose, mdiDelete, mdiDrag, mdiPencil } from "@mdi/js";
 | 
				
			||||||
  mdiClose,
 | 
					 | 
				
			||||||
  mdiDelete,
 | 
					 | 
				
			||||||
  mdiDragHorizontalVariant,
 | 
					 | 
				
			||||||
  mdiPencil,
 | 
					 | 
				
			||||||
} from "@mdi/js";
 | 
					 | 
				
			||||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
 | 
					import { css, html, LitElement, nothing, type PropertyValues } from "lit";
 | 
				
			||||||
import { customElement, property, query } from "lit/decorators";
 | 
					import { customElement, property, query } from "lit/decorators";
 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
@@ -52,10 +47,9 @@ export class HaObjectSelector extends LitElement {
 | 
				
			|||||||
    const translationKey = this.selector.object?.translation_key;
 | 
					    const translationKey = this.selector.object?.translation_key;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    if (this.localizeValue && translationKey) {
 | 
					    if (this.localizeValue && translationKey) {
 | 
				
			||||||
      const label =
 | 
					      const label = this.localizeValue(
 | 
				
			||||||
        this.localizeValue(`${translationKey}.fields.${schema.name}.name`) ||
 | 
					        `${translationKey}.fields.${schema.name}`
 | 
				
			||||||
        // Fallback for backward compatibility
 | 
					      );
 | 
				
			||||||
        this.localizeValue(`${translationKey}.fields.${schema.name}`);
 | 
					 | 
				
			||||||
      if (label) {
 | 
					      if (label) {
 | 
				
			||||||
        return label;
 | 
					        return label;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -63,20 +57,6 @@ export class HaObjectSelector extends LitElement {
 | 
				
			|||||||
    return this.selector.object?.fields?.[schema.name]?.label || schema.name;
 | 
					    return this.selector.object?.fields?.[schema.name]?.label || schema.name;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _computeHelper = (schema: HaFormSchema): string => {
 | 
					 | 
				
			||||||
    const translationKey = this.selector.object?.translation_key;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.localizeValue && translationKey) {
 | 
					 | 
				
			||||||
      const helper = this.localizeValue(
 | 
					 | 
				
			||||||
        `${translationKey}.fields.${schema.name}.description`
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      if (helper) {
 | 
					 | 
				
			||||||
        return helper;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return this.selector.object?.fields?.[schema.name]?.description || "";
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _renderItem(item: any, index: number) {
 | 
					  private _renderItem(item: any, index: number) {
 | 
				
			||||||
    const labelField =
 | 
					    const labelField =
 | 
				
			||||||
      this.selector.object!.label_field ||
 | 
					      this.selector.object!.label_field ||
 | 
				
			||||||
@@ -112,7 +92,7 @@ export class HaObjectSelector extends LitElement {
 | 
				
			|||||||
          ? html`
 | 
					          ? html`
 | 
				
			||||||
              <ha-svg-icon
 | 
					              <ha-svg-icon
 | 
				
			||||||
                class="handle"
 | 
					                class="handle"
 | 
				
			||||||
                .path=${mdiDragHorizontalVariant}
 | 
					                .path=${mdiDrag}
 | 
				
			||||||
                slot="start"
 | 
					                slot="start"
 | 
				
			||||||
              ></ha-svg-icon>
 | 
					              ></ha-svg-icon>
 | 
				
			||||||
            `
 | 
					            `
 | 
				
			||||||
@@ -229,7 +209,6 @@ export class HaObjectSelector extends LitElement {
 | 
				
			|||||||
      schema: this._schema(this.selector),
 | 
					      schema: this._schema(this.selector),
 | 
				
			||||||
      data: {},
 | 
					      data: {},
 | 
				
			||||||
      computeLabel: this._computeLabel,
 | 
					      computeLabel: this._computeLabel,
 | 
				
			||||||
      computeHelper: this._computeHelper,
 | 
					 | 
				
			||||||
      submitText: this.hass.localize("ui.common.add"),
 | 
					      submitText: this.hass.localize("ui.common.add"),
 | 
				
			||||||
    });
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,4 @@
 | 
				
			|||||||
import { mdiDragHorizontalVariant } from "@mdi/js";
 | 
					import { mdiDrag } from "@mdi/js";
 | 
				
			||||||
import { LitElement, css, html, nothing } from "lit";
 | 
					import { LitElement, css, html, nothing } from "lit";
 | 
				
			||||||
import { customElement, property, query } from "lit/decorators";
 | 
					import { customElement, property, query } from "lit/decorators";
 | 
				
			||||||
import { repeat } from "lit/directives/repeat";
 | 
					import { repeat } from "lit/directives/repeat";
 | 
				
			||||||
@@ -197,7 +197,7 @@ export class HaSelectSelector extends LitElement {
 | 
				
			|||||||
                            ? html`
 | 
					                            ? html`
 | 
				
			||||||
                                <ha-svg-icon
 | 
					                                <ha-svg-icon
 | 
				
			||||||
                                  slot="icon"
 | 
					                                  slot="icon"
 | 
				
			||||||
                                  .path=${mdiDragHorizontalVariant}
 | 
					                                  .path=${mdiDrag}
 | 
				
			||||||
                                ></ha-svg-icon>
 | 
					                                ></ha-svg-icon>
 | 
				
			||||||
                              `
 | 
					                              `
 | 
				
			||||||
                            : nothing}
 | 
					                            : nothing}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -36,7 +36,7 @@ export class HaSelectorUiStateContent extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
        .helper=${this.helper}
 | 
					        .helper=${this.helper}
 | 
				
			||||||
        .disabled=${this.disabled}
 | 
					        .disabled=${this.disabled}
 | 
				
			||||||
        .required=${this.required}
 | 
					        .required=${this.required}
 | 
				
			||||||
        .allowName=${this.selector.ui_state_content?.allow_name || false}
 | 
					        .allowName=${this.selector.ui_state_content?.allow_name}
 | 
				
			||||||
      ></ha-entity-state-content-picker>
 | 
					      ></ha-entity-state-content-picker>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -34,6 +34,8 @@ const LOAD_ELEMENTS = {
 | 
				
			|||||||
  file: () => import("./ha-selector-file"),
 | 
					  file: () => import("./ha-selector-file"),
 | 
				
			||||||
  floor: () => import("./ha-selector-floor"),
 | 
					  floor: () => import("./ha-selector-floor"),
 | 
				
			||||||
  label: () => import("./ha-selector-label"),
 | 
					  label: () => import("./ha-selector-label"),
 | 
				
			||||||
 | 
					  image: () => import("./ha-selector-image"),
 | 
				
			||||||
 | 
					  background: () => import("./ha-selector-background"),
 | 
				
			||||||
  language: () => import("./ha-selector-language"),
 | 
					  language: () => import("./ha-selector-language"),
 | 
				
			||||||
  navigation: () => import("./ha-selector-navigation"),
 | 
					  navigation: () => import("./ha-selector-navigation"),
 | 
				
			||||||
  number: () => import("./ha-selector-number"),
 | 
					  number: () => import("./ha-selector-number"),
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -94,9 +94,6 @@ export class HaServiceControl extends LitElement {
 | 
				
			|||||||
  @property({ attribute: "hide-picker", type: Boolean, reflect: true })
 | 
					  @property({ attribute: "hide-picker", type: Boolean, reflect: true })
 | 
				
			||||||
  public hidePicker = false;
 | 
					  public hidePicker = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @property({ attribute: "hide-description", type: Boolean })
 | 
					 | 
				
			||||||
  public hideDescription = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _value!: this["value"];
 | 
					  @state() private _value!: this["value"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @state() private _checkedKeys = new Set();
 | 
					  @state() private _checkedKeys = new Set();
 | 
				
			||||||
@@ -472,136 +469,135 @@ export class HaServiceControl extends LitElement {
 | 
				
			|||||||
      serviceData?.description;
 | 
					      serviceData?.description;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
    return html`${this.hidePicker
 | 
					    return html`${this.hidePicker
 | 
				
			||||||
      ? nothing
 | 
					        ? nothing
 | 
				
			||||||
      : html`<ha-service-picker
 | 
					        : html`<ha-service-picker
 | 
				
			||||||
          .hass=${this.hass}
 | 
					 | 
				
			||||||
          .value=${this._value?.action}
 | 
					 | 
				
			||||||
          .disabled=${this.disabled}
 | 
					 | 
				
			||||||
          @value-changed=${this._serviceChanged}
 | 
					 | 
				
			||||||
          .showServiceId=${this.showServiceId}
 | 
					 | 
				
			||||||
        ></ha-service-picker>`}
 | 
					 | 
				
			||||||
    ${this.hideDescription
 | 
					 | 
				
			||||||
      ? nothing
 | 
					 | 
				
			||||||
      : html`
 | 
					 | 
				
			||||||
          <div class="description">
 | 
					 | 
				
			||||||
            ${description ? html`<p>${description}</p>` : ""}
 | 
					 | 
				
			||||||
            ${this._manifest
 | 
					 | 
				
			||||||
              ? html` <a
 | 
					 | 
				
			||||||
                  href=${this._manifest.is_built_in
 | 
					 | 
				
			||||||
                    ? documentationUrl(
 | 
					 | 
				
			||||||
                        this.hass,
 | 
					 | 
				
			||||||
                        `/integrations/${this._manifest.domain}`
 | 
					 | 
				
			||||||
                      )
 | 
					 | 
				
			||||||
                    : this._manifest.documentation}
 | 
					 | 
				
			||||||
                  title=${this.hass.localize(
 | 
					 | 
				
			||||||
                    "ui.components.service-control.integration_doc"
 | 
					 | 
				
			||||||
                  )}
 | 
					 | 
				
			||||||
                  target="_blank"
 | 
					 | 
				
			||||||
                  rel="noreferrer"
 | 
					 | 
				
			||||||
                >
 | 
					 | 
				
			||||||
                  <ha-icon-button
 | 
					 | 
				
			||||||
                    .path=${mdiHelpCircle}
 | 
					 | 
				
			||||||
                    class="help-icon"
 | 
					 | 
				
			||||||
                  ></ha-icon-button>
 | 
					 | 
				
			||||||
                </a>`
 | 
					 | 
				
			||||||
              : nothing}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        `}
 | 
					 | 
				
			||||||
    ${serviceData && "target" in serviceData
 | 
					 | 
				
			||||||
      ? html`<ha-settings-row .narrow=${this.narrow}>
 | 
					 | 
				
			||||||
          ${hasOptional
 | 
					 | 
				
			||||||
            ? html`<div slot="prefix" class="checkbox-spacer"></div>`
 | 
					 | 
				
			||||||
            : ""}
 | 
					 | 
				
			||||||
          <span slot="heading"
 | 
					 | 
				
			||||||
            >${this.hass.localize("ui.components.service-control.target")}</span
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
          <span slot="description"
 | 
					 | 
				
			||||||
            >${this.hass.localize(
 | 
					 | 
				
			||||||
              "ui.components.service-control.target_secondary"
 | 
					 | 
				
			||||||
            )}</span
 | 
					 | 
				
			||||||
          ><ha-selector
 | 
					 | 
				
			||||||
            .hass=${this.hass}
 | 
					            .hass=${this.hass}
 | 
				
			||||||
            .selector=${this._targetSelector(
 | 
					            .value=${this._value?.action}
 | 
				
			||||||
              serviceData.target as TargetSelector,
 | 
					 | 
				
			||||||
              this._value?.target
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
            .disabled=${this.disabled}
 | 
					            .disabled=${this.disabled}
 | 
				
			||||||
            @value-changed=${this._targetChanged}
 | 
					            @value-changed=${this._serviceChanged}
 | 
				
			||||||
            .value=${this._value?.target}
 | 
					            .showServiceId=${this.showServiceId}
 | 
				
			||||||
          ></ha-selector
 | 
					          ></ha-service-picker>`}
 | 
				
			||||||
        ></ha-settings-row>`
 | 
					 | 
				
			||||||
      : entityId
 | 
					 | 
				
			||||||
        ? html`<ha-entity-picker
 | 
					 | 
				
			||||||
            .hass=${this.hass}
 | 
					 | 
				
			||||||
            .disabled=${this.disabled}
 | 
					 | 
				
			||||||
            .value=${this._value?.data?.entity_id}
 | 
					 | 
				
			||||||
            .label=${this.hass.localize(
 | 
					 | 
				
			||||||
              `component.${domain}.services.${serviceName}.fields.entity_id.description`
 | 
					 | 
				
			||||||
            ) || entityId.description}
 | 
					 | 
				
			||||||
            @value-changed=${this._entityPicked}
 | 
					 | 
				
			||||||
            allow-custom-entity
 | 
					 | 
				
			||||||
          ></ha-entity-picker>`
 | 
					 | 
				
			||||||
        : ""}
 | 
					 | 
				
			||||||
    ${shouldRenderServiceDataYaml
 | 
					 | 
				
			||||||
      ? html`<ha-yaml-editor
 | 
					 | 
				
			||||||
          .hass=${this.hass}
 | 
					 | 
				
			||||||
          .label=${this.hass.localize(
 | 
					 | 
				
			||||||
            "ui.components.service-control.action_data"
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
          .name=${"data"}
 | 
					 | 
				
			||||||
          .readOnly=${this.disabled}
 | 
					 | 
				
			||||||
          .defaultValue=${this._value?.data}
 | 
					 | 
				
			||||||
          @value-changed=${this._dataChanged}
 | 
					 | 
				
			||||||
        ></ha-yaml-editor>`
 | 
					 | 
				
			||||||
      : serviceData?.fields.map((dataField) => {
 | 
					 | 
				
			||||||
          if (!dataField.fields) {
 | 
					 | 
				
			||||||
            return this._renderField(
 | 
					 | 
				
			||||||
              dataField,
 | 
					 | 
				
			||||||
              hasOptional,
 | 
					 | 
				
			||||||
              domain,
 | 
					 | 
				
			||||||
              serviceName,
 | 
					 | 
				
			||||||
              targetEntities
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
          const fields = Object.entries(dataField.fields).map(
 | 
					      <div class="description">
 | 
				
			||||||
            ([key, field]) => ({ key, ...field })
 | 
					        ${description ? html`<p>${description}</p>` : ""}
 | 
				
			||||||
          );
 | 
					        ${this._manifest
 | 
				
			||||||
 | 
					          ? html` <a
 | 
				
			||||||
          return fields.length &&
 | 
					              href=${this._manifest.is_built_in
 | 
				
			||||||
            this._hasFilteredFields(fields, targetEntities)
 | 
					                ? documentationUrl(
 | 
				
			||||||
            ? html`<ha-expansion-panel
 | 
					                    this.hass,
 | 
				
			||||||
                left-chevron
 | 
					                    `/integrations/${this._manifest.domain}`
 | 
				
			||||||
                .expanded=${!dataField.collapsed}
 | 
					 | 
				
			||||||
                .header=${this.hass.localize(
 | 
					 | 
				
			||||||
                  `component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
 | 
					 | 
				
			||||||
                ) ||
 | 
					 | 
				
			||||||
                dataField.name ||
 | 
					 | 
				
			||||||
                dataField.key}
 | 
					 | 
				
			||||||
                .secondary=${this._getSectionDescription(
 | 
					 | 
				
			||||||
                  dataField,
 | 
					 | 
				
			||||||
                  domain,
 | 
					 | 
				
			||||||
                  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]) =>
 | 
					 | 
				
			||||||
                  this._renderField(
 | 
					 | 
				
			||||||
                    { key, ...field },
 | 
					 | 
				
			||||||
                    hasOptional,
 | 
					 | 
				
			||||||
                    domain,
 | 
					 | 
				
			||||||
                    serviceName,
 | 
					 | 
				
			||||||
                    targetEntities
 | 
					 | 
				
			||||||
                  )
 | 
					                  )
 | 
				
			||||||
                )}
 | 
					                : this._manifest.documentation}
 | 
				
			||||||
              </ha-expansion-panel>`
 | 
					              title=${this.hass.localize(
 | 
				
			||||||
            : nothing;
 | 
					                "ui.components.service-control.integration_doc"
 | 
				
			||||||
        })} `;
 | 
					              )}
 | 
				
			||||||
 | 
					              target="_blank"
 | 
				
			||||||
 | 
					              rel="noreferrer"
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					              <ha-icon-button
 | 
				
			||||||
 | 
					                .path=${mdiHelpCircle}
 | 
				
			||||||
 | 
					                class="help-icon"
 | 
				
			||||||
 | 
					              ></ha-icon-button>
 | 
				
			||||||
 | 
					            </a>`
 | 
				
			||||||
 | 
					          : nothing}
 | 
				
			||||||
 | 
					      </div>
 | 
				
			||||||
 | 
					      ${serviceData && "target" in serviceData
 | 
				
			||||||
 | 
					        ? html`<ha-settings-row .narrow=${this.narrow}>
 | 
				
			||||||
 | 
					            ${hasOptional
 | 
				
			||||||
 | 
					              ? html`<div slot="prefix" class="checkbox-spacer"></div>`
 | 
				
			||||||
 | 
					              : ""}
 | 
				
			||||||
 | 
					            <span slot="heading"
 | 
				
			||||||
 | 
					              >${this.hass.localize(
 | 
				
			||||||
 | 
					                "ui.components.service-control.target"
 | 
				
			||||||
 | 
					              )}</span
 | 
				
			||||||
 | 
					            >
 | 
				
			||||||
 | 
					            <span slot="description"
 | 
				
			||||||
 | 
					              >${this.hass.localize(
 | 
				
			||||||
 | 
					                "ui.components.service-control.target_secondary"
 | 
				
			||||||
 | 
					              )}</span
 | 
				
			||||||
 | 
					            ><ha-selector
 | 
				
			||||||
 | 
					              .hass=${this.hass}
 | 
				
			||||||
 | 
					              .selector=${this._targetSelector(
 | 
				
			||||||
 | 
					                serviceData.target as TargetSelector,
 | 
				
			||||||
 | 
					                this._value?.target
 | 
				
			||||||
 | 
					              )}
 | 
				
			||||||
 | 
					              .disabled=${this.disabled}
 | 
				
			||||||
 | 
					              @value-changed=${this._targetChanged}
 | 
				
			||||||
 | 
					              .value=${this._value?.target}
 | 
				
			||||||
 | 
					            ></ha-selector
 | 
				
			||||||
 | 
					          ></ha-settings-row>`
 | 
				
			||||||
 | 
					        : entityId
 | 
				
			||||||
 | 
					          ? html`<ha-entity-picker
 | 
				
			||||||
 | 
					              .hass=${this.hass}
 | 
				
			||||||
 | 
					              .disabled=${this.disabled}
 | 
				
			||||||
 | 
					              .value=${this._value?.data?.entity_id}
 | 
				
			||||||
 | 
					              .label=${this.hass.localize(
 | 
				
			||||||
 | 
					                `component.${domain}.services.${serviceName}.fields.entity_id.description`
 | 
				
			||||||
 | 
					              ) || entityId.description}
 | 
				
			||||||
 | 
					              @value-changed=${this._entityPicked}
 | 
				
			||||||
 | 
					              allow-custom-entity
 | 
				
			||||||
 | 
					            ></ha-entity-picker>`
 | 
				
			||||||
 | 
					          : ""}
 | 
				
			||||||
 | 
					      ${shouldRenderServiceDataYaml
 | 
				
			||||||
 | 
					        ? html`<ha-yaml-editor
 | 
				
			||||||
 | 
					            .hass=${this.hass}
 | 
				
			||||||
 | 
					            .label=${this.hass.localize(
 | 
				
			||||||
 | 
					              "ui.components.service-control.action_data"
 | 
				
			||||||
 | 
					            )}
 | 
				
			||||||
 | 
					            .name=${"data"}
 | 
				
			||||||
 | 
					            .readOnly=${this.disabled}
 | 
				
			||||||
 | 
					            .defaultValue=${this._value?.data}
 | 
				
			||||||
 | 
					            @value-changed=${this._dataChanged}
 | 
				
			||||||
 | 
					          ></ha-yaml-editor>`
 | 
				
			||||||
 | 
					        : serviceData?.fields.map((dataField) => {
 | 
				
			||||||
 | 
					            if (!dataField.fields) {
 | 
				
			||||||
 | 
					              return this._renderField(
 | 
				
			||||||
 | 
					                dataField,
 | 
				
			||||||
 | 
					                hasOptional,
 | 
				
			||||||
 | 
					                domain,
 | 
				
			||||||
 | 
					                serviceName,
 | 
				
			||||||
 | 
					                targetEntities
 | 
				
			||||||
 | 
					              );
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            const fields = Object.entries(dataField.fields).map(
 | 
				
			||||||
 | 
					              ([key, field]) => ({ key, ...field })
 | 
				
			||||||
 | 
					            );
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					            return fields.length &&
 | 
				
			||||||
 | 
					              this._hasFilteredFields(fields, targetEntities)
 | 
				
			||||||
 | 
					              ? html`<ha-expansion-panel
 | 
				
			||||||
 | 
					                  left-chevron
 | 
				
			||||||
 | 
					                  .expanded=${!dataField.collapsed}
 | 
				
			||||||
 | 
					                  .header=${this.hass.localize(
 | 
				
			||||||
 | 
					                    `component.${domain}.services.${serviceName}.sections.${dataField.key}.name`
 | 
				
			||||||
 | 
					                  ) ||
 | 
				
			||||||
 | 
					                  dataField.name ||
 | 
				
			||||||
 | 
					                  dataField.key}
 | 
				
			||||||
 | 
					                  .secondary=${this._getSectionDescription(
 | 
				
			||||||
 | 
					                    dataField,
 | 
				
			||||||
 | 
					                    domain,
 | 
				
			||||||
 | 
					                    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]) =>
 | 
				
			||||||
 | 
					                    this._renderField(
 | 
				
			||||||
 | 
					                      { key, ...field },
 | 
				
			||||||
 | 
					                      hasOptional,
 | 
				
			||||||
 | 
					                      domain,
 | 
				
			||||||
 | 
					                      serviceName,
 | 
				
			||||||
 | 
					                      targetEntities
 | 
				
			||||||
 | 
					                    )
 | 
				
			||||||
 | 
					                  )}
 | 
				
			||||||
 | 
					                </ha-expansion-panel>`
 | 
				
			||||||
 | 
					              : nothing;
 | 
				
			||||||
 | 
					          })} `;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _getSectionDescription(
 | 
					  private _getSectionDescription(
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -53,7 +53,7 @@ class HaServicePicker extends LitElement {
 | 
				
			|||||||
    item,
 | 
					    item,
 | 
				
			||||||
    { index }
 | 
					    { index }
 | 
				
			||||||
  ) => html`
 | 
					  ) => html`
 | 
				
			||||||
    <ha-combo-box-item type="button" .borderTop=${index !== 0}>
 | 
					    <ha-combo-box-item type="button" border-top .borderTop=${index !== 0}>
 | 
				
			||||||
      <ha-service-icon
 | 
					      <ha-service-icon
 | 
				
			||||||
        slot="start"
 | 
					        slot="start"
 | 
				
			||||||
        .hass=${this.hass}
 | 
					        .hass=${this.hass}
 | 
				
			||||||
@@ -76,42 +76,34 @@ class HaServicePicker extends LitElement {
 | 
				
			|||||||
    </ha-combo-box-item>
 | 
					    </ha-combo-box-item>
 | 
				
			||||||
  `;
 | 
					  `;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _valueRenderer = memoizeOne(
 | 
					  private _valueRenderer: PickerValueRenderer = (value) => {
 | 
				
			||||||
    (
 | 
					    const serviceId = value;
 | 
				
			||||||
      localize: LocalizeFunc,
 | 
					    const [domain, service] = serviceId.split(".");
 | 
				
			||||||
      services: HomeAssistant["services"]
 | 
					 | 
				
			||||||
    ): PickerValueRenderer =>
 | 
					 | 
				
			||||||
      (value) => {
 | 
					 | 
				
			||||||
        const serviceId = value;
 | 
					 | 
				
			||||||
        const [domain, service] = serviceId.split(".");
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
        if (!services[domain]?.[service]) {
 | 
					    if (!this.hass.services[domain]?.[service]) {
 | 
				
			||||||
          return html`
 | 
					      return html`
 | 
				
			||||||
            <ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
 | 
					        <ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon>
 | 
				
			||||||
            <span slot="headline">${value}</span>
 | 
					        <span slot="headline">${value}</span>
 | 
				
			||||||
          `;
 | 
					      `;
 | 
				
			||||||
        }
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        const serviceName =
 | 
					    const serviceName =
 | 
				
			||||||
          localize(`component.${domain}.services.${service}.name`) ||
 | 
					      this.hass.localize(`component.${domain}.services.${service}.name`) ||
 | 
				
			||||||
          services[domain][service].name ||
 | 
					      this.hass.services[domain][service].name ||
 | 
				
			||||||
          service;
 | 
					      service;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        return html`
 | 
					    return html`
 | 
				
			||||||
          <ha-service-icon
 | 
					      <ha-service-icon
 | 
				
			||||||
            slot="start"
 | 
					        slot="start"
 | 
				
			||||||
            .hass=${this.hass}
 | 
					        .hass=${this.hass}
 | 
				
			||||||
            .service=${serviceId}
 | 
					        .service=${serviceId}
 | 
				
			||||||
          ></ha-service-icon>
 | 
					      ></ha-service-icon>
 | 
				
			||||||
          <span slot="headline">${serviceName}</span>
 | 
					      <span slot="headline">${serviceName}</span>
 | 
				
			||||||
          ${this.showServiceId
 | 
					      ${this.showServiceId
 | 
				
			||||||
            ? html`<span slot="supporting-text" class="code"
 | 
					        ? html`<span slot="supporting-text" class="code">${serviceId}</span>`
 | 
				
			||||||
                >${serviceId}</span
 | 
					        : nothing}
 | 
				
			||||||
              >`
 | 
					    `;
 | 
				
			||||||
            : nothing}
 | 
					  };
 | 
				
			||||||
        `;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
  protected render(): TemplateResult {
 | 
					  protected render(): TemplateResult {
 | 
				
			||||||
    const placeholder =
 | 
					    const placeholder =
 | 
				
			||||||
@@ -131,10 +123,7 @@ class HaServicePicker extends LitElement {
 | 
				
			|||||||
        .value=${this.value}
 | 
					        .value=${this.value}
 | 
				
			||||||
        .getItems=${this._getItems}
 | 
					        .getItems=${this._getItems}
 | 
				
			||||||
        .rowRenderer=${this._rowRenderer}
 | 
					        .rowRenderer=${this._rowRenderer}
 | 
				
			||||||
        .valueRenderer=${this._valueRenderer(
 | 
					        .valueRenderer=${this._valueRenderer}
 | 
				
			||||||
          this.hass.localize,
 | 
					 | 
				
			||||||
          this.hass.services
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        @value-changed=${this._valueChanged}
 | 
					        @value-changed=${this._valueChanged}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
      </ha-generic-picker>
 | 
					      </ha-generic-picker>
 | 
				
			||||||
@@ -173,9 +162,7 @@ class HaServicePicker extends LitElement {
 | 
				
			|||||||
            const description =
 | 
					            const description =
 | 
				
			||||||
              this.hass.localize(
 | 
					              this.hass.localize(
 | 
				
			||||||
                `component.${domain}.services.${service}.description`
 | 
					                `component.${domain}.services.${service}.description`
 | 
				
			||||||
              ) ||
 | 
					              ) || services[domain][service].description;
 | 
				
			||||||
              services[domain][service].description ||
 | 
					 | 
				
			||||||
              "";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
            items.push({
 | 
					            items.push({
 | 
				
			||||||
              id: serviceId,
 | 
					              id: serviceId,
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -29,7 +29,6 @@ import memoizeOne from "memoize-one";
 | 
				
			|||||||
import { fireEvent } from "../common/dom/fire_event";
 | 
					import { fireEvent } from "../common/dom/fire_event";
 | 
				
			||||||
import { toggleAttribute } from "../common/dom/toggle_attribute";
 | 
					import { toggleAttribute } from "../common/dom/toggle_attribute";
 | 
				
			||||||
import { stringCompare } from "../common/string/compare";
 | 
					import { stringCompare } from "../common/string/compare";
 | 
				
			||||||
import { computeRTL } from "../common/util/compute_rtl";
 | 
					 | 
				
			||||||
import { throttle } from "../common/util/throttle";
 | 
					import { throttle } from "../common/util/throttle";
 | 
				
			||||||
import { subscribeFrontendUserData } from "../data/frontend";
 | 
					import { subscribeFrontendUserData } from "../data/frontend";
 | 
				
			||||||
import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
 | 
					import type { ActionHandlerDetail } from "../data/lovelace/action_handler";
 | 
				
			||||||
@@ -537,17 +536,11 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _renderUserItem(selectedPanel: string) {
 | 
					  private _renderUserItem(selectedPanel: string) {
 | 
				
			||||||
    const isRTL = computeRTL(this.hass);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html`
 | 
					    return html`
 | 
				
			||||||
      <ha-md-list-item
 | 
					      <ha-md-list-item
 | 
				
			||||||
        href="/profile"
 | 
					        href="/profile"
 | 
				
			||||||
        type="link"
 | 
					        type="link"
 | 
				
			||||||
        class=${classMap({
 | 
					        class="user ${selectedPanel === "profile" ? " selected" : ""}"
 | 
				
			||||||
          user: true,
 | 
					 | 
				
			||||||
          selected: selectedPanel === "profile",
 | 
					 | 
				
			||||||
          rtl: isRTL,
 | 
					 | 
				
			||||||
        })}
 | 
					 | 
				
			||||||
        @mouseenter=${this._itemMouseEnter}
 | 
					        @mouseenter=${this._itemMouseEnter}
 | 
				
			||||||
        @mouseleave=${this._itemMouseLeave}
 | 
					        @mouseleave=${this._itemMouseLeave}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
@@ -673,7 +666,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
    tooltip.style.display = "block";
 | 
					    tooltip.style.display = "block";
 | 
				
			||||||
    tooltip.style.position = "fixed";
 | 
					    tooltip.style.position = "fixed";
 | 
				
			||||||
    tooltip.style.top = `${top}px`;
 | 
					    tooltip.style.top = `${top}px`;
 | 
				
			||||||
    tooltip.style.left = `calc(${item.offsetLeft + item.clientWidth + 8}px + var(--safe-area-inset-left, var(--ha-space-0)))`;
 | 
					    tooltip.style.left = `calc(${item.offsetLeft + item.clientWidth + 8}px + var(--safe-area-inset-left, 0px))`;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _hideTooltip() {
 | 
					  private _hideTooltip() {
 | 
				
			||||||
@@ -712,17 +705,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          background-color: var(--sidebar-background-color);
 | 
					          background-color: var(--sidebar-background-color);
 | 
				
			||||||
          width: 100%;
 | 
					          width: 100%;
 | 
				
			||||||
          box-sizing: border-box;
 | 
					          box-sizing: border-box;
 | 
				
			||||||
          padding-bottom: calc(
 | 
					          padding-bottom: calc(14px + var(--safe-area-inset-bottom, 0px));
 | 
				
			||||||
            14px + var(--safe-area-inset-bottom, var(--ha-space-0))
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        .menu {
 | 
					        .menu {
 | 
				
			||||||
          height: calc(
 | 
					          height: calc(var(--header-height) + var(--safe-area-inset-top, 0px));
 | 
				
			||||||
            var(--header-height) + var(--safe-area-inset-top, var(--ha-space-0))
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          box-sizing: border-box;
 | 
					          box-sizing: border-box;
 | 
				
			||||||
          display: flex;
 | 
					          display: flex;
 | 
				
			||||||
          padding: 0 var(--ha-space-1);
 | 
					          padding: 0 4px;
 | 
				
			||||||
          border-bottom: 1px solid transparent;
 | 
					          border-bottom: 1px solid transparent;
 | 
				
			||||||
          white-space: nowrap;
 | 
					          white-space: nowrap;
 | 
				
			||||||
          font-weight: var(--ha-font-weight-normal);
 | 
					          font-weight: var(--ha-font-weight-normal);
 | 
				
			||||||
@@ -737,17 +726,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          );
 | 
					          );
 | 
				
			||||||
          font-size: var(--ha-font-size-xl);
 | 
					          font-size: var(--ha-font-size-xl);
 | 
				
			||||||
          align-items: center;
 | 
					          align-items: center;
 | 
				
			||||||
          padding-left: calc(
 | 
					          padding-left: calc(4px + var(--safe-area-inset-left, 0px));
 | 
				
			||||||
            var(--ha-space-1) + var(--safe-area-inset-left, var(--ha-space-0))
 | 
					          padding-inline-start: calc(4px + var(--safe-area-inset-left, 0px));
 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          padding-inline-start: calc(
 | 
					 | 
				
			||||||
            var(--ha-space-1) + var(--safe-area-inset-left, var(--ha-space-0))
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
          padding-inline-end: initial;
 | 
					          padding-inline-end: initial;
 | 
				
			||||||
          padding-top: var(--safe-area-inset-top, var(--ha-space-0));
 | 
					          padding-top: var(--safe-area-inset-top, 0px);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        :host([expanded]) .menu {
 | 
					        :host([expanded]) .menu {
 | 
				
			||||||
          width: calc(256px + var(--safe-area-inset-left, var(--ha-space-0)));
 | 
					          width: calc(256px + var(--safe-area-inset-left, 0px));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        :host([narrow][expanded]) .menu {
 | 
					        :host([narrow][expanded]) .menu {
 | 
				
			||||||
          width: 100%;
 | 
					          width: 100%;
 | 
				
			||||||
@@ -763,8 +748,8 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          display: none;
 | 
					          display: none;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        :host([narrow]) .title {
 | 
					        :host([narrow]) .title {
 | 
				
			||||||
          margin: var(--ha-space-0);
 | 
					          margin: 0;
 | 
				
			||||||
          padding: var(--ha-space-0) var(--ha-space-4);
 | 
					          padding: 0 16px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        :host([expanded]) .title {
 | 
					        :host([expanded]) .title {
 | 
				
			||||||
          display: initial;
 | 
					          display: initial;
 | 
				
			||||||
@@ -776,16 +761,13 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
        ha-fade-in,
 | 
					        ha-fade-in,
 | 
				
			||||||
        ha-md-list {
 | 
					        ha-md-list {
 | 
				
			||||||
          height: calc(
 | 
					          height: calc(
 | 
				
			||||||
            100% - var(--header-height) - var(
 | 
					            100% - var(--header-height) - var(--safe-area-inset-top, 0px) -
 | 
				
			||||||
                --safe-area-inset-top,
 | 
					 | 
				
			||||||
                var(--ha-space-0)
 | 
					 | 
				
			||||||
              ) -
 | 
					 | 
				
			||||||
              132px
 | 
					              132px
 | 
				
			||||||
          );
 | 
					          );
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ha-fade-in {
 | 
					        ha-fade-in {
 | 
				
			||||||
          padding: var(--ha-space-1) var(--ha-space-0);
 | 
					          padding: 4px 0;
 | 
				
			||||||
          box-sizing: border-box;
 | 
					          box-sizing: border-box;
 | 
				
			||||||
          display: flex;
 | 
					          display: flex;
 | 
				
			||||||
          justify-content: center;
 | 
					          justify-content: center;
 | 
				
			||||||
@@ -795,29 +777,29 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
        ha-md-list {
 | 
					        ha-md-list {
 | 
				
			||||||
          overflow-x: hidden;
 | 
					          overflow-x: hidden;
 | 
				
			||||||
          background: none;
 | 
					          background: none;
 | 
				
			||||||
          margin-left: var(--safe-area-inset-left, var(--ha-space-0));
 | 
					          margin-left: var(--safe-area-inset-left, 0px);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ha-md-list-item {
 | 
					        ha-md-list-item {
 | 
				
			||||||
          flex-shrink: 0;
 | 
					          flex-shrink: 0;
 | 
				
			||||||
          box-sizing: border-box;
 | 
					          box-sizing: border-box;
 | 
				
			||||||
          margin: var(--ha-space-1);
 | 
					          margin: 4px;
 | 
				
			||||||
          border-radius: var(--ha-border-radius-sm);
 | 
					          border-radius: var(--ha-border-radius-sm);
 | 
				
			||||||
          --md-list-item-one-line-container-height: var(--ha-space-10);
 | 
					          --md-list-item-one-line-container-height: 40px;
 | 
				
			||||||
          --md-list-item-top-space: 0;
 | 
					          --md-list-item-top-space: 0;
 | 
				
			||||||
          --md-list-item-bottom-space: 0;
 | 
					          --md-list-item-bottom-space: 0;
 | 
				
			||||||
          width: var(--ha-space-12);
 | 
					          width: 48px;
 | 
				
			||||||
          position: relative;
 | 
					          position: relative;
 | 
				
			||||||
          --md-list-item-label-text-color: var(--sidebar-text-color);
 | 
					          --md-list-item-label-text-color: var(--sidebar-text-color);
 | 
				
			||||||
          --md-list-item-leading-space: var(--ha-space-3);
 | 
					          --md-list-item-leading-space: 12px;
 | 
				
			||||||
          --md-list-item-trailing-space: var(--ha-space-3);
 | 
					          --md-list-item-trailing-space: 12px;
 | 
				
			||||||
          --md-list-item-leading-icon-size: var(--ha-space-6);
 | 
					          --md-list-item-leading-icon-size: 24px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        :host([expanded]) ha-md-list-item {
 | 
					        :host([expanded]) ha-md-list-item {
 | 
				
			||||||
          width: 248px;
 | 
					          width: 248px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
        :host([narrow][expanded]) ha-md-list-item {
 | 
					        :host([narrow][expanded]) ha-md-list-item {
 | 
				
			||||||
          width: calc(240px - var(--safe-area-inset-left, var(--ha-space-0)));
 | 
					          width: calc(240px - var(--safe-area-inset-left, 0px));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ha-md-list-item.selected {
 | 
					        ha-md-list-item.selected {
 | 
				
			||||||
@@ -841,7 +823,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        ha-icon[slot="start"],
 | 
					        ha-icon[slot="start"],
 | 
				
			||||||
        ha-svg-icon[slot="start"] {
 | 
					        ha-svg-icon[slot="start"] {
 | 
				
			||||||
          width: var(--ha-space-6);
 | 
					          width: 24px;
 | 
				
			||||||
          flex-shrink: 0;
 | 
					          flex-shrink: 0;
 | 
				
			||||||
          color: var(--sidebar-icon-color);
 | 
					          color: var(--sidebar-icon-color);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
@@ -874,7 +856,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          display: flex;
 | 
					          display: flex;
 | 
				
			||||||
          justify-content: center;
 | 
					          justify-content: center;
 | 
				
			||||||
          align-items: center;
 | 
					          align-items: center;
 | 
				
			||||||
          min-width: var(--ha-space-2);
 | 
					          min-width: 8px;
 | 
				
			||||||
          border-radius: var(--ha-border-radius-xl);
 | 
					          border-radius: var(--ha-border-radius-xl);
 | 
				
			||||||
          font-weight: var(--ha-font-weight-normal);
 | 
					          font-weight: var(--ha-font-weight-normal);
 | 
				
			||||||
          line-height: normal;
 | 
					          line-height: normal;
 | 
				
			||||||
@@ -885,26 +867,22 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
        ha-svg-icon + .badge {
 | 
					        ha-svg-icon + .badge {
 | 
				
			||||||
          position: absolute;
 | 
					          position: absolute;
 | 
				
			||||||
          top: var(--ha-space-1);
 | 
					          top: 4px;
 | 
				
			||||||
          left: 26px;
 | 
					          left: 26px;
 | 
				
			||||||
          border-radius: var(--ha-border-radius-md);
 | 
					          border-radius: var(--ha-border-radius-md);
 | 
				
			||||||
          font-size: 0.65em;
 | 
					          font-size: 0.65em;
 | 
				
			||||||
          line-height: var(--ha-line-height-expanded);
 | 
					          line-height: var(--ha-line-height-expanded);
 | 
				
			||||||
          padding: var(--ha-space-0) var(--ha-space-1);
 | 
					          padding: 0 4px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ha-md-list-item.user {
 | 
					        ha-md-list-item.user {
 | 
				
			||||||
          --md-list-item-leading-icon-size: var(--ha-space-10);
 | 
					          --md-list-item-leading-icon-size: 40px;
 | 
				
			||||||
          --md-list-item-leading-space: var(--ha-space-1);
 | 
					          --md-list-item-leading-space: 4px;
 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        ha-md-list-item.user.rtl {
 | 
					 | 
				
			||||||
          --md-list-item-leading-space: var(--ha-space-3);
 | 
					 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        ha-user-badge {
 | 
					        ha-user-badge {
 | 
				
			||||||
          flex-shrink: 0;
 | 
					          flex-shrink: 0;
 | 
				
			||||||
          margin-right: calc(var(--ha-space-2) * -1);
 | 
					          margin-right: -8px;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        .spacer {
 | 
					        .spacer {
 | 
				
			||||||
@@ -916,7 +894,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          color: var(--sidebar-text-color);
 | 
					          color: var(--sidebar-text-color);
 | 
				
			||||||
          font-size: var(--ha-font-size-m);
 | 
					          font-size: var(--ha-font-size-m);
 | 
				
			||||||
          font-weight: var(--ha-font-weight-medium);
 | 
					          font-weight: var(--ha-font-weight-medium);
 | 
				
			||||||
          padding: var(--ha-space-4);
 | 
					          padding: 16px;
 | 
				
			||||||
          white-space: nowrap;
 | 
					          white-space: nowrap;
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -928,7 +906,7 @@ class HaSidebar extends SubscribeMixin(LitElement) {
 | 
				
			|||||||
          white-space: nowrap;
 | 
					          white-space: nowrap;
 | 
				
			||||||
          color: var(--sidebar-background-color);
 | 
					          color: var(--sidebar-background-color);
 | 
				
			||||||
          background-color: var(--sidebar-text-color);
 | 
					          background-color: var(--sidebar-text-color);
 | 
				
			||||||
          padding: var(--ha-space-1);
 | 
					          padding: 4px;
 | 
				
			||||||
          font-weight: var(--ha-font-weight-medium);
 | 
					          font-weight: var(--ha-font-weight-medium);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -59,33 +59,12 @@ export class HaSlider extends Slider {
 | 
				
			|||||||
          background-color: var(--ha-slider-thumb-color, var(--primary-color));
 | 
					          background-color: var(--ha-slider-thumb-color, var(--primary-color));
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        #thumb:after {
 | 
					 | 
				
			||||||
          content: "";
 | 
					 | 
				
			||||||
          border-radius: 50%;
 | 
					 | 
				
			||||||
          position: absolute;
 | 
					 | 
				
			||||||
          width: calc(var(--thumb-width) * 2 + 8px);
 | 
					 | 
				
			||||||
          height: calc(var(--thumb-height) * 2 + 8px);
 | 
					 | 
				
			||||||
          left: calc(-50% - 4px);
 | 
					 | 
				
			||||||
          top: calc(-50% - 4px);
 | 
					 | 
				
			||||||
          cursor: pointer;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #slider:focus-visible:not(.disabled) #thumb,
 | 
					        #slider:focus-visible:not(.disabled) #thumb,
 | 
				
			||||||
        #slider:focus-visible:not(.disabled) #thumb-min,
 | 
					        #slider:focus-visible:not(.disabled) #thumb-min,
 | 
				
			||||||
        #slider:focus-visible:not(.disabled) #thumb-max {
 | 
					        #slider:focus-visible:not(.disabled) #thumb-max {
 | 
				
			||||||
          outline: var(--wa-focus-ring);
 | 
					          outline: var(--wa-focus-ring);
 | 
				
			||||||
        }
 | 
					        }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
        #track:after {
 | 
					 | 
				
			||||||
          content: "";
 | 
					 | 
				
			||||||
          position: absolute;
 | 
					 | 
				
			||||||
          top: calc(-50% - 4px);
 | 
					 | 
				
			||||||
          left: 0;
 | 
					 | 
				
			||||||
          width: 100%;
 | 
					 | 
				
			||||||
          height: calc(var(--track-size) * 2 + 8px);
 | 
					 | 
				
			||||||
          cursor: pointer;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        #indicator {
 | 
					        #indicator {
 | 
				
			||||||
          background-color: var(
 | 
					          background-color: var(
 | 
				
			||||||
            --ha-slider-indicator-color,
 | 
					            --ha-slider-indicator-color,
 | 
				
			||||||
 
 | 
				
			|||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										97
									
								
								src/components/ha-trigger-icon.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								src/components/ha-trigger-icon.ts
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
				
			|||||||
 | 
					import {
 | 
				
			||||||
 | 
					  mdiAvTimer,
 | 
				
			||||||
 | 
					  mdiCalendar,
 | 
				
			||||||
 | 
					  mdiClockOutline,
 | 
				
			||||||
 | 
					  mdiCodeBraces,
 | 
				
			||||||
 | 
					  mdiDevices,
 | 
				
			||||||
 | 
					  mdiFormatListBulleted,
 | 
				
			||||||
 | 
					  mdiGestureDoubleTap,
 | 
				
			||||||
 | 
					  mdiHomeAssistant,
 | 
				
			||||||
 | 
					  mdiMapMarker,
 | 
				
			||||||
 | 
					  mdiMapMarkerRadius,
 | 
				
			||||||
 | 
					  mdiMessageAlert,
 | 
				
			||||||
 | 
					  mdiMicrophoneMessage,
 | 
				
			||||||
 | 
					  mdiNfcVariant,
 | 
				
			||||||
 | 
					  mdiNumeric,
 | 
				
			||||||
 | 
					  mdiStateMachine,
 | 
				
			||||||
 | 
					  mdiSwapHorizontal,
 | 
				
			||||||
 | 
					  mdiWeatherSunny,
 | 
				
			||||||
 | 
					  mdiWebhook,
 | 
				
			||||||
 | 
					} from "@mdi/js";
 | 
				
			||||||
 | 
					import { html, LitElement, nothing } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property } from "lit/decorators";
 | 
				
			||||||
 | 
					import { until } from "lit/directives/until";
 | 
				
			||||||
 | 
					import { computeDomain } from "../common/entity/compute_domain";
 | 
				
			||||||
 | 
					import { FALLBACK_DOMAIN_ICONS, triggerIcon } from "../data/icons";
 | 
				
			||||||
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
 | 
					import "./ha-icon";
 | 
				
			||||||
 | 
					import "./ha-svg-icon";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const TRIGGER_ICONS = {
 | 
				
			||||||
 | 
					  calendar: mdiCalendar,
 | 
				
			||||||
 | 
					  device: mdiDevices,
 | 
				
			||||||
 | 
					  event: mdiGestureDoubleTap,
 | 
				
			||||||
 | 
					  state: mdiStateMachine,
 | 
				
			||||||
 | 
					  geo_location: mdiMapMarker,
 | 
				
			||||||
 | 
					  homeassistant: mdiHomeAssistant,
 | 
				
			||||||
 | 
					  mqtt: mdiSwapHorizontal,
 | 
				
			||||||
 | 
					  numeric_state: mdiNumeric,
 | 
				
			||||||
 | 
					  sun: mdiWeatherSunny,
 | 
				
			||||||
 | 
					  conversation: mdiMicrophoneMessage,
 | 
				
			||||||
 | 
					  tag: mdiNfcVariant,
 | 
				
			||||||
 | 
					  template: mdiCodeBraces,
 | 
				
			||||||
 | 
					  time: mdiClockOutline,
 | 
				
			||||||
 | 
					  time_pattern: mdiAvTimer,
 | 
				
			||||||
 | 
					  webhook: mdiWebhook,
 | 
				
			||||||
 | 
					  persistent_notification: mdiMessageAlert,
 | 
				
			||||||
 | 
					  zone: mdiMapMarkerRadius,
 | 
				
			||||||
 | 
					  list: mdiFormatListBulleted,
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					@customElement("ha-trigger-icon")
 | 
				
			||||||
 | 
					export class HaTriggerIcon extends LitElement {
 | 
				
			||||||
 | 
					  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public trigger?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  @property() public icon?: string;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  protected render() {
 | 
				
			||||||
 | 
					    if (this.icon) {
 | 
				
			||||||
 | 
					      return html`<ha-icon .icon=${this.icon}></ha-icon>`;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.trigger) {
 | 
				
			||||||
 | 
					      return nothing;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    if (!this.hass) {
 | 
				
			||||||
 | 
					      return this._renderFallback();
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    const icon = triggerIcon(this.hass, this.trigger).then((icn) => {
 | 
				
			||||||
 | 
					      if (icn) {
 | 
				
			||||||
 | 
					        return html`<ha-icon .icon=${icn}></ha-icon>`;
 | 
				
			||||||
 | 
					      }
 | 
				
			||||||
 | 
					      return this._renderFallback();
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html`${until(icon)}`;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  private _renderFallback() {
 | 
				
			||||||
 | 
					    const domain = computeDomain(this.trigger!);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					    return html`
 | 
				
			||||||
 | 
					      <ha-svg-icon
 | 
				
			||||||
 | 
					        .path=${TRIGGER_ICONS[this.trigger!] || FALLBACK_DOMAIN_ICONS[domain]}
 | 
				
			||||||
 | 
					      ></ha-svg-icon>
 | 
				
			||||||
 | 
					    `;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					declare global {
 | 
				
			||||||
 | 
					  interface HTMLElementTagNameMap {
 | 
				
			||||||
 | 
					    "ha-trigger-icon": HaTriggerIcon;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -1,18 +1,12 @@
 | 
				
			|||||||
 | 
					import { css, html, LitElement, nothing } from "lit";
 | 
				
			||||||
 | 
					import { customElement, property, state } from "lit/decorators";
 | 
				
			||||||
import "@home-assistant/webawesome/dist/components/dialog/dialog";
 | 
					import "@home-assistant/webawesome/dist/components/dialog/dialog";
 | 
				
			||||||
import { mdiClose } from "@mdi/js";
 | 
					import { mdiClose } from "@mdi/js";
 | 
				
			||||||
import { css, html, LitElement, nothing } from "lit";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  customElement,
 | 
					 | 
				
			||||||
  eventOptions,
 | 
					 | 
				
			||||||
  property,
 | 
					 | 
				
			||||||
  query,
 | 
					 | 
				
			||||||
  state,
 | 
					 | 
				
			||||||
} from "lit/decorators";
 | 
					 | 
				
			||||||
import { fireEvent } from "../common/dom/fire_event";
 | 
					 | 
				
			||||||
import { haStyleScrollbar } from "../resources/styles";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					 | 
				
			||||||
import "./ha-dialog-header";
 | 
					import "./ha-dialog-header";
 | 
				
			||||||
import "./ha-icon-button";
 | 
					import "./ha-icon-button";
 | 
				
			||||||
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
 | 
					import { fireEvent } from "../common/dom/fire_event";
 | 
				
			||||||
 | 
					import { haStyleScrollbar } from "../resources/styles";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type DialogWidth = "small" | "medium" | "large" | "full";
 | 
					export type DialogWidth = "small" | "medium" | "large" | "full";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -96,11 +90,6 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
  @state()
 | 
					  @state()
 | 
				
			||||||
  private _open = false;
 | 
					  private _open = false;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @query(".body") public bodyContainer!: HTMLDivElement;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state()
 | 
					 | 
				
			||||||
  private _bodyScrolled = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected updated(
 | 
					  protected updated(
 | 
				
			||||||
    changedProperties: Map<string | number | symbol, unknown>
 | 
					    changedProperties: Map<string | number | symbol, unknown>
 | 
				
			||||||
  ): void {
 | 
					  ): void {
 | 
				
			||||||
@@ -118,14 +107,10 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
        .lightDismiss=${!this.preventScrimClose}
 | 
					        .lightDismiss=${!this.preventScrimClose}
 | 
				
			||||||
        without-header
 | 
					        without-header
 | 
				
			||||||
        @wa-show=${this._handleShow}
 | 
					        @wa-show=${this._handleShow}
 | 
				
			||||||
        @wa-after-show=${this._handleAfterShow}
 | 
					 | 
				
			||||||
        @wa-after-hide=${this._handleAfterHide}
 | 
					        @wa-after-hide=${this._handleAfterHide}
 | 
				
			||||||
      >
 | 
					      >
 | 
				
			||||||
        <slot name="header">
 | 
					        <slot name="header">
 | 
				
			||||||
          <ha-dialog-header
 | 
					          <ha-dialog-header .subtitlePosition=${this.headerSubtitlePosition}>
 | 
				
			||||||
            .subtitlePosition=${this.headerSubtitlePosition}
 | 
					 | 
				
			||||||
            .showBorder=${this._bodyScrolled}
 | 
					 | 
				
			||||||
          >
 | 
					 | 
				
			||||||
            <slot name="headerNavigationIcon" slot="navigationIcon">
 | 
					            <slot name="headerNavigationIcon" slot="navigationIcon">
 | 
				
			||||||
              <ha-icon-button
 | 
					              <ha-icon-button
 | 
				
			||||||
                data-dialog="close"
 | 
					                data-dialog="close"
 | 
				
			||||||
@@ -144,7 +129,7 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
            <slot name="headerActionItems" slot="actionItems"></slot>
 | 
					            <slot name="headerActionItems" slot="actionItems"></slot>
 | 
				
			||||||
          </ha-dialog-header>
 | 
					          </ha-dialog-header>
 | 
				
			||||||
        </slot>
 | 
					        </slot>
 | 
				
			||||||
        <div class="body ha-scrollbar" @scroll=${this._handleBodyScroll}>
 | 
					        <div class="body ha-scrollbar">
 | 
				
			||||||
          <slot></slot>
 | 
					          <slot></slot>
 | 
				
			||||||
        </div>
 | 
					        </div>
 | 
				
			||||||
        <slot name="footer" slot="footer"></slot>
 | 
					        <slot name="footer" slot="footer"></slot>
 | 
				
			||||||
@@ -161,10 +146,6 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
    (this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
 | 
					    (this.querySelector("[autofocus]") as HTMLElement | null)?.focus();
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  private _handleAfterShow = () => {
 | 
					 | 
				
			||||||
    fireEvent(this, "after-show");
 | 
					 | 
				
			||||||
  };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleAfterHide = () => {
 | 
					  private _handleAfterHide = () => {
 | 
				
			||||||
    this._open = false;
 | 
					    this._open = false;
 | 
				
			||||||
    fireEvent(this, "closed");
 | 
					    fireEvent(this, "closed");
 | 
				
			||||||
@@ -175,11 +156,6 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
    this._open = false;
 | 
					    this._open = false;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  @eventOptions({ passive: true })
 | 
					 | 
				
			||||||
  private _handleBodyScroll(ev: Event) {
 | 
					 | 
				
			||||||
    this._bodyScrolled = (ev.target as HTMLDivElement).scrollTop > 0;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static styles = [
 | 
					  static styles = [
 | 
				
			||||||
    haStyleScrollbar,
 | 
					    haStyleScrollbar,
 | 
				
			||||||
    css`
 | 
					    css`
 | 
				
			||||||
@@ -196,7 +172,7 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
            )
 | 
					            )
 | 
				
			||||||
          )
 | 
					          )
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        --width: min(var(--ha-dialog-width-md, 580px), var(--full-width));
 | 
					        --width: var(--ha-dialog-width-md, min(580px, var(--full-width)));
 | 
				
			||||||
        --spacing: var(--dialog-content-padding, var(--ha-space-6));
 | 
					        --spacing: var(--dialog-content-padding, var(--ha-space-6));
 | 
				
			||||||
        --show-duration: var(--ha-dialog-show-duration, 200ms);
 | 
					        --show-duration: var(--ha-dialog-show-duration, 200ms);
 | 
				
			||||||
        --hide-duration: var(--ha-dialog-hide-duration, 200ms);
 | 
					        --hide-duration: var(--ha-dialog-hide-duration, 200ms);
 | 
				
			||||||
@@ -217,11 +193,11 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      :host([width="small"]) wa-dialog {
 | 
					      :host([width="small"]) wa-dialog {
 | 
				
			||||||
        --width: min(var(--ha-dialog-width-sm, 320px), var(--full-width));
 | 
					        --width: var(--ha-dialog-width-sm, min(320px, var(--full-width)));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      :host([width="large"]) wa-dialog {
 | 
					      :host([width="large"]) wa-dialog {
 | 
				
			||||||
        --width: min(var(--ha-dialog-width-lg, 720px), var(--full-width));
 | 
					        --width: var(--ha-dialog-width-lg, min(720px, var(--full-width)));
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
      :host([width="full"]) wa-dialog {
 | 
					      :host([width="full"]) wa-dialog {
 | 
				
			||||||
@@ -235,7 +211,6 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
          --ha-dialog-max-height,
 | 
					          --ha-dialog-max-height,
 | 
				
			||||||
          calc(100% - var(--ha-space-20))
 | 
					          calc(100% - var(--ha-space-20))
 | 
				
			||||||
        );
 | 
					        );
 | 
				
			||||||
        min-height: var(--ha-dialog-min-height);
 | 
					 | 
				
			||||||
        position: var(--dialog-surface-position, relative);
 | 
					        position: var(--dialog-surface-position, relative);
 | 
				
			||||||
        margin-top: var(--dialog-surface-margin-top, auto);
 | 
					        margin-top: var(--dialog-surface-margin-top, auto);
 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
@@ -272,7 +247,10 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
      .header-title {
 | 
					      .header-title {
 | 
				
			||||||
        margin: 0;
 | 
					        margin: 0;
 | 
				
			||||||
        margin-bottom: 0;
 | 
					        margin-bottom: 0;
 | 
				
			||||||
        color: var(--ha-dialog-header-title-color, var(--primary-text-color));
 | 
					        color: var(
 | 
				
			||||||
 | 
					          --ha-dialog-header-title-color,
 | 
				
			||||||
 | 
					          var(--ha-color-on-surface-default, var(--primary-text-color))
 | 
				
			||||||
 | 
					        );
 | 
				
			||||||
        font-size: var(
 | 
					        font-size: var(
 | 
				
			||||||
          --ha-dialog-header-title-font-size,
 | 
					          --ha-dialog-header-title-font-size,
 | 
				
			||||||
          var(--ha-font-size-2xl)
 | 
					          var(--ha-font-size-2xl)
 | 
				
			||||||
@@ -309,7 +287,6 @@ export class HaWaDialog extends LitElement {
 | 
				
			|||||||
      }
 | 
					      }
 | 
				
			||||||
      :host([flexcontent]) .body {
 | 
					      :host([flexcontent]) .body {
 | 
				
			||||||
        max-width: 100%;
 | 
					        max-width: 100%;
 | 
				
			||||||
        flex: 1;
 | 
					 | 
				
			||||||
        display: flex;
 | 
					        display: flex;
 | 
				
			||||||
        flex-direction: column;
 | 
					        flex-direction: column;
 | 
				
			||||||
      }
 | 
					      }
 | 
				
			||||||
@@ -338,7 +315,6 @@ declare global {
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
  interface HASSDomEvents {
 | 
					  interface HASSDomEvents {
 | 
				
			||||||
    opened: undefined;
 | 
					    opened: undefined;
 | 
				
			||||||
    "after-show": undefined;
 | 
					 | 
				
			||||||
    closed: undefined;
 | 
					    closed: undefined;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -321,10 +321,6 @@ class HaWebRtcPlayer extends LitElement {
 | 
				
			|||||||
    if (!this._remoteStream) {
 | 
					    if (!this._remoteStream) {
 | 
				
			||||||
      return;
 | 
					      return;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    // If the track is audio and the player is muted, we do not add it to the stream.
 | 
					 | 
				
			||||||
    if (event.track.kind === "audio" && this.muted) {
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    this._remoteStream.addTrack(event.track);
 | 
					    this._remoteStream.addTrack(event.track);
 | 
				
			||||||
    if (!this.hasUpdated) {
 | 
					    if (!this.hasUpdated) {
 | 
				
			||||||
      await this.updateComplete;
 | 
					      await this.updateComplete;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -15,7 +15,6 @@ import { classMap } from "lit/directives/class-map";
 | 
				
			|||||||
import { styleMap } from "lit/directives/style-map";
 | 
					import { styleMap } from "lit/directives/style-map";
 | 
				
			||||||
import { until } from "lit/directives/until";
 | 
					import { until } from "lit/directives/until";
 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					import { fireEvent } from "../../common/dom/fire_event";
 | 
				
			||||||
import { slugify } from "../../common/string/slugify";
 | 
					 | 
				
			||||||
import { debounce } from "../../common/util/debounce";
 | 
					import { debounce } from "../../common/util/debounce";
 | 
				
			||||||
import { isUnavailableState } from "../../data/entity";
 | 
					import { isUnavailableState } from "../../data/entity";
 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
@@ -694,12 +693,10 @@ export class HaMediaPlayerBrowse extends LitElement {
 | 
				
			|||||||
                `
 | 
					                `
 | 
				
			||||||
              : ""}
 | 
					              : ""}
 | 
				
			||||||
          </div>
 | 
					          </div>
 | 
				
			||||||
          <ha-tooltip .for="grid-${slugify(child.title)}" distance="-4">
 | 
					          <ha-tooltip .for="grid-${child.title}" distance="-4">
 | 
				
			||||||
            ${child.title}
 | 
					            ${child.title}
 | 
				
			||||||
          </ha-tooltip>
 | 
					          </ha-tooltip>
 | 
				
			||||||
          <div .id="grid-${slugify(child.title)}" class="title">
 | 
					          <div .id="grid-${child.title}" class="title">${child.title}</div>
 | 
				
			||||||
            ${child.title}
 | 
					 | 
				
			||||||
          </div>
 | 
					 | 
				
			||||||
        </ha-card>
 | 
					        </ha-card>
 | 
				
			||||||
      </div>
 | 
					      </div>
 | 
				
			||||||
    `;
 | 
					    `;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,76 +0,0 @@
 | 
				
			|||||||
import { html, LitElement, nothing } from "lit";
 | 
					 | 
				
			||||||
import { customElement, property, state } from "lit/decorators";
 | 
					 | 
				
			||||||
import { fireEvent } from "../../../common/dom/fire_event";
 | 
					 | 
				
			||||||
import type { HassDialog } from "../../../dialogs/make-dialog-manager";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../../../types";
 | 
					 | 
				
			||||||
import "../../ha-dialog-header";
 | 
					 | 
				
			||||||
import "../../ha-icon-button";
 | 
					 | 
				
			||||||
import "../../ha-icon-next";
 | 
					 | 
				
			||||||
import "../../ha-md-list";
 | 
					 | 
				
			||||||
import "../../ha-md-list-item";
 | 
					 | 
				
			||||||
import "../../ha-svg-icon";
 | 
					 | 
				
			||||||
import "../../ha-wa-dialog";
 | 
					 | 
				
			||||||
import "../ha-target-picker-item-row";
 | 
					 | 
				
			||||||
import type { TargetDetailsDialogParams } from "./show-dialog-target-details";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ha-dialog-target-details")
 | 
					 | 
				
			||||||
class DialogTargetDetails extends LitElement implements HassDialog {
 | 
					 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _params?: TargetDetailsDialogParams;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _opened = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public showDialog(params: TargetDetailsDialogParams): void {
 | 
					 | 
				
			||||||
    this._params = params;
 | 
					 | 
				
			||||||
    this._opened = true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  public closeDialog() {
 | 
					 | 
				
			||||||
    this._opened = false;
 | 
					 | 
				
			||||||
    return true;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _dialogClosed() {
 | 
					 | 
				
			||||||
    fireEvent(this, "dialog-closed", { dialog: this.localName });
 | 
					 | 
				
			||||||
    this._params = undefined;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render() {
 | 
					 | 
				
			||||||
    if (!this._params) {
 | 
					 | 
				
			||||||
      return nothing;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html`
 | 
					 | 
				
			||||||
      <ha-wa-dialog
 | 
					 | 
				
			||||||
        .hass=${this.hass}
 | 
					 | 
				
			||||||
        .open=${this._opened}
 | 
					 | 
				
			||||||
        header-title=${this.hass.localize(
 | 
					 | 
				
			||||||
          "ui.components.target-picker.target_details"
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
        header-subtitle=${`${this.hass.localize(
 | 
					 | 
				
			||||||
          `ui.components.target-picker.type.${this._params.type}`
 | 
					 | 
				
			||||||
        )}:
 | 
					 | 
				
			||||||
            ${this._params.title}`}
 | 
					 | 
				
			||||||
        @closed=${this._dialogClosed}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        <ha-target-picker-item-row
 | 
					 | 
				
			||||||
          .hass=${this.hass}
 | 
					 | 
				
			||||||
          .type=${this._params.type}
 | 
					 | 
				
			||||||
          .itemId=${this._params.itemId}
 | 
					 | 
				
			||||||
          .deviceFilter=${this._params.deviceFilter}
 | 
					 | 
				
			||||||
          .entityFilter=${this._params.entityFilter}
 | 
					 | 
				
			||||||
          .includeDomains=${this._params.includeDomains}
 | 
					 | 
				
			||||||
          .includeDeviceClasses=${this._params.includeDeviceClasses}
 | 
					 | 
				
			||||||
          expand
 | 
					 | 
				
			||||||
        ></ha-target-picker-item-row>
 | 
					 | 
				
			||||||
      </ha-wa-dialog>
 | 
					 | 
				
			||||||
    `;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare global {
 | 
					 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					 | 
				
			||||||
    "ha-dialog-target-details": DialogTargetDetails;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,28 +0,0 @@
 | 
				
			|||||||
import { fireEvent } from "../../../common/dom/fire_event";
 | 
					 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity";
 | 
					 | 
				
			||||||
import type { TargetType } from "../../../data/target";
 | 
					 | 
				
			||||||
import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type NewBackupType = "automatic" | "manual";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface TargetDetailsDialogParams {
 | 
					 | 
				
			||||||
  title: string;
 | 
					 | 
				
			||||||
  type: TargetType;
 | 
					 | 
				
			||||||
  itemId: string;
 | 
					 | 
				
			||||||
  deviceFilter?: HaDevicePickerDeviceFilterFunc;
 | 
					 | 
				
			||||||
  entityFilter?: HaEntityPickerEntityFilterFunc;
 | 
					 | 
				
			||||||
  includeDomains?: string[];
 | 
					 | 
				
			||||||
  includeDeviceClasses?: string[];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const loadTargetDetailsDialog = () => import("./dialog-target-details");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const showTargetDetailsDialog = (
 | 
					 | 
				
			||||||
  element: HTMLElement,
 | 
					 | 
				
			||||||
  params: TargetDetailsDialogParams
 | 
					 | 
				
			||||||
) =>
 | 
					 | 
				
			||||||
  fireEvent(element, "show-dialog", {
 | 
					 | 
				
			||||||
    dialogTag: "ha-dialog-target-details",
 | 
					 | 
				
			||||||
    dialogImport: loadTargetDetailsDialog,
 | 
					 | 
				
			||||||
    dialogParams: params,
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
@@ -1,113 +0,0 @@
 | 
				
			|||||||
import { css, html, LitElement, nothing } from "lit";
 | 
					 | 
				
			||||||
import { customElement, property } from "lit/decorators";
 | 
					 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
 | 
					 | 
				
			||||||
import type { TargetType, TargetTypeFloorless } from "../../data/target";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					 | 
				
			||||||
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
 | 
					 | 
				
			||||||
import "../ha-expansion-panel";
 | 
					 | 
				
			||||||
import "../ha-md-list";
 | 
					 | 
				
			||||||
import "./ha-target-picker-item-row";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ha-target-picker-item-group")
 | 
					 | 
				
			||||||
export class HaTargetPickerItemGroup extends LitElement {
 | 
					 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property() public type!: TargetTypeFloorless;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ attribute: false }) public items!: Partial<
 | 
					 | 
				
			||||||
    Record<TargetType, string[]>
 | 
					 | 
				
			||||||
  >;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ type: Boolean, reflect: true }) public collapsed = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ attribute: false })
 | 
					 | 
				
			||||||
  public deviceFilter?: HaDevicePickerDeviceFilterFunc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ attribute: false })
 | 
					 | 
				
			||||||
  public entityFilter?: HaEntityPickerEntityFilterFunc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Show only targets with entities from specific domains.
 | 
					 | 
				
			||||||
   * @type {Array}
 | 
					 | 
				
			||||||
   * @attr include-domains
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  @property({ type: Array, attribute: "include-domains" })
 | 
					 | 
				
			||||||
  public includeDomains?: string[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Show only targets with entities of these device classes.
 | 
					 | 
				
			||||||
   * @type {Array}
 | 
					 | 
				
			||||||
   * @attr include-device-classes
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  @property({ type: Array, attribute: "include-device-classes" })
 | 
					 | 
				
			||||||
  public includeDeviceClasses?: string[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render() {
 | 
					 | 
				
			||||||
    let count = 0;
 | 
					 | 
				
			||||||
    Object.values(this.items).forEach((items) => {
 | 
					 | 
				
			||||||
      if (items) {
 | 
					 | 
				
			||||||
        count += items.length;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html`<ha-expansion-panel
 | 
					 | 
				
			||||||
      .expanded=${!this.collapsed}
 | 
					 | 
				
			||||||
      left-chevron
 | 
					 | 
				
			||||||
      @expanded-changed=${this._expandedChanged}
 | 
					 | 
				
			||||||
    >
 | 
					 | 
				
			||||||
      <div slot="header" class="heading">
 | 
					 | 
				
			||||||
        ${this.hass.localize(
 | 
					 | 
				
			||||||
          `ui.components.target-picker.selected.${this.type}`,
 | 
					 | 
				
			||||||
          {
 | 
					 | 
				
			||||||
            count,
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        )}
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
      ${Object.entries(this.items).map(([type, items]) =>
 | 
					 | 
				
			||||||
        items
 | 
					 | 
				
			||||||
          ? items.map(
 | 
					 | 
				
			||||||
              (item) =>
 | 
					 | 
				
			||||||
                html`<ha-target-picker-item-row
 | 
					 | 
				
			||||||
                  .hass=${this.hass}
 | 
					 | 
				
			||||||
                  .type=${type as TargetTypeFloorless}
 | 
					 | 
				
			||||||
                  .itemId=${item}
 | 
					 | 
				
			||||||
                  .deviceFilter=${this.deviceFilter}
 | 
					 | 
				
			||||||
                  .entityFilter=${this.entityFilter}
 | 
					 | 
				
			||||||
                  .includeDomains=${this.includeDomains}
 | 
					 | 
				
			||||||
                  .includeDeviceClasses=${this.includeDeviceClasses}
 | 
					 | 
				
			||||||
                ></ha-target-picker-item-row>`
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          : nothing
 | 
					 | 
				
			||||||
      )}
 | 
					 | 
				
			||||||
    </ha-expansion-panel>`;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _expandedChanged(ev: CustomEvent) {
 | 
					 | 
				
			||||||
    this.collapsed = !ev.detail.expanded;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static styles = css`
 | 
					 | 
				
			||||||
    :host {
 | 
					 | 
				
			||||||
      display: block;
 | 
					 | 
				
			||||||
      --expansion-panel-content-padding: var(--ha-space-0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    ha-expansion-panel::part(summary) {
 | 
					 | 
				
			||||||
      background-color: var(--ha-color-fill-neutral-quiet-resting);
 | 
					 | 
				
			||||||
      padding: var(--ha-space-1) var(--ha-space-2);
 | 
					 | 
				
			||||||
      font-weight: var(--ha-font-weight-bold);
 | 
					 | 
				
			||||||
      color: var(--secondary-text-color);
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      justify-content: space-between;
 | 
					 | 
				
			||||||
      min-height: unset;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    ha-md-list {
 | 
					 | 
				
			||||||
      padding: var(--ha-space-0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  `;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare global {
 | 
					 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					 | 
				
			||||||
    "ha-target-picker-item-group": HaTargetPickerItemGroup;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -1,694 +0,0 @@
 | 
				
			|||||||
import { consume } from "@lit/context";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  mdiClose,
 | 
					 | 
				
			||||||
  mdiDevices,
 | 
					 | 
				
			||||||
  mdiHome,
 | 
					 | 
				
			||||||
  mdiLabel,
 | 
					 | 
				
			||||||
  mdiTextureBox,
 | 
					 | 
				
			||||||
} from "@mdi/js";
 | 
					 | 
				
			||||||
import { css, html, LitElement, nothing, type PropertyValues } from "lit";
 | 
					 | 
				
			||||||
import { customElement, property, query, state } from "lit/decorators";
 | 
					 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					 | 
				
			||||||
import { computeAreaName } from "../../common/entity/compute_area_name";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  computeDeviceName,
 | 
					 | 
				
			||||||
  computeDeviceNameDisplay,
 | 
					 | 
				
			||||||
} from "../../common/entity/compute_device_name";
 | 
					 | 
				
			||||||
import { computeDomain } from "../../common/entity/compute_domain";
 | 
					 | 
				
			||||||
import { computeEntityName } from "../../common/entity/compute_entity_name";
 | 
					 | 
				
			||||||
import { getEntityContext } from "../../common/entity/context/get_entity_context";
 | 
					 | 
				
			||||||
import { computeRTL } from "../../common/util/compute_rtl";
 | 
					 | 
				
			||||||
import { getConfigEntry } from "../../data/config_entries";
 | 
					 | 
				
			||||||
import { labelsContext } from "../../data/context";
 | 
					 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "../../data/entity";
 | 
					 | 
				
			||||||
import { domainToName } from "../../data/integration";
 | 
					 | 
				
			||||||
import type { LabelRegistryEntry } from "../../data/label_registry";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  areaMeetsFilter,
 | 
					 | 
				
			||||||
  deviceMeetsFilter,
 | 
					 | 
				
			||||||
  entityRegMeetsFilter,
 | 
					 | 
				
			||||||
  extractFromTarget,
 | 
					 | 
				
			||||||
  type ExtractFromTargetResult,
 | 
					 | 
				
			||||||
  type ExtractFromTargetResultReferenced,
 | 
					 | 
				
			||||||
  type TargetType,
 | 
					 | 
				
			||||||
} from "../../data/target";
 | 
					 | 
				
			||||||
import { buttonLinkStyle } from "../../resources/styles";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					 | 
				
			||||||
import { brandsUrl } from "../../util/brands-url";
 | 
					 | 
				
			||||||
import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker";
 | 
					 | 
				
			||||||
import { floorDefaultIconPath } from "../ha-floor-icon";
 | 
					 | 
				
			||||||
import "../ha-icon-button";
 | 
					 | 
				
			||||||
import "../ha-md-list";
 | 
					 | 
				
			||||||
import type { HaMdList } from "../ha-md-list";
 | 
					 | 
				
			||||||
import "../ha-md-list-item";
 | 
					 | 
				
			||||||
import type { HaMdListItem } from "../ha-md-list-item";
 | 
					 | 
				
			||||||
import "../ha-state-icon";
 | 
					 | 
				
			||||||
import "../ha-svg-icon";
 | 
					 | 
				
			||||||
import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ha-target-picker-item-row")
 | 
					 | 
				
			||||||
export class HaTargetPickerItemRow extends LitElement {
 | 
					 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ reflect: true }) public type!: TargetType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ attribute: "item-id" }) public itemId!: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ type: Boolean }) public expand = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ type: Boolean, attribute: "sub-entry", reflect: true })
 | 
					 | 
				
			||||||
  public subEntry = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ type: Boolean, attribute: "hide-context" })
 | 
					 | 
				
			||||||
  public hideContext = false;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ attribute: false })
 | 
					 | 
				
			||||||
  public parentEntries?: ExtractFromTargetResultReferenced;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ attribute: false })
 | 
					 | 
				
			||||||
  public deviceFilter?: HaDevicePickerDeviceFilterFunc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ attribute: false })
 | 
					 | 
				
			||||||
  public entityFilter?: HaEntityPickerEntityFilterFunc;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Show only targets with entities from specific domains.
 | 
					 | 
				
			||||||
   * @type {Array}
 | 
					 | 
				
			||||||
   * @attr include-domains
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  @property({ type: Array, attribute: "include-domains" })
 | 
					 | 
				
			||||||
  public includeDomains?: string[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  /**
 | 
					 | 
				
			||||||
   * Show only targets with entities of these device classes.
 | 
					 | 
				
			||||||
   * @type {Array}
 | 
					 | 
				
			||||||
   * @attr include-device-classes
 | 
					 | 
				
			||||||
   */
 | 
					 | 
				
			||||||
  @property({ type: Array, attribute: "include-device-classes" })
 | 
					 | 
				
			||||||
  public includeDeviceClasses?: string[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _iconImg?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _domainName?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _entries?: ExtractFromTargetResult;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state()
 | 
					 | 
				
			||||||
  @consume({ context: labelsContext, subscribe: true })
 | 
					 | 
				
			||||||
  _labelRegistry!: LabelRegistryEntry[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @query("ha-md-list-item") public item?: HaMdListItem;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @query("ha-md-list") public list?: HaMdList;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @query("ha-target-picker-item-row") public itemRow?: HaTargetPickerItemRow;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected willUpdate(changedProps: PropertyValues) {
 | 
					 | 
				
			||||||
    if (!this.subEntry && changedProps.has("itemId")) {
 | 
					 | 
				
			||||||
      this._updateItemData();
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render() {
 | 
					 | 
				
			||||||
    const { name, context, iconPath, fallbackIconPath, stateObject } =
 | 
					 | 
				
			||||||
      this._itemData(this.type, this.itemId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const showEntities = this.type !== "entity";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const entries = this.parentEntries || this._entries;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // Don't show sub entries that have no entities
 | 
					 | 
				
			||||||
    if (
 | 
					 | 
				
			||||||
      this.subEntry &&
 | 
					 | 
				
			||||||
      this.type !== "entity" &&
 | 
					 | 
				
			||||||
      (!entries || entries.referenced_entities.length === 0)
 | 
					 | 
				
			||||||
    ) {
 | 
					 | 
				
			||||||
      return nothing;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html`
 | 
					 | 
				
			||||||
      <ha-md-list-item type="text">
 | 
					 | 
				
			||||||
        <div class="icon" slot="start">
 | 
					 | 
				
			||||||
          ${this.subEntry
 | 
					 | 
				
			||||||
            ? html`
 | 
					 | 
				
			||||||
                <div class="horizontal-line-wrapper">
 | 
					 | 
				
			||||||
                  <div class="horizontal-line"></div>
 | 
					 | 
				
			||||||
                </div>
 | 
					 | 
				
			||||||
              `
 | 
					 | 
				
			||||||
            : nothing}
 | 
					 | 
				
			||||||
          ${iconPath
 | 
					 | 
				
			||||||
            ? html`<ha-icon .icon=${iconPath}></ha-icon>`
 | 
					 | 
				
			||||||
            : this._iconImg
 | 
					 | 
				
			||||||
              ? html`<img
 | 
					 | 
				
			||||||
                  alt=${this._domainName || ""}
 | 
					 | 
				
			||||||
                  crossorigin="anonymous"
 | 
					 | 
				
			||||||
                  referrerpolicy="no-referrer"
 | 
					 | 
				
			||||||
                  src=${this._iconImg}
 | 
					 | 
				
			||||||
                />`
 | 
					 | 
				
			||||||
              : fallbackIconPath
 | 
					 | 
				
			||||||
                ? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>`
 | 
					 | 
				
			||||||
                : stateObject
 | 
					 | 
				
			||||||
                  ? html`
 | 
					 | 
				
			||||||
                      <ha-state-icon
 | 
					 | 
				
			||||||
                        .hass=${this.hass}
 | 
					 | 
				
			||||||
                        .stateObj=${stateObject}
 | 
					 | 
				
			||||||
                      >
 | 
					 | 
				
			||||||
                      </ha-state-icon>
 | 
					 | 
				
			||||||
                    `
 | 
					 | 
				
			||||||
                  : nothing}
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
        <div slot="headline">${name}</div>
 | 
					 | 
				
			||||||
        ${context && !this.hideContext
 | 
					 | 
				
			||||||
          ? html`<span slot="supporting-text">${context}</span>`
 | 
					 | 
				
			||||||
          : nothing}
 | 
					 | 
				
			||||||
        ${this._domainName && this.subEntry
 | 
					 | 
				
			||||||
          ? html`<span slot="supporting-text" class="domain"
 | 
					 | 
				
			||||||
              >${this._domainName}</span
 | 
					 | 
				
			||||||
            >`
 | 
					 | 
				
			||||||
          : nothing}
 | 
					 | 
				
			||||||
        ${!this.subEntry && entries && showEntities
 | 
					 | 
				
			||||||
          ? html`
 | 
					 | 
				
			||||||
              <div slot="end" class="summary">
 | 
					 | 
				
			||||||
                ${showEntities &&
 | 
					 | 
				
			||||||
                !this.expand &&
 | 
					 | 
				
			||||||
                entries?.referenced_entities.length
 | 
					 | 
				
			||||||
                  ? html`<button class="main link" @click=${this._openDetails}>
 | 
					 | 
				
			||||||
                      ${this.hass.localize(
 | 
					 | 
				
			||||||
                        "ui.components.target-picker.entities_count",
 | 
					 | 
				
			||||||
                        {
 | 
					 | 
				
			||||||
                          count: entries?.referenced_entities.length,
 | 
					 | 
				
			||||||
                        }
 | 
					 | 
				
			||||||
                      )}
 | 
					 | 
				
			||||||
                    </button>`
 | 
					 | 
				
			||||||
                  : showEntities
 | 
					 | 
				
			||||||
                    ? html`<span class="main">
 | 
					 | 
				
			||||||
                        ${this.hass.localize(
 | 
					 | 
				
			||||||
                          "ui.components.target-picker.entities_count",
 | 
					 | 
				
			||||||
                          {
 | 
					 | 
				
			||||||
                            count: entries?.referenced_entities.length,
 | 
					 | 
				
			||||||
                          }
 | 
					 | 
				
			||||||
                        )}
 | 
					 | 
				
			||||||
                      </span>`
 | 
					 | 
				
			||||||
                    : nothing}
 | 
					 | 
				
			||||||
              </div>
 | 
					 | 
				
			||||||
            `
 | 
					 | 
				
			||||||
          : nothing}
 | 
					 | 
				
			||||||
        ${!this.expand && !this.subEntry
 | 
					 | 
				
			||||||
          ? html`
 | 
					 | 
				
			||||||
              <ha-icon-button
 | 
					 | 
				
			||||||
                .path=${mdiClose}
 | 
					 | 
				
			||||||
                slot="end"
 | 
					 | 
				
			||||||
                @click=${this._removeItem}
 | 
					 | 
				
			||||||
              ></ha-icon-button>
 | 
					 | 
				
			||||||
            `
 | 
					 | 
				
			||||||
          : nothing}
 | 
					 | 
				
			||||||
      </ha-md-list-item>
 | 
					 | 
				
			||||||
      ${this.expand && entries && entries.referenced_entities
 | 
					 | 
				
			||||||
        ? this._renderEntries()
 | 
					 | 
				
			||||||
        : nothing}
 | 
					 | 
				
			||||||
    `;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _renderEntries() {
 | 
					 | 
				
			||||||
    const entries = this.parentEntries || this._entries;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    let nextType: TargetType =
 | 
					 | 
				
			||||||
      this.type === "floor"
 | 
					 | 
				
			||||||
        ? "area"
 | 
					 | 
				
			||||||
        : this.type === "area"
 | 
					 | 
				
			||||||
          ? "device"
 | 
					 | 
				
			||||||
          : "entity";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (this.type === "label") {
 | 
					 | 
				
			||||||
      if (entries?.referenced_areas.length) {
 | 
					 | 
				
			||||||
        nextType = "area";
 | 
					 | 
				
			||||||
      } else if (entries?.referenced_devices.length) {
 | 
					 | 
				
			||||||
        nextType = "device";
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const rows1 =
 | 
					 | 
				
			||||||
      (nextType === "area"
 | 
					 | 
				
			||||||
        ? entries?.referenced_areas
 | 
					 | 
				
			||||||
        : nextType === "device" && this.type !== "label"
 | 
					 | 
				
			||||||
          ? entries?.referenced_devices
 | 
					 | 
				
			||||||
          : this.type !== "label"
 | 
					 | 
				
			||||||
            ? entries?.referenced_entities
 | 
					 | 
				
			||||||
            : []) || [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const devicesInAreas = [] as string[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const rows1Entries =
 | 
					 | 
				
			||||||
      nextType === "entity"
 | 
					 | 
				
			||||||
        ? undefined
 | 
					 | 
				
			||||||
        : rows1.map((rowItem) => {
 | 
					 | 
				
			||||||
            const nextEntries = {
 | 
					 | 
				
			||||||
              referenced_areas: [] as string[],
 | 
					 | 
				
			||||||
              referenced_devices: [] as string[],
 | 
					 | 
				
			||||||
              referenced_entities: [] as string[],
 | 
					 | 
				
			||||||
            };
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            if (nextType === "area") {
 | 
					 | 
				
			||||||
              nextEntries.referenced_devices =
 | 
					 | 
				
			||||||
                entries?.referenced_devices.filter(
 | 
					 | 
				
			||||||
                  (device_id) =>
 | 
					 | 
				
			||||||
                    this.hass.devices?.[device_id]?.area_id === rowItem &&
 | 
					 | 
				
			||||||
                    entries?.referenced_entities.some(
 | 
					 | 
				
			||||||
                      (entity_id) =>
 | 
					 | 
				
			||||||
                        this.hass.entities?.[entity_id]?.device_id === device_id
 | 
					 | 
				
			||||||
                    )
 | 
					 | 
				
			||||||
                ) || ([] as string[]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              devicesInAreas.push(...nextEntries.referenced_devices);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              nextEntries.referenced_entities =
 | 
					 | 
				
			||||||
                entries?.referenced_entities.filter((entity_id) => {
 | 
					 | 
				
			||||||
                  const entity = this.hass.entities[entity_id];
 | 
					 | 
				
			||||||
                  return (
 | 
					 | 
				
			||||||
                    entity.area_id === rowItem ||
 | 
					 | 
				
			||||||
                    !entity.device_id ||
 | 
					 | 
				
			||||||
                    nextEntries.referenced_devices.includes(entity.device_id)
 | 
					 | 
				
			||||||
                  );
 | 
					 | 
				
			||||||
                }) || ([] as string[]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
              return nextEntries;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            nextEntries.referenced_entities =
 | 
					 | 
				
			||||||
              entries?.referenced_entities.filter(
 | 
					 | 
				
			||||||
                (entity_id) =>
 | 
					 | 
				
			||||||
                  this.hass.entities?.[entity_id]?.device_id === rowItem
 | 
					 | 
				
			||||||
              ) || ([] as string[]);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            return nextEntries;
 | 
					 | 
				
			||||||
          });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const entityRows =
 | 
					 | 
				
			||||||
      this.type === "label" && entries
 | 
					 | 
				
			||||||
        ? entries.referenced_entities.filter((entity_id) => {
 | 
					 | 
				
			||||||
            const entity = this.hass.entities[entity_id];
 | 
					 | 
				
			||||||
            return (
 | 
					 | 
				
			||||||
              entity.labels.includes(this.itemId) &&
 | 
					 | 
				
			||||||
              !entries.referenced_devices.includes(entity.device_id || "")
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          })
 | 
					 | 
				
			||||||
        : nextType === "device" && entries
 | 
					 | 
				
			||||||
          ? entries.referenced_entities.filter(
 | 
					 | 
				
			||||||
              (entity_id) =>
 | 
					 | 
				
			||||||
                this.hass.entities[entity_id].area_id === this.itemId
 | 
					 | 
				
			||||||
            )
 | 
					 | 
				
			||||||
          : [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const deviceRows =
 | 
					 | 
				
			||||||
      this.type === "label" && entries
 | 
					 | 
				
			||||||
        ? entries.referenced_devices.filter(
 | 
					 | 
				
			||||||
            (device_id) =>
 | 
					 | 
				
			||||||
              !devicesInAreas.includes(device_id) &&
 | 
					 | 
				
			||||||
              this.hass.devices[device_id].labels.includes(this.itemId)
 | 
					 | 
				
			||||||
          )
 | 
					 | 
				
			||||||
        : [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const deviceRowsEntries =
 | 
					 | 
				
			||||||
      deviceRows.length === 0
 | 
					 | 
				
			||||||
        ? undefined
 | 
					 | 
				
			||||||
        : deviceRows.map((device_id) => ({
 | 
					 | 
				
			||||||
            referenced_areas: [] as string[],
 | 
					 | 
				
			||||||
            referenced_devices: [] as string[],
 | 
					 | 
				
			||||||
            referenced_entities:
 | 
					 | 
				
			||||||
              entries?.referenced_entities.filter(
 | 
					 | 
				
			||||||
                (entity_id) =>
 | 
					 | 
				
			||||||
                  this.hass.entities?.[entity_id]?.device_id === device_id
 | 
					 | 
				
			||||||
              ) || ([] as string[]),
 | 
					 | 
				
			||||||
          }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html`
 | 
					 | 
				
			||||||
      <div class="entries-tree">
 | 
					 | 
				
			||||||
        <div class="line-wrapper">
 | 
					 | 
				
			||||||
          <div class="line"></div>
 | 
					 | 
				
			||||||
        </div>
 | 
					 | 
				
			||||||
        <ha-md-list class="entries">
 | 
					 | 
				
			||||||
          ${rows1.map(
 | 
					 | 
				
			||||||
            (itemId, index) => html`
 | 
					 | 
				
			||||||
              <ha-target-picker-item-row
 | 
					 | 
				
			||||||
                sub-entry
 | 
					 | 
				
			||||||
                .hass=${this.hass}
 | 
					 | 
				
			||||||
                .type=${nextType}
 | 
					 | 
				
			||||||
                .itemId=${itemId}
 | 
					 | 
				
			||||||
                .parentEntries=${rows1Entries?.[index]}
 | 
					 | 
				
			||||||
                .hideContext=${this.hideContext || this.type !== "label"}
 | 
					 | 
				
			||||||
                expand
 | 
					 | 
				
			||||||
              ></ha-target-picker-item-row>
 | 
					 | 
				
			||||||
            `
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
          ${deviceRows.map(
 | 
					 | 
				
			||||||
            (itemId, index) => html`
 | 
					 | 
				
			||||||
              <ha-target-picker-item-row
 | 
					 | 
				
			||||||
                sub-entry
 | 
					 | 
				
			||||||
                .hass=${this.hass}
 | 
					 | 
				
			||||||
                type="device"
 | 
					 | 
				
			||||||
                .itemId=${itemId}
 | 
					 | 
				
			||||||
                .parentEntries=${deviceRowsEntries?.[index]}
 | 
					 | 
				
			||||||
                .hideContext=${this.hideContext || this.type !== "label"}
 | 
					 | 
				
			||||||
                expand
 | 
					 | 
				
			||||||
              ></ha-target-picker-item-row>
 | 
					 | 
				
			||||||
            `
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
          ${entityRows.map(
 | 
					 | 
				
			||||||
            (itemId) => html`
 | 
					 | 
				
			||||||
              <ha-target-picker-item-row
 | 
					 | 
				
			||||||
                sub-entry
 | 
					 | 
				
			||||||
                .hass=${this.hass}
 | 
					 | 
				
			||||||
                type="entity"
 | 
					 | 
				
			||||||
                .itemId=${itemId}
 | 
					 | 
				
			||||||
                .hideContext=${this.hideContext || this.type !== "label"}
 | 
					 | 
				
			||||||
              ></ha-target-picker-item-row>
 | 
					 | 
				
			||||||
            `
 | 
					 | 
				
			||||||
          )}
 | 
					 | 
				
			||||||
        </ha-md-list>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    `;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _updateItemData() {
 | 
					 | 
				
			||||||
    if (this.type === "entity") {
 | 
					 | 
				
			||||||
      this._entries = undefined;
 | 
					 | 
				
			||||||
      return;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const entries = await extractFromTarget(this.hass, {
 | 
					 | 
				
			||||||
        [`${this.type}_id`]: [this.itemId],
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const hiddenAreaIds: string[] = [];
 | 
					 | 
				
			||||||
      if (this.type === "floor" || this.type === "label") {
 | 
					 | 
				
			||||||
        entries.referenced_areas = entries.referenced_areas.filter(
 | 
					 | 
				
			||||||
          (area_id) => {
 | 
					 | 
				
			||||||
            const area = this.hass.areas[area_id];
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              (this.type === "floor" || area.labels.includes(this.itemId)) &&
 | 
					 | 
				
			||||||
              areaMeetsFilter(
 | 
					 | 
				
			||||||
                area,
 | 
					 | 
				
			||||||
                this.hass.devices,
 | 
					 | 
				
			||||||
                this.hass.entities,
 | 
					 | 
				
			||||||
                this.deviceFilter,
 | 
					 | 
				
			||||||
                this.includeDomains,
 | 
					 | 
				
			||||||
                this.includeDeviceClasses,
 | 
					 | 
				
			||||||
                this.hass.states,
 | 
					 | 
				
			||||||
                this.entityFilter
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            hiddenAreaIds.push(area_id);
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const hiddenDeviceIds: string[] = [];
 | 
					 | 
				
			||||||
      if (
 | 
					 | 
				
			||||||
        this.type === "floor" ||
 | 
					 | 
				
			||||||
        this.type === "area" ||
 | 
					 | 
				
			||||||
        this.type === "label"
 | 
					 | 
				
			||||||
      ) {
 | 
					 | 
				
			||||||
        entries.referenced_devices = entries.referenced_devices.filter(
 | 
					 | 
				
			||||||
          (device_id) => {
 | 
					 | 
				
			||||||
            const device = this.hass.devices[device_id];
 | 
					 | 
				
			||||||
            if (
 | 
					 | 
				
			||||||
              !hiddenAreaIds.includes(device.area_id || "") &&
 | 
					 | 
				
			||||||
              deviceMeetsFilter(
 | 
					 | 
				
			||||||
                device,
 | 
					 | 
				
			||||||
                this.hass.entities,
 | 
					 | 
				
			||||||
                this.deviceFilter,
 | 
					 | 
				
			||||||
                this.includeDomains,
 | 
					 | 
				
			||||||
                this.includeDeviceClasses,
 | 
					 | 
				
			||||||
                this.hass.states,
 | 
					 | 
				
			||||||
                this.entityFilter
 | 
					 | 
				
			||||||
              )
 | 
					 | 
				
			||||||
            ) {
 | 
					 | 
				
			||||||
              return true;
 | 
					 | 
				
			||||||
            }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
            hiddenDeviceIds.push(device_id);
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      entries.referenced_entities = entries.referenced_entities.filter(
 | 
					 | 
				
			||||||
        (entity_id) => {
 | 
					 | 
				
			||||||
          const entity = this.hass.entities[entity_id];
 | 
					 | 
				
			||||||
          if (hiddenDeviceIds.includes(entity.device_id || "")) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          if (
 | 
					 | 
				
			||||||
            (this.type === "area" && entity.area_id === this.itemId) ||
 | 
					 | 
				
			||||||
            (this.type === "floor" &&
 | 
					 | 
				
			||||||
              entity.area_id &&
 | 
					 | 
				
			||||||
              entries.referenced_areas.includes(entity.area_id)) ||
 | 
					 | 
				
			||||||
            (this.type === "label" && entity.labels.includes(this.itemId)) ||
 | 
					 | 
				
			||||||
            entries.referenced_devices.includes(entity.device_id || "")
 | 
					 | 
				
			||||||
          ) {
 | 
					 | 
				
			||||||
            return entityRegMeetsFilter(
 | 
					 | 
				
			||||||
              entity,
 | 
					 | 
				
			||||||
              this.type === "label",
 | 
					 | 
				
			||||||
              this.includeDomains,
 | 
					 | 
				
			||||||
              this.includeDeviceClasses,
 | 
					 | 
				
			||||||
              this.hass.states,
 | 
					 | 
				
			||||||
              this.entityFilter
 | 
					 | 
				
			||||||
            );
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this._entries = entries;
 | 
					 | 
				
			||||||
    } catch (e) {
 | 
					 | 
				
			||||||
      // eslint-disable-next-line no-console
 | 
					 | 
				
			||||||
      console.error("Failed to extract target", e);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _itemData = memoizeOne((type: TargetType, item: string) => {
 | 
					 | 
				
			||||||
    if (type === "floor") {
 | 
					 | 
				
			||||||
      const floor = this.hass.floors?.[item];
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        name: floor?.name || item,
 | 
					 | 
				
			||||||
        iconPath: floor?.icon,
 | 
					 | 
				
			||||||
        fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (type === "area") {
 | 
					 | 
				
			||||||
      const area = this.hass.areas?.[item];
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        name: area?.name || item,
 | 
					 | 
				
			||||||
        context: area.floor_id && this.hass.floors?.[area.floor_id]?.name,
 | 
					 | 
				
			||||||
        iconPath: area?.icon,
 | 
					 | 
				
			||||||
        fallbackIconPath: mdiTextureBox,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (type === "device") {
 | 
					 | 
				
			||||||
      const device = this.hass.devices?.[item];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (device.primary_config_entry) {
 | 
					 | 
				
			||||||
        this._getDeviceDomain(device.primary_config_entry);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        name: device ? computeDeviceNameDisplay(device, this.hass) : item,
 | 
					 | 
				
			||||||
        context: device?.area_id && this.hass.areas?.[device.area_id]?.name,
 | 
					 | 
				
			||||||
        fallbackIconPath: mdiDevices,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (type === "entity") {
 | 
					 | 
				
			||||||
      this._setDomainName(computeDomain(item));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const stateObject = this.hass.states[item];
 | 
					 | 
				
			||||||
      const entityName = computeEntityName(
 | 
					 | 
				
			||||||
        stateObject,
 | 
					 | 
				
			||||||
        this.hass.entities,
 | 
					 | 
				
			||||||
        this.hass.devices
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      const { area, device } = getEntityContext(
 | 
					 | 
				
			||||||
        stateObject,
 | 
					 | 
				
			||||||
        this.hass.entities,
 | 
					 | 
				
			||||||
        this.hass.devices,
 | 
					 | 
				
			||||||
        this.hass.areas,
 | 
					 | 
				
			||||||
        this.hass.floors
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
      const deviceName = device ? computeDeviceName(device) : undefined;
 | 
					 | 
				
			||||||
      const areaName = area ? computeAreaName(area) : undefined;
 | 
					 | 
				
			||||||
      const context = [areaName, entityName ? deviceName : undefined]
 | 
					 | 
				
			||||||
        .filter(Boolean)
 | 
					 | 
				
			||||||
        .join(computeRTL(this.hass) ? " ◂ " : " ▸ ");
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        name: entityName || deviceName || item,
 | 
					 | 
				
			||||||
        context,
 | 
					 | 
				
			||||||
        stateObject,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // type label
 | 
					 | 
				
			||||||
    const label = this._labelRegistry.find((lab) => lab.label_id === item);
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      name: label?.name || item,
 | 
					 | 
				
			||||||
      iconPath: label?.icon,
 | 
					 | 
				
			||||||
      fallbackIconPath: mdiLabel,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _setDomainName(domain: string) {
 | 
					 | 
				
			||||||
    this._domainName = domainToName(this.hass.localize, domain);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _removeItem(ev) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    fireEvent(this, "remove-target-item", {
 | 
					 | 
				
			||||||
      type: this.type,
 | 
					 | 
				
			||||||
      id: this.itemId,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _getDeviceDomain(configEntryId: string) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const data = await getConfigEntry(this.hass, configEntryId);
 | 
					 | 
				
			||||||
      const domain = data.config_entry.domain;
 | 
					 | 
				
			||||||
      this._iconImg = brandsUrl({
 | 
					 | 
				
			||||||
        domain: domain,
 | 
					 | 
				
			||||||
        type: "icon",
 | 
					 | 
				
			||||||
        darkOptimized: this.hass.themes?.darkMode,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this._setDomainName(domain);
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
      // failed to load config entry -> ignore
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _openDetails() {
 | 
					 | 
				
			||||||
    showTargetDetailsDialog(this, {
 | 
					 | 
				
			||||||
      title: this._itemData(this.type, this.itemId).name,
 | 
					 | 
				
			||||||
      type: this.type,
 | 
					 | 
				
			||||||
      itemId: this.itemId,
 | 
					 | 
				
			||||||
      deviceFilter: this.deviceFilter,
 | 
					 | 
				
			||||||
      entityFilter: this.entityFilter,
 | 
					 | 
				
			||||||
      includeDomains: this.includeDomains,
 | 
					 | 
				
			||||||
      includeDeviceClasses: this.includeDeviceClasses,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static styles = [
 | 
					 | 
				
			||||||
    buttonLinkStyle,
 | 
					 | 
				
			||||||
    css`
 | 
					 | 
				
			||||||
      :host {
 | 
					 | 
				
			||||||
        --md-list-item-top-space: var(--ha-space-0);
 | 
					 | 
				
			||||||
        --md-list-item-bottom-space: var(--ha-space-0);
 | 
					 | 
				
			||||||
        --md-list-item-leading-space: var(--ha-space-2);
 | 
					 | 
				
			||||||
        --md-list-item-trailing-space: var(--ha-space-2);
 | 
					 | 
				
			||||||
        --md-list-item-two-line-container-height: 56px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      :host([expand]:not([sub-entry])) ha-md-list-item {
 | 
					 | 
				
			||||||
        border: 2px solid var(--ha-color-border-neutral-loud);
 | 
					 | 
				
			||||||
        background-color: var(--ha-color-fill-neutral-quiet-resting);
 | 
					 | 
				
			||||||
        border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg));
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      state-badge {
 | 
					 | 
				
			||||||
        color: var(--ha-color-on-neutral-quiet);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .icon {
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      img {
 | 
					 | 
				
			||||||
        width: 24px;
 | 
					 | 
				
			||||||
        height: 24px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      ha-icon-button {
 | 
					 | 
				
			||||||
        --mdc-icon-button-size: 32px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .summary {
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        flex-direction: column;
 | 
					 | 
				
			||||||
        align-items: flex-end;
 | 
					 | 
				
			||||||
        line-height: var(--ha-line-height-condensed);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      :host([sub-entry]) .summary {
 | 
					 | 
				
			||||||
        margin-right: var(--ha-space-12);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .summary .main {
 | 
					 | 
				
			||||||
        font-weight: var(--ha-font-weight-medium);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .summary .secondary {
 | 
					 | 
				
			||||||
        font-size: var(--ha-font-size-s);
 | 
					 | 
				
			||||||
        color: var(--secondary-text-color);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .entries-tree {
 | 
					 | 
				
			||||||
        display: flex;
 | 
					 | 
				
			||||||
        position: relative;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .entries-tree .line-wrapper {
 | 
					 | 
				
			||||||
        padding: var(--ha-space-5);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .entries-tree .line-wrapper .line {
 | 
					 | 
				
			||||||
        border-left: 2px dashed var(--divider-color);
 | 
					 | 
				
			||||||
        height: calc(100% - 28px);
 | 
					 | 
				
			||||||
        position: absolute;
 | 
					 | 
				
			||||||
        top: 0;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      :host([sub-entry]) .entries-tree .line-wrapper .line {
 | 
					 | 
				
			||||||
        height: calc(100% - 12px);
 | 
					 | 
				
			||||||
        top: -18px;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .entries {
 | 
					 | 
				
			||||||
        padding: 0;
 | 
					 | 
				
			||||||
        --md-item-overflow: visible;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .horizontal-line-wrapper {
 | 
					 | 
				
			||||||
        position: relative;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      .horizontal-line-wrapper .horizontal-line {
 | 
					 | 
				
			||||||
        position: absolute;
 | 
					 | 
				
			||||||
        top: 11px;
 | 
					 | 
				
			||||||
        margin-inline-start: -28px;
 | 
					 | 
				
			||||||
        width: 29px;
 | 
					 | 
				
			||||||
        border-top: 2px dashed var(--divider-color);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      button.link {
 | 
					 | 
				
			||||||
        text-decoration: none;
 | 
					 | 
				
			||||||
        color: var(--primary-color);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      button.link:hover,
 | 
					 | 
				
			||||||
      button.link:focus {
 | 
					 | 
				
			||||||
        text-decoration: underline;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      .domain {
 | 
					 | 
				
			||||||
        width: fit-content;
 | 
					 | 
				
			||||||
        border-radius: var(--ha-border-radius-md);
 | 
					 | 
				
			||||||
        background-color: var(--ha-color-fill-neutral-quiet-resting);
 | 
					 | 
				
			||||||
        padding: var(--ha-space-1);
 | 
					 | 
				
			||||||
        font-family: var(--ha-font-family-code);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    `,
 | 
					 | 
				
			||||||
  ];
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare global {
 | 
					 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					 | 
				
			||||||
    "ha-target-picker-item-row": HaTargetPickerItemRow;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,338 +0,0 @@
 | 
				
			|||||||
import { consume } from "@lit/context";
 | 
					 | 
				
			||||||
// @ts-ignore
 | 
					 | 
				
			||||||
import chipStyles from "@material/chips/dist/mdc.chips.min.css";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  mdiClose,
 | 
					 | 
				
			||||||
  mdiDevices,
 | 
					 | 
				
			||||||
  mdiHome,
 | 
					 | 
				
			||||||
  mdiLabel,
 | 
					 | 
				
			||||||
  mdiTextureBox,
 | 
					 | 
				
			||||||
  mdiUnfoldMoreVertical,
 | 
					 | 
				
			||||||
} from "@mdi/js";
 | 
					 | 
				
			||||||
import { css, html, LitElement, nothing, unsafeCSS } from "lit";
 | 
					 | 
				
			||||||
import { customElement, property, state } from "lit/decorators";
 | 
					 | 
				
			||||||
import { classMap } from "lit/directives/class-map";
 | 
					 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					 | 
				
			||||||
import { computeCssColor } from "../../common/color/compute-color";
 | 
					 | 
				
			||||||
import { hex2rgb } from "../../common/color/convert-color";
 | 
					 | 
				
			||||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
					 | 
				
			||||||
import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name";
 | 
					 | 
				
			||||||
import { computeDomain } from "../../common/entity/compute_domain";
 | 
					 | 
				
			||||||
import { computeStateName } from "../../common/entity/compute_state_name";
 | 
					 | 
				
			||||||
import { slugify } from "../../common/string/slugify";
 | 
					 | 
				
			||||||
import { getConfigEntry } from "../../data/config_entries";
 | 
					 | 
				
			||||||
import { labelsContext } from "../../data/context";
 | 
					 | 
				
			||||||
import { domainToName } from "../../data/integration";
 | 
					 | 
				
			||||||
import type { LabelRegistryEntry } from "../../data/label_registry";
 | 
					 | 
				
			||||||
import type { TargetType } from "../../data/target";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../../types";
 | 
					 | 
				
			||||||
import { brandsUrl } from "../../util/brands-url";
 | 
					 | 
				
			||||||
import { floorDefaultIconPath } from "../ha-floor-icon";
 | 
					 | 
				
			||||||
import "../ha-icon";
 | 
					 | 
				
			||||||
import "../ha-icon-button";
 | 
					 | 
				
			||||||
import "../ha-md-list";
 | 
					 | 
				
			||||||
import "../ha-md-list-item";
 | 
					 | 
				
			||||||
import "../ha-state-icon";
 | 
					 | 
				
			||||||
import "../ha-tooltip";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
@customElement("ha-target-picker-value-chip")
 | 
					 | 
				
			||||||
export class HaTargetPickerValueChip extends LitElement {
 | 
					 | 
				
			||||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property() public type!: TargetType;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @property({ attribute: "item-id" }) public itemId!: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _domainName?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state() private _iconImg?: string;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  @state()
 | 
					 | 
				
			||||||
  @consume({ context: labelsContext, subscribe: true })
 | 
					 | 
				
			||||||
  _labelRegistry!: LabelRegistryEntry[];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  protected render() {
 | 
					 | 
				
			||||||
    const { name, iconPath, fallbackIconPath, stateObject, color } =
 | 
					 | 
				
			||||||
      this._itemData(this.type, this.itemId);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return html`
 | 
					 | 
				
			||||||
      <div
 | 
					 | 
				
			||||||
        class="mdc-chip ${classMap({
 | 
					 | 
				
			||||||
          [this.type]: true,
 | 
					 | 
				
			||||||
        })}"
 | 
					 | 
				
			||||||
        style=${color
 | 
					 | 
				
			||||||
          ? `--color: rgb(${color}); --background-color: rgba(${color}, .5)`
 | 
					 | 
				
			||||||
          : ""}
 | 
					 | 
				
			||||||
      >
 | 
					 | 
				
			||||||
        ${iconPath
 | 
					 | 
				
			||||||
          ? html`<ha-icon
 | 
					 | 
				
			||||||
              class="mdc-chip__icon mdc-chip__icon--leading"
 | 
					 | 
				
			||||||
              .icon=${iconPath}
 | 
					 | 
				
			||||||
            ></ha-icon>`
 | 
					 | 
				
			||||||
          : this._iconImg
 | 
					 | 
				
			||||||
            ? html`<img
 | 
					 | 
				
			||||||
                class="mdc-chip__icon mdc-chip__icon--leading"
 | 
					 | 
				
			||||||
                alt=${this._domainName || ""}
 | 
					 | 
				
			||||||
                crossorigin="anonymous"
 | 
					 | 
				
			||||||
                referrerpolicy="no-referrer"
 | 
					 | 
				
			||||||
                src=${this._iconImg}
 | 
					 | 
				
			||||||
              />`
 | 
					 | 
				
			||||||
            : fallbackIconPath
 | 
					 | 
				
			||||||
              ? html`<ha-svg-icon
 | 
					 | 
				
			||||||
                  class="mdc-chip__icon mdc-chip__icon--leading"
 | 
					 | 
				
			||||||
                  .path=${fallbackIconPath}
 | 
					 | 
				
			||||||
                ></ha-svg-icon>`
 | 
					 | 
				
			||||||
              : stateObject
 | 
					 | 
				
			||||||
                ? html`<ha-state-icon
 | 
					 | 
				
			||||||
                    class="mdc-chip__icon mdc-chip__icon--leading"
 | 
					 | 
				
			||||||
                    .hass=${this.hass}
 | 
					 | 
				
			||||||
                    .stateObj=${stateObject}
 | 
					 | 
				
			||||||
                  ></ha-state-icon>`
 | 
					 | 
				
			||||||
                : nothing}
 | 
					 | 
				
			||||||
        <span role="gridcell">
 | 
					 | 
				
			||||||
          <span role="button" tabindex="0" class="mdc-chip__primary-action">
 | 
					 | 
				
			||||||
            <span id="title-${this.itemId}" class="mdc-chip__text"
 | 
					 | 
				
			||||||
              >${name}</span
 | 
					 | 
				
			||||||
            >
 | 
					 | 
				
			||||||
          </span>
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
        ${this.type === "entity"
 | 
					 | 
				
			||||||
          ? nothing
 | 
					 | 
				
			||||||
          : html`<span role="gridcell">
 | 
					 | 
				
			||||||
              <ha-tooltip .for="expand-${slugify(this.itemId)}"
 | 
					 | 
				
			||||||
                >${this.hass.localize(
 | 
					 | 
				
			||||||
                  `ui.components.target-picker.expand_${this.type}_id`
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
              </ha-tooltip>
 | 
					 | 
				
			||||||
              <ha-icon-button
 | 
					 | 
				
			||||||
                class="expand-btn mdc-chip__icon mdc-chip__icon--trailing"
 | 
					 | 
				
			||||||
                .label=${this.hass.localize(
 | 
					 | 
				
			||||||
                  "ui.components.target-picker.expand"
 | 
					 | 
				
			||||||
                )}
 | 
					 | 
				
			||||||
                .path=${mdiUnfoldMoreVertical}
 | 
					 | 
				
			||||||
                hide-title
 | 
					 | 
				
			||||||
                .id="expand-${slugify(this.itemId)}"
 | 
					 | 
				
			||||||
                .type=${this.type}
 | 
					 | 
				
			||||||
                @click=${this._handleExpand}
 | 
					 | 
				
			||||||
              ></ha-icon-button>
 | 
					 | 
				
			||||||
            </span>`}
 | 
					 | 
				
			||||||
        <span role="gridcell">
 | 
					 | 
				
			||||||
          <ha-tooltip .for="remove-${slugify(this.itemId)}">
 | 
					 | 
				
			||||||
            ${this.hass.localize(
 | 
					 | 
				
			||||||
              `ui.components.target-picker.remove_${this.type}_id`
 | 
					 | 
				
			||||||
            )}
 | 
					 | 
				
			||||||
          </ha-tooltip>
 | 
					 | 
				
			||||||
          <ha-icon-button
 | 
					 | 
				
			||||||
            class="mdc-chip__icon mdc-chip__icon--trailing"
 | 
					 | 
				
			||||||
            .label=${this.hass.localize("ui.components.target-picker.remove")}
 | 
					 | 
				
			||||||
            .path=${mdiClose}
 | 
					 | 
				
			||||||
            hide-title
 | 
					 | 
				
			||||||
            .id="remove-${slugify(this.itemId)}"
 | 
					 | 
				
			||||||
            .type=${this.type}
 | 
					 | 
				
			||||||
            @click=${this._removeItem}
 | 
					 | 
				
			||||||
          ></ha-icon-button>
 | 
					 | 
				
			||||||
        </span>
 | 
					 | 
				
			||||||
      </div>
 | 
					 | 
				
			||||||
    `;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _itemData = memoizeOne((type: TargetType, itemId: string) => {
 | 
					 | 
				
			||||||
    if (type === "floor") {
 | 
					 | 
				
			||||||
      const floor = this.hass.floors?.[itemId];
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        name: floor?.name || itemId,
 | 
					 | 
				
			||||||
        iconPath: floor?.icon,
 | 
					 | 
				
			||||||
        fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (type === "area") {
 | 
					 | 
				
			||||||
      const area = this.hass.areas?.[itemId];
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        name: area?.name || itemId,
 | 
					 | 
				
			||||||
        iconPath: area?.icon,
 | 
					 | 
				
			||||||
        fallbackIconPath: mdiTextureBox,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (type === "device") {
 | 
					 | 
				
			||||||
      const device = this.hass.devices?.[itemId];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      if (device.primary_config_entry) {
 | 
					 | 
				
			||||||
        this._getDeviceDomain(device.primary_config_entry);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        name: device ? computeDeviceNameDisplay(device, this.hass) : itemId,
 | 
					 | 
				
			||||||
        fallbackIconPath: mdiDevices,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (type === "entity") {
 | 
					 | 
				
			||||||
      this._setDomainName(computeDomain(itemId));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const stateObj = this.hass.states[itemId];
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        name: computeStateName(stateObj) || itemId,
 | 
					 | 
				
			||||||
        stateObject: stateObj,
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    // type label
 | 
					 | 
				
			||||||
    const label = this._labelRegistry.find((lab) => lab.label_id === itemId);
 | 
					 | 
				
			||||||
    let color = label?.color ? computeCssColor(label.color) : undefined;
 | 
					 | 
				
			||||||
    if (color?.startsWith("var(")) {
 | 
					 | 
				
			||||||
      const computedStyles = getComputedStyle(this);
 | 
					 | 
				
			||||||
      color = computedStyles.getPropertyValue(
 | 
					 | 
				
			||||||
        color.substring(4, color.length - 1)
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    if (color?.startsWith("#")) {
 | 
					 | 
				
			||||||
      color = hex2rgb(color).join(",");
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      name: label?.name || itemId,
 | 
					 | 
				
			||||||
      iconPath: label?.icon,
 | 
					 | 
				
			||||||
      fallbackIconPath: mdiLabel,
 | 
					 | 
				
			||||||
      color,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _setDomainName(domain: string) {
 | 
					 | 
				
			||||||
    this._domainName = domainToName(this.hass.localize, domain);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private async _getDeviceDomain(configEntryId: string) {
 | 
					 | 
				
			||||||
    try {
 | 
					 | 
				
			||||||
      const data = await getConfigEntry(this.hass, configEntryId);
 | 
					 | 
				
			||||||
      const domain = data.config_entry.domain;
 | 
					 | 
				
			||||||
      this._iconImg = brandsUrl({
 | 
					 | 
				
			||||||
        domain: domain,
 | 
					 | 
				
			||||||
        type: "icon",
 | 
					 | 
				
			||||||
        darkOptimized: this.hass.themes?.darkMode,
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      this._setDomainName(domain);
 | 
					 | 
				
			||||||
    } catch {
 | 
					 | 
				
			||||||
      // failed to load config entry -> ignore
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _removeItem(ev) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    fireEvent(this, "remove-target-item", {
 | 
					 | 
				
			||||||
      type: this.type,
 | 
					 | 
				
			||||||
      id: this.itemId,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  private _handleExpand(ev) {
 | 
					 | 
				
			||||||
    ev.stopPropagation();
 | 
					 | 
				
			||||||
    fireEvent(this, "expand-target-item", {
 | 
					 | 
				
			||||||
      type: this.type,
 | 
					 | 
				
			||||||
      id: this.itemId,
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  static styles = css`
 | 
					 | 
				
			||||||
    ${unsafeCSS(chipStyles)}
 | 
					 | 
				
			||||||
    .mdc-chip {
 | 
					 | 
				
			||||||
      color: var(--primary-text-color);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.add {
 | 
					 | 
				
			||||||
      color: rgba(0, 0, 0, 0.87);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .add-container {
 | 
					 | 
				
			||||||
      position: relative;
 | 
					 | 
				
			||||||
      display: inline-flex;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip:not(.add) {
 | 
					 | 
				
			||||||
      cursor: default;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip ha-icon-button {
 | 
					 | 
				
			||||||
      --mdc-icon-button-size: 24px;
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      outline: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip ha-icon-button ha-svg-icon {
 | 
					 | 
				
			||||||
      border-radius: 50%;
 | 
					 | 
				
			||||||
      background: var(--secondary-text-color);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip__icon.mdc-chip__icon--trailing {
 | 
					 | 
				
			||||||
      width: var(--ha-space-4);
 | 
					 | 
				
			||||||
      height: var(--ha-space-4);
 | 
					 | 
				
			||||||
      --mdc-icon-size: 14px;
 | 
					 | 
				
			||||||
      color: var(--secondary-text-color);
 | 
					 | 
				
			||||||
      margin-inline-start: var(--ha-space-1) !important;
 | 
					 | 
				
			||||||
      margin-inline-end: calc(-1 * var(--ha-space-1)) !important;
 | 
					 | 
				
			||||||
      direction: var(--direction);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip__icon--leading {
 | 
					 | 
				
			||||||
      display: flex;
 | 
					 | 
				
			||||||
      align-items: center;
 | 
					 | 
				
			||||||
      justify-content: center;
 | 
					 | 
				
			||||||
      --mdc-icon-size: 20px;
 | 
					 | 
				
			||||||
      border-radius: var(--ha-border-radius-circle);
 | 
					 | 
				
			||||||
      padding: 6px;
 | 
					 | 
				
			||||||
      margin-left: -13px !important;
 | 
					 | 
				
			||||||
      margin-inline-start: -13px !important;
 | 
					 | 
				
			||||||
      margin-inline-end: var(--ha-space-1) !important;
 | 
					 | 
				
			||||||
      direction: var(--direction);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .expand-btn {
 | 
					 | 
				
			||||||
      margin-right: var(--ha-space-0);
 | 
					 | 
				
			||||||
      margin-inline-end: var(--ha-space-0);
 | 
					 | 
				
			||||||
      margin-inline-start: initial;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.area:not(.add),
 | 
					 | 
				
			||||||
    .mdc-chip.floor:not(.add) {
 | 
					 | 
				
			||||||
      border: 1px solid #fed6a4;
 | 
					 | 
				
			||||||
      background: var(--card-background-color);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.area:not(.add) .mdc-chip__icon--leading,
 | 
					 | 
				
			||||||
    .mdc-chip.area.add,
 | 
					 | 
				
			||||||
    .mdc-chip.floor:not(.add) .mdc-chip__icon--leading,
 | 
					 | 
				
			||||||
    .mdc-chip.floor.add {
 | 
					 | 
				
			||||||
      background: #fed6a4;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.device:not(.add) {
 | 
					 | 
				
			||||||
      border: 1px solid #a8e1fb;
 | 
					 | 
				
			||||||
      background: var(--card-background-color);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.device:not(.add) .mdc-chip__icon--leading,
 | 
					 | 
				
			||||||
    .mdc-chip.device.add {
 | 
					 | 
				
			||||||
      background: #a8e1fb;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.entity:not(.add) {
 | 
					 | 
				
			||||||
      border: 1px solid #d2e7b9;
 | 
					 | 
				
			||||||
      background: var(--card-background-color);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.entity:not(.add) .mdc-chip__icon--leading,
 | 
					 | 
				
			||||||
    .mdc-chip.entity.add {
 | 
					 | 
				
			||||||
      background: #d2e7b9;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.label:not(.add) {
 | 
					 | 
				
			||||||
      border: 1px solid var(--color, #e0e0e0);
 | 
					 | 
				
			||||||
      background: var(--card-background-color);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip.label:not(.add) .mdc-chip__icon--leading,
 | 
					 | 
				
			||||||
    .mdc-chip.label.add {
 | 
					 | 
				
			||||||
      background: var(--background-color, #e0e0e0);
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .mdc-chip:hover {
 | 
					 | 
				
			||||||
      z-index: 5;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    :host([disabled]) .mdc-chip {
 | 
					 | 
				
			||||||
      opacity: var(--light-disabled-opacity);
 | 
					 | 
				
			||||||
      pointer-events: none;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    .tooltip-icon-img {
 | 
					 | 
				
			||||||
      width: 24px;
 | 
					 | 
				
			||||||
      height: 24px;
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  `;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
declare global {
 | 
					 | 
				
			||||||
  interface HTMLElementTagNameMap {
 | 
					 | 
				
			||||||
    "ha-target-picker-value-chip": HaTargetPickerValueChip;
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
@@ -6,6 +6,8 @@ import {
 | 
				
			|||||||
  mdiCallSplit,
 | 
					  mdiCallSplit,
 | 
				
			||||||
  mdiCodeBraces,
 | 
					  mdiCodeBraces,
 | 
				
			||||||
  mdiDevices,
 | 
					  mdiDevices,
 | 
				
			||||||
 | 
					  mdiDotsHorizontal,
 | 
				
			||||||
 | 
					  mdiExcavator,
 | 
				
			||||||
  mdiFormatListNumbered,
 | 
					  mdiFormatListNumbered,
 | 
				
			||||||
  mdiGestureDoubleTap,
 | 
					  mdiGestureDoubleTap,
 | 
				
			||||||
  mdiHandBackRight,
 | 
					  mdiHandBackRight,
 | 
				
			||||||
@@ -14,10 +16,10 @@ import {
 | 
				
			|||||||
  mdiRoomService,
 | 
					  mdiRoomService,
 | 
				
			||||||
  mdiShuffleDisabled,
 | 
					  mdiShuffleDisabled,
 | 
				
			||||||
  mdiTimerOutline,
 | 
					  mdiTimerOutline,
 | 
				
			||||||
 | 
					  mdiTools,
 | 
				
			||||||
  mdiTrafficLight,
 | 
					  mdiTrafficLight,
 | 
				
			||||||
} from "@mdi/js";
 | 
					} from "@mdi/js";
 | 
				
			||||||
import type { AutomationElementGroupCollection } from "./automation";
 | 
					import type { AutomationElementGroup } from "./automation";
 | 
				
			||||||
import type { Action } from "./script";
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACTION_ICONS = {
 | 
					export const ACTION_ICONS = {
 | 
				
			||||||
  condition: mdiAbTesting,
 | 
					  condition: mdiAbTesting,
 | 
				
			||||||
@@ -46,73 +48,37 @@ export const YAML_ONLY_ACTION_TYPES = new Set<keyof typeof ACTION_ICONS>([
 | 
				
			|||||||
  "variables",
 | 
					  "variables",
 | 
				
			||||||
]);
 | 
					]);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const ACTION_COLLECTIONS: AutomationElementGroupCollection[] = [
 | 
					export const ACTION_GROUPS: AutomationElementGroup = {
 | 
				
			||||||
  {
 | 
					  device_id: {},
 | 
				
			||||||
    groups: {
 | 
					  helpers: {
 | 
				
			||||||
      device_id: {},
 | 
					    icon: mdiTools,
 | 
				
			||||||
      serviceGroups: {},
 | 
					    members: {},
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  building_blocks: {
 | 
				
			||||||
 | 
					    icon: mdiExcavator,
 | 
				
			||||||
 | 
					    members: {
 | 
				
			||||||
 | 
					      condition: {},
 | 
				
			||||||
 | 
					      delay: {},
 | 
				
			||||||
 | 
					      wait_template: {},
 | 
				
			||||||
 | 
					      wait_for_trigger: {},
 | 
				
			||||||
 | 
					      repeat_count: {},
 | 
				
			||||||
 | 
					      repeat_while: {},
 | 
				
			||||||
 | 
					      repeat_until: {},
 | 
				
			||||||
 | 
					      repeat_for_each: {},
 | 
				
			||||||
 | 
					      choose: {},
 | 
				
			||||||
 | 
					      if: {},
 | 
				
			||||||
 | 
					      stop: {},
 | 
				
			||||||
 | 
					      sequence: {},
 | 
				
			||||||
 | 
					      parallel: {},
 | 
				
			||||||
 | 
					      variables: {},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  other: {
 | 
				
			||||||
    titleKey: "ui.panel.config.automation.editor.actions.groups.helpers.label",
 | 
					    icon: mdiDotsHorizontal,
 | 
				
			||||||
    groups: {
 | 
					    members: {
 | 
				
			||||||
      helpers: {},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  {
 | 
					 | 
				
			||||||
    titleKey: "ui.panel.config.automation.editor.actions.groups.other.label",
 | 
					 | 
				
			||||||
    groups: {
 | 
					 | 
				
			||||||
      event: {},
 | 
					      event: {},
 | 
				
			||||||
      service: {},
 | 
					      service: {},
 | 
				
			||||||
      set_conversation_response: {},
 | 
					      set_conversation_response: {},
 | 
				
			||||||
      other: {},
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
] as const;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const ACTION_BUILDING_BLOCKS_GROUP = {
 | 
					 | 
				
			||||||
  condition: {},
 | 
					 | 
				
			||||||
  delay: {},
 | 
					 | 
				
			||||||
  wait_template: {},
 | 
					 | 
				
			||||||
  wait_for_trigger: {},
 | 
					 | 
				
			||||||
  repeat_count: {},
 | 
					 | 
				
			||||||
  repeat_while: {},
 | 
					 | 
				
			||||||
  repeat_until: {},
 | 
					 | 
				
			||||||
  repeat_for_each: {},
 | 
					 | 
				
			||||||
  choose: {},
 | 
					 | 
				
			||||||
  if: {},
 | 
					 | 
				
			||||||
  stop: {},
 | 
					 | 
				
			||||||
  sequence: {},
 | 
					 | 
				
			||||||
  parallel: {},
 | 
					 | 
				
			||||||
  variables: {},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
// These will be replaced with the correct action
 | 
					 | 
				
			||||||
export const VIRTUAL_ACTIONS: Partial<
 | 
					 | 
				
			||||||
  Record<keyof typeof ACTION_BUILDING_BLOCKS_GROUP, Action>
 | 
					 | 
				
			||||||
> = {
 | 
					 | 
				
			||||||
  repeat_count: {
 | 
					 | 
				
			||||||
    repeat: {
 | 
					 | 
				
			||||||
      count: 2,
 | 
					 | 
				
			||||||
      sequence: [],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  repeat_while: {
 | 
					 | 
				
			||||||
    repeat: {
 | 
					 | 
				
			||||||
      while: [],
 | 
					 | 
				
			||||||
      sequence: [],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  repeat_until: {
 | 
					 | 
				
			||||||
    repeat: {
 | 
					 | 
				
			||||||
      until: [],
 | 
					 | 
				
			||||||
      sequence: [],
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					 | 
				
			||||||
  repeat_for_each: {
 | 
					 | 
				
			||||||
    repeat: {
 | 
					 | 
				
			||||||
      for_each: {},
 | 
					 | 
				
			||||||
      sequence: [],
 | 
					 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
} as const;
 | 
					} as const;
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,260 +0,0 @@
 | 
				
			|||||||
import { computeAreaName } from "../common/entity/compute_area_name";
 | 
					 | 
				
			||||||
import { computeDomain } from "../common/entity/compute_domain";
 | 
					 | 
				
			||||||
import { computeFloorName } from "../common/entity/compute_floor_name";
 | 
					 | 
				
			||||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
 | 
					 | 
				
			||||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					 | 
				
			||||||
import type { AreaRegistryEntry } from "./area_registry";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getDeviceEntityDisplayLookup,
 | 
					 | 
				
			||||||
  type DeviceEntityDisplayLookup,
 | 
					 | 
				
			||||||
  type DeviceRegistryEntry,
 | 
					 | 
				
			||||||
} from "./device_registry";
 | 
					 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
 | 
					 | 
				
			||||||
import type { EntityRegistryDisplayEntry } from "./entity_registry";
 | 
					 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  floorCompare,
 | 
					 | 
				
			||||||
  getFloorAreaLookup,
 | 
					 | 
				
			||||||
  type FloorRegistryEntry,
 | 
					 | 
				
			||||||
} from "./floor_registry";
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface FloorComboBoxItem extends PickerComboBoxItem {
 | 
					 | 
				
			||||||
  type: "floor" | "area";
 | 
					 | 
				
			||||||
  floor?: FloorRegistryEntry;
 | 
					 | 
				
			||||||
  area?: AreaRegistryEntry;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface AreaFloorValue {
 | 
					 | 
				
			||||||
  id: string;
 | 
					 | 
				
			||||||
  type: "floor" | "area";
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getAreasAndFloors = (
 | 
					 | 
				
			||||||
  states: HomeAssistant["states"],
 | 
					 | 
				
			||||||
  haFloors: HomeAssistant["floors"],
 | 
					 | 
				
			||||||
  haAreas: HomeAssistant["areas"],
 | 
					 | 
				
			||||||
  haDevices: HomeAssistant["devices"],
 | 
					 | 
				
			||||||
  haEntities: HomeAssistant["entities"],
 | 
					 | 
				
			||||||
  formatId: (value: AreaFloorValue) => string,
 | 
					 | 
				
			||||||
  includeDomains?: string[],
 | 
					 | 
				
			||||||
  excludeDomains?: string[],
 | 
					 | 
				
			||||||
  includeDeviceClasses?: string[],
 | 
					 | 
				
			||||||
  deviceFilter?: HaDevicePickerDeviceFilterFunc,
 | 
					 | 
				
			||||||
  entityFilter?: HaEntityPickerEntityFilterFunc,
 | 
					 | 
				
			||||||
  excludeAreas?: string[],
 | 
					 | 
				
			||||||
  excludeFloors?: string[]
 | 
					 | 
				
			||||||
): FloorComboBoxItem[] => {
 | 
					 | 
				
			||||||
  const floors = Object.values(haFloors);
 | 
					 | 
				
			||||||
  const areas = Object.values(haAreas);
 | 
					 | 
				
			||||||
  const devices = Object.values(haDevices);
 | 
					 | 
				
			||||||
  const entities = Object.values(haEntities);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let deviceEntityLookup: DeviceEntityDisplayLookup = {};
 | 
					 | 
				
			||||||
  let inputDevices: DeviceRegistryEntry[] | undefined;
 | 
					 | 
				
			||||||
  let inputEntities: EntityRegistryDisplayEntry[] | undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (
 | 
					 | 
				
			||||||
    includeDomains ||
 | 
					 | 
				
			||||||
    excludeDomains ||
 | 
					 | 
				
			||||||
    includeDeviceClasses ||
 | 
					 | 
				
			||||||
    deviceFilter ||
 | 
					 | 
				
			||||||
    entityFilter
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
 | 
					 | 
				
			||||||
    inputDevices = devices;
 | 
					 | 
				
			||||||
    inputEntities = entities.filter((entity) => entity.area_id);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (includeDomains) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => {
 | 
					 | 
				
			||||||
        const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
        if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return deviceEntityLookup[device.id].some((entity) =>
 | 
					 | 
				
			||||||
          includeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      inputEntities = inputEntities!.filter((entity) =>
 | 
					 | 
				
			||||||
        includeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (excludeDomains) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => {
 | 
					 | 
				
			||||||
        const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
        if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
          return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return entities.every(
 | 
					 | 
				
			||||||
          (entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      inputEntities = inputEntities!.filter(
 | 
					 | 
				
			||||||
        (entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (includeDeviceClasses) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => {
 | 
					 | 
				
			||||||
        const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
        if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return deviceEntityLookup[device.id].some((entity) => {
 | 
					 | 
				
			||||||
          const stateObj = states[entity.entity_id];
 | 
					 | 
				
			||||||
          if (!stateObj) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            stateObj.attributes.device_class &&
 | 
					 | 
				
			||||||
            includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      inputEntities = inputEntities!.filter((entity) => {
 | 
					 | 
				
			||||||
        const stateObj = states[entity.entity_id];
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          stateObj.attributes.device_class &&
 | 
					 | 
				
			||||||
          includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (deviceFilter) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (entityFilter) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => {
 | 
					 | 
				
			||||||
        const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
        if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return deviceEntityLookup[device.id].some((entity) => {
 | 
					 | 
				
			||||||
          const stateObj = states[entity.entity_id];
 | 
					 | 
				
			||||||
          if (!stateObj) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return entityFilter(stateObj);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      inputEntities = inputEntities!.filter((entity) => {
 | 
					 | 
				
			||||||
        const stateObj = states[entity.entity_id];
 | 
					 | 
				
			||||||
        if (!stateObj) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return entityFilter!(stateObj);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let outputAreas = areas;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let areaIds: string[] | undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (inputDevices) {
 | 
					 | 
				
			||||||
    areaIds = inputDevices
 | 
					 | 
				
			||||||
      .filter((device) => device.area_id)
 | 
					 | 
				
			||||||
      .map((device) => device.area_id!);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (inputEntities) {
 | 
					 | 
				
			||||||
    areaIds = (areaIds ?? []).concat(
 | 
					 | 
				
			||||||
      inputEntities
 | 
					 | 
				
			||||||
        .filter((entity) => entity.area_id)
 | 
					 | 
				
			||||||
        .map((entity) => entity.area_id!)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (areaIds) {
 | 
					 | 
				
			||||||
    outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id));
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (excludeAreas) {
 | 
					 | 
				
			||||||
    outputAreas = outputAreas.filter(
 | 
					 | 
				
			||||||
      (area) => !excludeAreas!.includes(area.area_id)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (excludeFloors) {
 | 
					 | 
				
			||||||
    outputAreas = outputAreas.filter(
 | 
					 | 
				
			||||||
      (area) => !area.floor_id || !excludeFloors!.includes(area.floor_id)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const floorAreaLookup = getFloorAreaLookup(outputAreas);
 | 
					 | 
				
			||||||
  const unassignedAreas = Object.values(outputAreas).filter(
 | 
					 | 
				
			||||||
    (area) => !area.floor_id || !floorAreaLookup[area.floor_id]
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const compare = floorCompare(haFloors);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  // @ts-ignore
 | 
					 | 
				
			||||||
  const floorAreaEntries: [
 | 
					 | 
				
			||||||
    FloorRegistryEntry | undefined,
 | 
					 | 
				
			||||||
    AreaRegistryEntry[],
 | 
					 | 
				
			||||||
  ][] = Object.entries(floorAreaLookup)
 | 
					 | 
				
			||||||
    .map(([floorId, floorAreas]) => {
 | 
					 | 
				
			||||||
      const floor = floors.find((fl) => fl.floor_id === floorId)!;
 | 
					 | 
				
			||||||
      return [floor, floorAreas] as const;
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
    .sort(([floorA], [floorB]) => compare(floorA.floor_id, floorB.floor_id));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const items: FloorComboBoxItem[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  floorAreaEntries.forEach(([floor, floorAreas]) => {
 | 
					 | 
				
			||||||
    if (floor) {
 | 
					 | 
				
			||||||
      const floorName = computeFloorName(floor);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      const areaSearchLabels = floorAreas
 | 
					 | 
				
			||||||
        .map((area) => {
 | 
					 | 
				
			||||||
          const areaName = computeAreaName(area) || area.area_id;
 | 
					 | 
				
			||||||
          return [area.area_id, areaName, ...area.aliases];
 | 
					 | 
				
			||||||
        })
 | 
					 | 
				
			||||||
        .flat();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
      items.push({
 | 
					 | 
				
			||||||
        id: formatId({ id: floor.floor_id, type: "floor" }),
 | 
					 | 
				
			||||||
        type: "floor",
 | 
					 | 
				
			||||||
        primary: floorName,
 | 
					 | 
				
			||||||
        floor: floor,
 | 
					 | 
				
			||||||
        icon: floor.icon || undefined,
 | 
					 | 
				
			||||||
        search_labels: [
 | 
					 | 
				
			||||||
          floor.floor_id,
 | 
					 | 
				
			||||||
          floorName,
 | 
					 | 
				
			||||||
          ...floor.aliases,
 | 
					 | 
				
			||||||
          ...areaSearchLabels,
 | 
					 | 
				
			||||||
        ],
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
    items.push(
 | 
					 | 
				
			||||||
      ...floorAreas.map((area) => {
 | 
					 | 
				
			||||||
        const areaName = computeAreaName(area) || area.area_id;
 | 
					 | 
				
			||||||
        return {
 | 
					 | 
				
			||||||
          id: formatId({ id: area.area_id, type: "area" }),
 | 
					 | 
				
			||||||
          type: "area" as const,
 | 
					 | 
				
			||||||
          primary: areaName,
 | 
					 | 
				
			||||||
          area: area,
 | 
					 | 
				
			||||||
          icon: area.icon || undefined,
 | 
					 | 
				
			||||||
          search_labels: [area.area_id, areaName, ...area.aliases],
 | 
					 | 
				
			||||||
        };
 | 
					 | 
				
			||||||
      })
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  items.push(
 | 
					 | 
				
			||||||
    ...unassignedAreas.map((area) => {
 | 
					 | 
				
			||||||
      const areaName = computeAreaName(area) || area.area_id;
 | 
					 | 
				
			||||||
      return {
 | 
					 | 
				
			||||||
        id: formatId({ id: area.area_id, type: "area" }),
 | 
					 | 
				
			||||||
        type: "area" as const,
 | 
					 | 
				
			||||||
        primary: areaName,
 | 
					 | 
				
			||||||
        area: area,
 | 
					 | 
				
			||||||
        icon: area.icon || undefined,
 | 
					 | 
				
			||||||
        search_labels: [area.area_id, areaName, ...area.aliases],
 | 
					 | 
				
			||||||
      };
 | 
					 | 
				
			||||||
    })
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return items;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
@@ -1,10 +1,10 @@
 | 
				
			|||||||
import type {
 | 
					import type {
 | 
				
			||||||
  HassEntityAttributeBase,
 | 
					  HassEntityAttributeBase,
 | 
				
			||||||
  HassEntityBase,
 | 
					  HassEntityBase,
 | 
				
			||||||
 | 
					  HassServiceTarget,
 | 
				
			||||||
} from "home-assistant-js-websocket";
 | 
					} from "home-assistant-js-websocket";
 | 
				
			||||||
import { ensureArray } from "../common/array/ensure-array";
 | 
					import { ensureArray } from "../common/array/ensure-array";
 | 
				
			||||||
import { navigate } from "../common/navigate";
 | 
					import { navigate } from "../common/navigate";
 | 
				
			||||||
import type { LocalizeKeys } from "../common/translations/localize";
 | 
					 | 
				
			||||||
import { createSearchParam } from "../common/url/search-params";
 | 
					import { createSearchParam } from "../common/url/search-params";
 | 
				
			||||||
import type { Context, HomeAssistant } from "../types";
 | 
					import type { Context, HomeAssistant } from "../types";
 | 
				
			||||||
import type { BlueprintInput } from "./blueprint";
 | 
					import type { BlueprintInput } from "./blueprint";
 | 
				
			||||||
@@ -12,6 +12,7 @@ import { CONDITION_BUILDING_BLOCKS } from "./condition";
 | 
				
			|||||||
import type { DeviceCondition, DeviceTrigger } from "./device_automation";
 | 
					import type { DeviceCondition, DeviceTrigger } from "./device_automation";
 | 
				
			||||||
import type { Action, Field, MODES } from "./script";
 | 
					import type { Action, Field, MODES } from "./script";
 | 
				
			||||||
import { migrateAutomationAction } from "./script";
 | 
					import { migrateAutomationAction } from "./script";
 | 
				
			||||||
 | 
					import type { TriggerDescription } from "./trigger";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
 | 
					export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
 | 
				
			||||||
export const AUTOMATION_DEFAULT_MAX = 10;
 | 
					export const AUTOMATION_DEFAULT_MAX = 10;
 | 
				
			||||||
@@ -85,6 +86,11 @@ export interface BaseTrigger {
 | 
				
			|||||||
  id?: string;
 | 
					  id?: string;
 | 
				
			||||||
  variables?: Record<string, unknown>;
 | 
					  variables?: Record<string, unknown>;
 | 
				
			||||||
  enabled?: boolean;
 | 
					  enabled?: boolean;
 | 
				
			||||||
 | 
					  options?: Record<string, unknown>;
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export interface PlatformTrigger extends BaseTrigger {
 | 
				
			||||||
 | 
					  target?: HassServiceTarget;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface StateTrigger extends BaseTrigger {
 | 
					export interface StateTrigger extends BaseTrigger {
 | 
				
			||||||
@@ -294,11 +300,6 @@ export interface ShorthandNotCondition extends ShorthandBaseCondition {
 | 
				
			|||||||
  not: Condition[];
 | 
					  not: Condition[];
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface AutomationElementGroupCollection {
 | 
					 | 
				
			||||||
  titleKey?: LocalizeKeys;
 | 
					 | 
				
			||||||
  groups: AutomationElementGroup;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export type AutomationElementGroup = Record<
 | 
					export type AutomationElementGroup = Record<
 | 
				
			||||||
  string,
 | 
					  string,
 | 
				
			||||||
  { icon?: string; members?: AutomationElementGroup }
 | 
					  { icon?: string; members?: AutomationElementGroup }
 | 
				
			||||||
@@ -576,6 +577,7 @@ export interface TriggerSidebarConfig extends BaseSidebarConfig {
 | 
				
			|||||||
  insertAfter: (value: Trigger | Trigger[]) => boolean;
 | 
					  insertAfter: (value: Trigger | Trigger[]) => boolean;
 | 
				
			||||||
  toggleYamlMode: () => void;
 | 
					  toggleYamlMode: () => void;
 | 
				
			||||||
  config: Trigger;
 | 
					  config: Trigger;
 | 
				
			||||||
 | 
					  description?: TriggerDescription;
 | 
				
			||||||
  yamlMode: boolean;
 | 
					  yamlMode: boolean;
 | 
				
			||||||
  uiSupported: boolean;
 | 
					  uiSupported: boolean;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -803,9 +803,15 @@ const tryDescribeTrigger = (
 | 
				
			|||||||
    );
 | 
					    );
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const triggerType = trigger.trigger;
 | 
				
			||||||
 | 
					  const [domain, type] = triggerType.split(".", 2);
 | 
				
			||||||
 | 
					
 | 
				
			||||||
  return (
 | 
					  return (
 | 
				
			||||||
    hass.localize(
 | 
					    hass.localize(
 | 
				
			||||||
      `ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
 | 
					      `component.${domain}.triggers.${type || "_"}.description_configured`
 | 
				
			||||||
 | 
					    ) ||
 | 
				
			||||||
 | 
					    hass.localize(
 | 
				
			||||||
 | 
					      `ui.panel.config.automation.editor.triggers.type.${triggerType}.label`
 | 
				
			||||||
    ) ||
 | 
					    ) ||
 | 
				
			||||||
    hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
 | 
					    hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
 | 
				
			||||||
  );
 | 
					  );
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -3,6 +3,8 @@ import {
 | 
				
			|||||||
  mdiClockOutline,
 | 
					  mdiClockOutline,
 | 
				
			||||||
  mdiCodeBraces,
 | 
					  mdiCodeBraces,
 | 
				
			||||||
  mdiDevices,
 | 
					  mdiDevices,
 | 
				
			||||||
 | 
					  mdiDotsHorizontal,
 | 
				
			||||||
 | 
					  mdiExcavator,
 | 
				
			||||||
  mdiGateOr,
 | 
					  mdiGateOr,
 | 
				
			||||||
  mdiIdentifier,
 | 
					  mdiIdentifier,
 | 
				
			||||||
  mdiMapClock,
 | 
					  mdiMapClock,
 | 
				
			||||||
@@ -13,7 +15,7 @@ import {
 | 
				
			|||||||
  mdiStateMachine,
 | 
					  mdiStateMachine,
 | 
				
			||||||
  mdiWeatherSunny,
 | 
					  mdiWeatherSunny,
 | 
				
			||||||
} from "@mdi/js";
 | 
					} from "@mdi/js";
 | 
				
			||||||
import type { AutomationElementGroupCollection } from "./automation";
 | 
					import type { AutomationElementGroup } from "./automation";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CONDITION_ICONS = {
 | 
					export const CONDITION_ICONS = {
 | 
				
			||||||
  device: mdiDevices,
 | 
					  device: mdiDevices,
 | 
				
			||||||
@@ -29,31 +31,25 @@ export const CONDITION_ICONS = {
 | 
				
			|||||||
  zone: mdiMapMarkerRadius,
 | 
					  zone: mdiMapMarkerRadius,
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CONDITION_COLLECTIONS: AutomationElementGroupCollection[] = [
 | 
					export const CONDITION_GROUPS: AutomationElementGroup = {
 | 
				
			||||||
  {
 | 
					  device: {},
 | 
				
			||||||
    groups: {
 | 
					  entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
 | 
				
			||||||
      device: {},
 | 
					  time_location: {
 | 
				
			||||||
      entity: { icon: mdiShape, members: { state: {}, numeric_state: {} } },
 | 
					    icon: mdiMapClock,
 | 
				
			||||||
      time_location: {
 | 
					    members: { sun: {}, time: {}, zone: {} },
 | 
				
			||||||
        icon: mdiMapClock,
 | 
					 | 
				
			||||||
        members: { sun: {}, time: {}, zone: {} },
 | 
					 | 
				
			||||||
      },
 | 
					 | 
				
			||||||
    },
 | 
					 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
  {
 | 
					  building_blocks: {
 | 
				
			||||||
    titleKey: "ui.panel.config.automation.editor.conditions.groups.other.label",
 | 
					    icon: mdiExcavator,
 | 
				
			||||||
    groups: {
 | 
					    members: { and: {}, or: {}, not: {} },
 | 
				
			||||||
 | 
					  },
 | 
				
			||||||
 | 
					  other: {
 | 
				
			||||||
 | 
					    icon: mdiDotsHorizontal,
 | 
				
			||||||
 | 
					    members: {
 | 
				
			||||||
      template: {},
 | 
					      template: {},
 | 
				
			||||||
      trigger: {},
 | 
					      trigger: {},
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
  },
 | 
					  },
 | 
				
			||||||
] as const;
 | 
					} as const;
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const CONDITION_BUILDING_BLOCKS_GROUP = {
 | 
					 | 
				
			||||||
  and: {},
 | 
					 | 
				
			||||||
  or: {},
 | 
					 | 
				
			||||||
  not: {},
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const CONDITION_BUILDING_BLOCKS = ["and", "or", "not"];
 | 
					export const CONDITION_BUILDING_BLOCKS = ["and", "or", "not"];
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -79,7 +79,6 @@ export interface DataEntryFlowStepAbort {
 | 
				
			|||||||
  reason: string;
 | 
					  reason: string;
 | 
				
			||||||
  description_placeholders?: Record<string, string>;
 | 
					  description_placeholders?: Record<string, string>;
 | 
				
			||||||
  translation_domain?: string;
 | 
					  translation_domain?: string;
 | 
				
			||||||
  next_flow?: [FlowType, string]; // [flow_type, flow_id]
 | 
					 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface DataEntryFlowStepProgress {
 | 
					export interface DataEntryFlowStepProgress {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,12 @@
 | 
				
			|||||||
import { computeAreaName } from "../common/entity/compute_area_name";
 | 
					 | 
				
			||||||
import { computeDeviceNameDisplay } from "../common/entity/compute_device_name";
 | 
					 | 
				
			||||||
import { computeDomain } from "../common/entity/compute_domain";
 | 
					 | 
				
			||||||
import { computeStateName } from "../common/entity/compute_state_name";
 | 
					import { computeStateName } from "../common/entity/compute_state_name";
 | 
				
			||||||
import { getDeviceContext } from "../common/entity/context/get_device_context";
 | 
					 | 
				
			||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
 | 
					import { caseInsensitiveStringCompare } from "../common/string/compare";
 | 
				
			||||||
import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
 | 
					 | 
				
			||||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
import type { ConfigEntry } from "./config_entries";
 | 
					import type { ConfigEntry } from "./config_entries";
 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
 | 
					 | 
				
			||||||
import type {
 | 
					import type {
 | 
				
			||||||
  EntityRegistryDisplayEntry,
 | 
					  EntityRegistryDisplayEntry,
 | 
				
			||||||
  EntityRegistryEntry,
 | 
					  EntityRegistryEntry,
 | 
				
			||||||
} from "./entity_registry";
 | 
					} from "./entity_registry";
 | 
				
			||||||
import type { EntitySources } from "./entity_sources";
 | 
					import type { EntitySources } from "./entity_sources";
 | 
				
			||||||
import { domainToName } from "./integration";
 | 
					 | 
				
			||||||
import type { RegistryEntry } from "./registry";
 | 
					import type { RegistryEntry } from "./registry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export {
 | 
					export {
 | 
				
			||||||
@@ -171,147 +163,3 @@ export const getDeviceIntegrationLookup = (
 | 
				
			|||||||
  }
 | 
					  }
 | 
				
			||||||
  return deviceIntegrations;
 | 
					  return deviceIntegrations;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface DevicePickerItem extends PickerComboBoxItem {
 | 
					 | 
				
			||||||
  domain?: string;
 | 
					 | 
				
			||||||
  domain_name?: string;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getDevices = (
 | 
					 | 
				
			||||||
  hass: HomeAssistant,
 | 
					 | 
				
			||||||
  configEntryLookup: Record<string, ConfigEntry>,
 | 
					 | 
				
			||||||
  includeDomains?: string[],
 | 
					 | 
				
			||||||
  excludeDomains?: string[],
 | 
					 | 
				
			||||||
  includeDeviceClasses?: string[],
 | 
					 | 
				
			||||||
  deviceFilter?: HaDevicePickerDeviceFilterFunc,
 | 
					 | 
				
			||||||
  entityFilter?: HaEntityPickerEntityFilterFunc,
 | 
					 | 
				
			||||||
  excludeDevices?: string[],
 | 
					 | 
				
			||||||
  value?: string
 | 
					 | 
				
			||||||
): DevicePickerItem[] => {
 | 
					 | 
				
			||||||
  const devices = Object.values(hass.devices);
 | 
					 | 
				
			||||||
  const entities = Object.values(hass.entities);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let deviceEntityLookup: DeviceEntityDisplayLookup = {};
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (
 | 
					 | 
				
			||||||
    includeDomains ||
 | 
					 | 
				
			||||||
    excludeDomains ||
 | 
					 | 
				
			||||||
    includeDeviceClasses ||
 | 
					 | 
				
			||||||
    entityFilter
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let inputDevices = devices.filter(
 | 
					 | 
				
			||||||
    (device) => device.id === value || !device.disabled_by
 | 
					 | 
				
			||||||
  );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (includeDomains) {
 | 
					 | 
				
			||||||
    inputDevices = inputDevices.filter((device) => {
 | 
					 | 
				
			||||||
      const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
      if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return deviceEntityLookup[device.id].some((entity) =>
 | 
					 | 
				
			||||||
        includeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (excludeDomains) {
 | 
					 | 
				
			||||||
    inputDevices = inputDevices.filter((device) => {
 | 
					 | 
				
			||||||
      const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
      if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
        return true;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return entities.every(
 | 
					 | 
				
			||||||
        (entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (excludeDevices) {
 | 
					 | 
				
			||||||
    inputDevices = inputDevices.filter(
 | 
					 | 
				
			||||||
      (device) => !excludeDevices!.includes(device.id)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (includeDeviceClasses) {
 | 
					 | 
				
			||||||
    inputDevices = inputDevices.filter((device) => {
 | 
					 | 
				
			||||||
      const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
      if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return deviceEntityLookup[device.id].some((entity) => {
 | 
					 | 
				
			||||||
        const stateObj = hass.states[entity.entity_id];
 | 
					 | 
				
			||||||
        if (!stateObj) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          stateObj.attributes.device_class &&
 | 
					 | 
				
			||||||
          includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (entityFilter) {
 | 
					 | 
				
			||||||
    inputDevices = inputDevices.filter((device) => {
 | 
					 | 
				
			||||||
      const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
      if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
        return false;
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      return devEntities.some((entity) => {
 | 
					 | 
				
			||||||
        const stateObj = hass.states[entity.entity_id];
 | 
					 | 
				
			||||||
        if (!stateObj) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return entityFilter(stateObj);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (deviceFilter) {
 | 
					 | 
				
			||||||
    inputDevices = inputDevices.filter(
 | 
					 | 
				
			||||||
      (device) =>
 | 
					 | 
				
			||||||
        // We always want to include the device of the current value
 | 
					 | 
				
			||||||
        device.id === value || deviceFilter!(device)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const outputDevices = inputDevices.map<DevicePickerItem>((device) => {
 | 
					 | 
				
			||||||
    const deviceName = computeDeviceNameDisplay(
 | 
					 | 
				
			||||||
      device,
 | 
					 | 
				
			||||||
      hass,
 | 
					 | 
				
			||||||
      deviceEntityLookup[device.id]
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const { area } = getDeviceContext(device, hass);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const areaName = area ? computeAreaName(area) : undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const configEntry = device.primary_config_entry
 | 
					 | 
				
			||||||
      ? configEntryLookup?.[device.primary_config_entry]
 | 
					 | 
				
			||||||
      : undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const domain = configEntry?.domain;
 | 
					 | 
				
			||||||
    const domainName = domain ? domainToName(hass.localize, domain) : undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      id: device.id,
 | 
					 | 
				
			||||||
      label: "",
 | 
					 | 
				
			||||||
      primary:
 | 
					 | 
				
			||||||
        deviceName ||
 | 
					 | 
				
			||||||
        hass.localize("ui.components.device-picker.unnamed_device"),
 | 
					 | 
				
			||||||
      secondary: areaName,
 | 
					 | 
				
			||||||
      domain: configEntry?.domain,
 | 
					 | 
				
			||||||
      domain_name: domainName,
 | 
					 | 
				
			||||||
      search_labels: [deviceName, areaName, domain, domainName].filter(
 | 
					 | 
				
			||||||
        Boolean
 | 
					 | 
				
			||||||
      ) as string[],
 | 
					 | 
				
			||||||
      sorting_label: deviceName || "zzz",
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return outputDevices;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,4 +1,3 @@
 | 
				
			|||||||
import type { HassEntity } from "home-assistant-js-websocket";
 | 
					 | 
				
			||||||
import { arrayLiteralIncludes } from "../common/array/literal-includes";
 | 
					import { arrayLiteralIncludes } from "../common/array/literal-includes";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const UNAVAILABLE = "unavailable";
 | 
					export const UNAVAILABLE = "unavailable";
 | 
				
			||||||
@@ -11,5 +10,3 @@ export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const;
 | 
				
			|||||||
 | 
					
 | 
				
			||||||
export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
 | 
					export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES);
 | 
				
			||||||
export const isOffState = arrayLiteralIncludes(OFF_STATES);
 | 
					export const isOffState = arrayLiteralIncludes(OFF_STATES);
 | 
				
			||||||
 | 
					 | 
				
			||||||
export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean;
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,17 +1,12 @@
 | 
				
			|||||||
import type { Connection, HassEntity } from "home-assistant-js-websocket";
 | 
					import type { Connection } from "home-assistant-js-websocket";
 | 
				
			||||||
import { createCollection } from "home-assistant-js-websocket";
 | 
					import { createCollection } from "home-assistant-js-websocket";
 | 
				
			||||||
import type { Store } from "home-assistant-js-websocket/dist/store";
 | 
					import type { Store } from "home-assistant-js-websocket/dist/store";
 | 
				
			||||||
import memoizeOne from "memoize-one";
 | 
					import memoizeOne from "memoize-one";
 | 
				
			||||||
import { computeDomain } from "../common/entity/compute_domain";
 | 
					import { computeDomain } from "../common/entity/compute_domain";
 | 
				
			||||||
import { computeEntityNameList } from "../common/entity/compute_entity_name_display";
 | 
					 | 
				
			||||||
import { computeStateName } from "../common/entity/compute_state_name";
 | 
					import { computeStateName } from "../common/entity/compute_state_name";
 | 
				
			||||||
import { caseInsensitiveStringCompare } from "../common/string/compare";
 | 
					import { caseInsensitiveStringCompare } from "../common/string/compare";
 | 
				
			||||||
import { computeRTL } from "../common/util/compute_rtl";
 | 
					 | 
				
			||||||
import { debounce } from "../common/util/debounce";
 | 
					import { debounce } from "../common/util/debounce";
 | 
				
			||||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
 | 
					 | 
				
			||||||
import { domainToName } from "./integration";
 | 
					 | 
				
			||||||
import type { LightColor } from "./light";
 | 
					import type { LightColor } from "./light";
 | 
				
			||||||
import type { RegistryEntry } from "./registry";
 | 
					import type { RegistryEntry } from "./registry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -329,122 +324,3 @@ export const getAutomaticEntityIds = (
 | 
				
			|||||||
    type: "config/entity_registry/get_automatic_entity_ids",
 | 
					    type: "config/entity_registry/get_automatic_entity_ids",
 | 
				
			||||||
    entity_ids,
 | 
					    entity_ids,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
export interface EntityComboBoxItem extends PickerComboBoxItem {
 | 
					 | 
				
			||||||
  domain_name?: string;
 | 
					 | 
				
			||||||
  stateObj?: HassEntity;
 | 
					 | 
				
			||||||
}
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getEntities = (
 | 
					 | 
				
			||||||
  hass: HomeAssistant,
 | 
					 | 
				
			||||||
  includeDomains?: string[],
 | 
					 | 
				
			||||||
  excludeDomains?: string[],
 | 
					 | 
				
			||||||
  entityFilter?: HaEntityPickerEntityFilterFunc,
 | 
					 | 
				
			||||||
  includeDeviceClasses?: string[],
 | 
					 | 
				
			||||||
  includeUnitOfMeasurement?: string[],
 | 
					 | 
				
			||||||
  includeEntities?: string[],
 | 
					 | 
				
			||||||
  excludeEntities?: string[],
 | 
					 | 
				
			||||||
  value?: string
 | 
					 | 
				
			||||||
): EntityComboBoxItem[] => {
 | 
					 | 
				
			||||||
  let items: EntityComboBoxItem[] = [];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let entityIds = Object.keys(hass.states);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (includeEntities) {
 | 
					 | 
				
			||||||
    entityIds = entityIds.filter((entityId) =>
 | 
					 | 
				
			||||||
      includeEntities.includes(entityId)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (excludeEntities) {
 | 
					 | 
				
			||||||
    entityIds = entityIds.filter(
 | 
					 | 
				
			||||||
      (entityId) => !excludeEntities.includes(entityId)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (includeDomains) {
 | 
					 | 
				
			||||||
    entityIds = entityIds.filter((eid) =>
 | 
					 | 
				
			||||||
      includeDomains.includes(computeDomain(eid))
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (excludeDomains) {
 | 
					 | 
				
			||||||
    entityIds = entityIds.filter(
 | 
					 | 
				
			||||||
      (eid) => !excludeDomains.includes(computeDomain(eid))
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  items = entityIds.map<EntityComboBoxItem>((entityId) => {
 | 
					 | 
				
			||||||
    const stateObj = hass.states[entityId];
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const friendlyName = computeStateName(stateObj); // Keep this for search
 | 
					 | 
				
			||||||
    const [entityName, deviceName, areaName] = computeEntityNameList(
 | 
					 | 
				
			||||||
      stateObj,
 | 
					 | 
				
			||||||
      [{ type: "entity" }, { type: "device" }, { type: "area" }],
 | 
					 | 
				
			||||||
      hass.entities,
 | 
					 | 
				
			||||||
      hass.devices,
 | 
					 | 
				
			||||||
      hass.areas,
 | 
					 | 
				
			||||||
      hass.floors
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const domainName = domainToName(hass.localize, computeDomain(entityId));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const isRTL = computeRTL(hass);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    const primary = entityName || deviceName || entityId;
 | 
					 | 
				
			||||||
    const secondary = [areaName, entityName ? deviceName : undefined]
 | 
					 | 
				
			||||||
      .filter(Boolean)
 | 
					 | 
				
			||||||
      .join(isRTL ? " ◂ " : " ▸ ");
 | 
					 | 
				
			||||||
    const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - ");
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    return {
 | 
					 | 
				
			||||||
      id: entityId,
 | 
					 | 
				
			||||||
      primary: primary,
 | 
					 | 
				
			||||||
      secondary: secondary,
 | 
					 | 
				
			||||||
      domain_name: domainName,
 | 
					 | 
				
			||||||
      sorting_label: [deviceName, entityName].filter(Boolean).join("_"),
 | 
					 | 
				
			||||||
      search_labels: [
 | 
					 | 
				
			||||||
        entityName,
 | 
					 | 
				
			||||||
        deviceName,
 | 
					 | 
				
			||||||
        areaName,
 | 
					 | 
				
			||||||
        domainName,
 | 
					 | 
				
			||||||
        friendlyName,
 | 
					 | 
				
			||||||
        entityId,
 | 
					 | 
				
			||||||
      ].filter(Boolean) as string[],
 | 
					 | 
				
			||||||
      a11y_label: a11yLabel,
 | 
					 | 
				
			||||||
      stateObj: stateObj,
 | 
					 | 
				
			||||||
    };
 | 
					 | 
				
			||||||
  });
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (includeDeviceClasses) {
 | 
					 | 
				
			||||||
    items = items.filter(
 | 
					 | 
				
			||||||
      (item) =>
 | 
					 | 
				
			||||||
        // We always want to include the entity of the current value
 | 
					 | 
				
			||||||
        item.id === value ||
 | 
					 | 
				
			||||||
        (item.stateObj?.attributes.device_class &&
 | 
					 | 
				
			||||||
          includeDeviceClasses.includes(item.stateObj.attributes.device_class))
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (includeUnitOfMeasurement) {
 | 
					 | 
				
			||||||
    items = items.filter(
 | 
					 | 
				
			||||||
      (item) =>
 | 
					 | 
				
			||||||
        // We always want to include the entity of the current value
 | 
					 | 
				
			||||||
        item.id === value ||
 | 
					 | 
				
			||||||
        (item.stateObj?.attributes.unit_of_measurement &&
 | 
					 | 
				
			||||||
          includeUnitOfMeasurement.includes(
 | 
					 | 
				
			||||||
            item.stateObj.attributes.unit_of_measurement
 | 
					 | 
				
			||||||
          ))
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (entityFilter) {
 | 
					 | 
				
			||||||
    items = items.filter(
 | 
					 | 
				
			||||||
      (item) =>
 | 
					 | 
				
			||||||
        // We always want to include the entity of the current value
 | 
					 | 
				
			||||||
        item.id === value || (item.stateObj && entityFilter!(item.stateObj))
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return items;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
@@ -68,18 +68,13 @@ export const getFloorAreaLookup = (
 | 
				
			|||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const floorCompare =
 | 
					export const floorCompare =
 | 
				
			||||||
  (entries?: HomeAssistant["floors"], order?: string[]) =>
 | 
					  (entries?: FloorRegistryEntry[], order?: string[]) =>
 | 
				
			||||||
  (a: string, b: string) => {
 | 
					  (a: string, b: string) => {
 | 
				
			||||||
    const indexA = order ? order.indexOf(a) : -1;
 | 
					    const indexA = order ? order.indexOf(a) : -1;
 | 
				
			||||||
    const indexB = order ? order.indexOf(b) : -1;
 | 
					    const indexB = order ? order.indexOf(b) : -1;
 | 
				
			||||||
    if (indexA === -1 && indexB === -1) {
 | 
					    if (indexA === -1 && indexB === -1) {
 | 
				
			||||||
      const floorA = entries?.[a];
 | 
					      const nameA = entries?.[a]?.name ?? a;
 | 
				
			||||||
      const floorB = entries?.[b];
 | 
					      const nameB = entries?.[b]?.name ?? b;
 | 
				
			||||||
      if (floorA && floorB && floorA.level !== floorB.level) {
 | 
					 | 
				
			||||||
        return (floorB.level ?? -9999) - (floorA.level ?? -9999);
 | 
					 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
      const nameA = floorA?.name ?? a;
 | 
					 | 
				
			||||||
      const nameB = floorB?.name ?? b;
 | 
					 | 
				
			||||||
      return stringCompare(nameA, nameB);
 | 
					      return stringCompare(nameA, nameB);
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    if (indexA === -1) {
 | 
					    if (indexA === -1) {
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -435,9 +435,9 @@ export const convertStatisticsToHistory = (
 | 
				
			|||||||
  Object.entries(orderedStatistics).forEach(([key, value]) => {
 | 
					  Object.entries(orderedStatistics).forEach(([key, value]) => {
 | 
				
			||||||
    const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
 | 
					    const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({
 | 
				
			||||||
      s: e.mean != null ? e.mean.toString() : e.state!.toString(),
 | 
					      s: e.mean != null ? e.mean.toString() : e.state!.toString(),
 | 
				
			||||||
      lc: e.end / 1000,
 | 
					      lc: e.start / 1000,
 | 
				
			||||||
      a: {},
 | 
					      a: {},
 | 
				
			||||||
      lu: e.end / 1000,
 | 
					      lu: e.start / 1000,
 | 
				
			||||||
    }));
 | 
					    }));
 | 
				
			||||||
    statsHistoryStates[key] = entityHistoryStates;
 | 
					    statsHistoryStates[key] = entityHistoryStates;
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -131,14 +131,19 @@ const resources: {
 | 
				
			|||||||
    all?: Promise<Record<string, ServiceIcons>>;
 | 
					    all?: Promise<Record<string, ServiceIcons>>;
 | 
				
			||||||
    domains: Record<string, ServiceIcons | Promise<ServiceIcons>>;
 | 
					    domains: Record<string, ServiceIcons | Promise<ServiceIcons>>;
 | 
				
			||||||
  };
 | 
					  };
 | 
				
			||||||
 | 
					  triggers: {
 | 
				
			||||||
 | 
					    all?: Promise<Record<string, TriggerIcons>>;
 | 
				
			||||||
 | 
					    domains: Record<string, TriggerIcons | Promise<TriggerIcons>>;
 | 
				
			||||||
 | 
					  };
 | 
				
			||||||
} = {
 | 
					} = {
 | 
				
			||||||
  entity: {},
 | 
					  entity: {},
 | 
				
			||||||
  entity_component: {},
 | 
					  entity_component: {},
 | 
				
			||||||
  services: { domains: {} },
 | 
					  services: { domains: {} },
 | 
				
			||||||
 | 
					  triggers: { domains: {} },
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface IconResources<
 | 
					interface IconResources<
 | 
				
			||||||
  T extends ComponentIcons | PlatformIcons | ServiceIcons,
 | 
					  T extends ComponentIcons | PlatformIcons | ServiceIcons | TriggerIcons,
 | 
				
			||||||
> {
 | 
					> {
 | 
				
			||||||
  resources: Record<string, T>;
 | 
					  resources: Record<string, T>;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
@@ -182,12 +187,22 @@ type ServiceIcons = Record<
 | 
				
			|||||||
  { service: string; sections?: Record<string, string> }
 | 
					  { service: string; sections?: Record<string, string> }
 | 
				
			||||||
>;
 | 
					>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export type IconCategory = "entity" | "entity_component" | "services";
 | 
					type TriggerIcons = Record<
 | 
				
			||||||
 | 
					  string,
 | 
				
			||||||
 | 
					  { trigger: string; sections?: Record<string, string> }
 | 
				
			||||||
 | 
					>;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export type IconCategory =
 | 
				
			||||||
 | 
					  | "entity"
 | 
				
			||||||
 | 
					  | "entity_component"
 | 
				
			||||||
 | 
					  | "services"
 | 
				
			||||||
 | 
					  | "triggers";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
interface CategoryType {
 | 
					interface CategoryType {
 | 
				
			||||||
  entity: PlatformIcons;
 | 
					  entity: PlatformIcons;
 | 
				
			||||||
  entity_component: ComponentIcons;
 | 
					  entity_component: ComponentIcons;
 | 
				
			||||||
  services: ServiceIcons;
 | 
					  services: ServiceIcons;
 | 
				
			||||||
 | 
					  triggers: TriggerIcons;
 | 
				
			||||||
}
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const getHassIcons = async <T extends IconCategory>(
 | 
					export const getHassIcons = async <T extends IconCategory>(
 | 
				
			||||||
@@ -265,12 +280,10 @@ export const getServiceIcons = async (
 | 
				
			|||||||
    if (!force && resources.services.all) {
 | 
					    if (!force && resources.services.all) {
 | 
				
			||||||
      return resources.services.all;
 | 
					      return resources.services.all;
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
    resources.services.all = getHassIcons(hass, "services", domain).then(
 | 
					    resources.services.all = getHassIcons(hass, "services").then((res) => {
 | 
				
			||||||
      (res) => {
 | 
					      resources.services.domains = res.resources;
 | 
				
			||||||
        resources.services.domains = res.resources;
 | 
					      return res?.resources;
 | 
				
			||||||
        return res?.resources;
 | 
					    });
 | 
				
			||||||
      }
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    return resources.services.all;
 | 
					    return resources.services.all;
 | 
				
			||||||
  }
 | 
					  }
 | 
				
			||||||
  if (!force && domain in resources.services.domains) {
 | 
					  if (!force && domain in resources.services.domains) {
 | 
				
			||||||
@@ -292,6 +305,40 @@ export const getServiceIcons = async (
 | 
				
			|||||||
  return resources.services.domains[domain];
 | 
					  return resources.services.domains[domain];
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const getTriggerIcons = async (
 | 
				
			||||||
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
 | 
					  domain?: string,
 | 
				
			||||||
 | 
					  force = false
 | 
				
			||||||
 | 
					): Promise<TriggerIcons | Record<string, TriggerIcons> | undefined> => {
 | 
				
			||||||
 | 
					  if (!domain) {
 | 
				
			||||||
 | 
					    if (!force && resources.triggers.all) {
 | 
				
			||||||
 | 
					      return resources.triggers.all;
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    resources.triggers.all = getHassIcons(hass, "triggers").then((res) => {
 | 
				
			||||||
 | 
					      resources.triggers.domains = res.resources;
 | 
				
			||||||
 | 
					      return res?.resources;
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    return resources.triggers.all;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!force && domain in resources.triggers.domains) {
 | 
				
			||||||
 | 
					    return resources.triggers.domains[domain];
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (resources.triggers.all && !force) {
 | 
				
			||||||
 | 
					    await resources.triggers.all;
 | 
				
			||||||
 | 
					    if (domain in resources.triggers.domains) {
 | 
				
			||||||
 | 
					      return resources.triggers.domains[domain];
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!isComponentLoaded(hass, domain)) {
 | 
				
			||||||
 | 
					    return undefined;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  const result = getHassIcons(hass, "triggers", domain);
 | 
				
			||||||
 | 
					  resources.triggers.domains[domain] = result.then(
 | 
				
			||||||
 | 
					    (res) => res?.resources[domain]
 | 
				
			||||||
 | 
					  );
 | 
				
			||||||
 | 
					  return resources.triggers.domains[domain];
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
// Cache for sorted range keys
 | 
					// Cache for sorted range keys
 | 
				
			||||||
const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
 | 
					const sortedRangeCache = new WeakMap<Record<string, string>, number[]>();
 | 
				
			||||||
 | 
					
 | 
				
			||||||
@@ -471,6 +518,26 @@ export const attributeIcon = async (
 | 
				
			|||||||
  return icon;
 | 
					  return icon;
 | 
				
			||||||
};
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					export const triggerIcon = async (
 | 
				
			||||||
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
 | 
					  trigger: string
 | 
				
			||||||
 | 
					): Promise<string | undefined> => {
 | 
				
			||||||
 | 
					  let icon: string | undefined;
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const domain = trigger.includes(".") ? computeDomain(trigger) : trigger;
 | 
				
			||||||
 | 
					  const triggerName = trigger.includes(".") ? computeObjectId(trigger) : "_";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					  const triggerIcons = await getTriggerIcons(hass, domain);
 | 
				
			||||||
 | 
					  if (triggerIcons) {
 | 
				
			||||||
 | 
					    const trgrIcon = triggerIcons[triggerName] as TriggerIcons[string];
 | 
				
			||||||
 | 
					    icon = trgrIcon?.trigger;
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  if (!icon) {
 | 
				
			||||||
 | 
					    icon = await domainIcon(hass, domain);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					  return icon;
 | 
				
			||||||
 | 
					};
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export const serviceIcon = async (
 | 
					export const serviceIcon = async (
 | 
				
			||||||
  hass: HomeAssistant,
 | 
					  hass: HomeAssistant,
 | 
				
			||||||
  service: string
 | 
					  service: string
 | 
				
			||||||
 
 | 
				
			|||||||
@@ -1,20 +1,9 @@
 | 
				
			|||||||
import { mdiLabel } from "@mdi/js";
 | 
					 | 
				
			||||||
import type { Connection } from "home-assistant-js-websocket";
 | 
					import type { Connection } from "home-assistant-js-websocket";
 | 
				
			||||||
import { createCollection } from "home-assistant-js-websocket";
 | 
					import { createCollection } from "home-assistant-js-websocket";
 | 
				
			||||||
import type { Store } from "home-assistant-js-websocket/dist/store";
 | 
					import type { Store } from "home-assistant-js-websocket/dist/store";
 | 
				
			||||||
import { computeDomain } from "../common/entity/compute_domain";
 | 
					 | 
				
			||||||
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 type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker";
 | 
					 | 
				
			||||||
import type { PickerComboBoxItem } from "../components/ha-picker-combo-box";
 | 
					 | 
				
			||||||
import type { HomeAssistant } from "../types";
 | 
					import type { HomeAssistant } from "../types";
 | 
				
			||||||
import {
 | 
					 | 
				
			||||||
  getDeviceEntityDisplayLookup,
 | 
					 | 
				
			||||||
  type DeviceEntityDisplayLookup,
 | 
					 | 
				
			||||||
  type DeviceRegistryEntry,
 | 
					 | 
				
			||||||
} from "./device_registry";
 | 
					 | 
				
			||||||
import type { HaEntityPickerEntityFilterFunc } from "./entity";
 | 
					 | 
				
			||||||
import type { EntityRegistryDisplayEntry } from "./entity_registry";
 | 
					 | 
				
			||||||
import type { RegistryEntry } from "./registry";
 | 
					import type { RegistryEntry } from "./registry";
 | 
				
			||||||
 | 
					
 | 
				
			||||||
export interface LabelRegistryEntry extends RegistryEntry {
 | 
					export interface LabelRegistryEntry extends RegistryEntry {
 | 
				
			||||||
@@ -99,179 +88,3 @@ export const deleteLabelRegistryEntry = (
 | 
				
			|||||||
    type: "config/label_registry/delete",
 | 
					    type: "config/label_registry/delete",
 | 
				
			||||||
    label_id: labelId,
 | 
					    label_id: labelId,
 | 
				
			||||||
  });
 | 
					  });
 | 
				
			||||||
 | 
					 | 
				
			||||||
export const getLabels = (
 | 
					 | 
				
			||||||
  hass: HomeAssistant,
 | 
					 | 
				
			||||||
  labels?: LabelRegistryEntry[],
 | 
					 | 
				
			||||||
  includeDomains?: string[],
 | 
					 | 
				
			||||||
  excludeDomains?: string[],
 | 
					 | 
				
			||||||
  includeDeviceClasses?: string[],
 | 
					 | 
				
			||||||
  deviceFilter?: HaDevicePickerDeviceFilterFunc,
 | 
					 | 
				
			||||||
  entityFilter?: HaEntityPickerEntityFilterFunc,
 | 
					 | 
				
			||||||
  excludeLabels?: string[]
 | 
					 | 
				
			||||||
): PickerComboBoxItem[] => {
 | 
					 | 
				
			||||||
  if (!labels || labels.length === 0) {
 | 
					 | 
				
			||||||
    return [];
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const devices = Object.values(hass.devices);
 | 
					 | 
				
			||||||
  const entities = Object.values(hass.entities);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let deviceEntityLookup: DeviceEntityDisplayLookup = {};
 | 
					 | 
				
			||||||
  let inputDevices: DeviceRegistryEntry[] | undefined;
 | 
					 | 
				
			||||||
  let inputEntities: EntityRegistryDisplayEntry[] | undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (
 | 
					 | 
				
			||||||
    includeDomains ||
 | 
					 | 
				
			||||||
    excludeDomains ||
 | 
					 | 
				
			||||||
    includeDeviceClasses ||
 | 
					 | 
				
			||||||
    deviceFilter ||
 | 
					 | 
				
			||||||
    entityFilter
 | 
					 | 
				
			||||||
  ) {
 | 
					 | 
				
			||||||
    deviceEntityLookup = getDeviceEntityDisplayLookup(entities);
 | 
					 | 
				
			||||||
    inputDevices = devices;
 | 
					 | 
				
			||||||
    inputEntities = entities.filter((entity) => entity.labels.length > 0);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (includeDomains) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => {
 | 
					 | 
				
			||||||
        const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
        if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return deviceEntityLookup[device.id].some((entity) =>
 | 
					 | 
				
			||||||
          includeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      inputEntities = inputEntities!.filter((entity) =>
 | 
					 | 
				
			||||||
        includeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (excludeDomains) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => {
 | 
					 | 
				
			||||||
        const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
        if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
          return true;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return entities.every(
 | 
					 | 
				
			||||||
          (entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      inputEntities = inputEntities!.filter(
 | 
					 | 
				
			||||||
        (entity) => !excludeDomains.includes(computeDomain(entity.entity_id))
 | 
					 | 
				
			||||||
      );
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (includeDeviceClasses) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => {
 | 
					 | 
				
			||||||
        const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
        if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return deviceEntityLookup[device.id].some((entity) => {
 | 
					 | 
				
			||||||
          const stateObj = hass.states[entity.entity_id];
 | 
					 | 
				
			||||||
          if (!stateObj) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return (
 | 
					 | 
				
			||||||
            stateObj.attributes.device_class &&
 | 
					 | 
				
			||||||
            includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
					 | 
				
			||||||
          );
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      inputEntities = inputEntities!.filter((entity) => {
 | 
					 | 
				
			||||||
        const stateObj = hass.states[entity.entity_id];
 | 
					 | 
				
			||||||
        return (
 | 
					 | 
				
			||||||
          stateObj.attributes.device_class &&
 | 
					 | 
				
			||||||
          includeDeviceClasses.includes(stateObj.attributes.device_class)
 | 
					 | 
				
			||||||
        );
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (deviceFilter) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => deviceFilter!(device));
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    if (entityFilter) {
 | 
					 | 
				
			||||||
      inputDevices = inputDevices!.filter((device) => {
 | 
					 | 
				
			||||||
        const devEntities = deviceEntityLookup[device.id];
 | 
					 | 
				
			||||||
        if (!devEntities || !devEntities.length) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return deviceEntityLookup[device.id].some((entity) => {
 | 
					 | 
				
			||||||
          const stateObj = hass.states[entity.entity_id];
 | 
					 | 
				
			||||||
          if (!stateObj) {
 | 
					 | 
				
			||||||
            return false;
 | 
					 | 
				
			||||||
          }
 | 
					 | 
				
			||||||
          return entityFilter(stateObj);
 | 
					 | 
				
			||||||
        });
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
      inputEntities = inputEntities!.filter((entity) => {
 | 
					 | 
				
			||||||
        const stateObj = hass.states[entity.entity_id];
 | 
					 | 
				
			||||||
        if (!stateObj) {
 | 
					 | 
				
			||||||
          return false;
 | 
					 | 
				
			||||||
        }
 | 
					 | 
				
			||||||
        return entityFilter!(stateObj);
 | 
					 | 
				
			||||||
      });
 | 
					 | 
				
			||||||
    }
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let outputLabels = labels;
 | 
					 | 
				
			||||||
  const usedLabels = new Set<string>();
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  let areaIds: string[] | undefined;
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (inputDevices) {
 | 
					 | 
				
			||||||
    areaIds = inputDevices
 | 
					 | 
				
			||||||
      .filter((device) => device.area_id)
 | 
					 | 
				
			||||||
      .map((device) => device.area_id!);
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
    inputDevices.forEach((device) => {
 | 
					 | 
				
			||||||
      device.labels.forEach((label) => usedLabels.add(label));
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (inputEntities) {
 | 
					 | 
				
			||||||
    areaIds = (areaIds ?? []).concat(
 | 
					 | 
				
			||||||
      inputEntities
 | 
					 | 
				
			||||||
        .filter((entity) => entity.area_id)
 | 
					 | 
				
			||||||
        .map((entity) => entity.area_id!)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
    inputEntities.forEach((entity) => {
 | 
					 | 
				
			||||||
      entity.labels.forEach((label) => usedLabels.add(label));
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (areaIds) {
 | 
					 | 
				
			||||||
    areaIds.forEach((areaId) => {
 | 
					 | 
				
			||||||
      const area = hass.areas[areaId];
 | 
					 | 
				
			||||||
      area.labels.forEach((label) => usedLabels.add(label));
 | 
					 | 
				
			||||||
    });
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (excludeLabels) {
 | 
					 | 
				
			||||||
    outputLabels = outputLabels.filter(
 | 
					 | 
				
			||||||
      (label) => !excludeLabels!.includes(label.label_id)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  if (inputDevices || inputEntities) {
 | 
					 | 
				
			||||||
    outputLabels = outputLabels.filter((label) =>
 | 
					 | 
				
			||||||
      usedLabels.has(label.label_id)
 | 
					 | 
				
			||||||
    );
 | 
					 | 
				
			||||||
  }
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  const items = outputLabels.map<PickerComboBoxItem>((label) => ({
 | 
					 | 
				
			||||||
    id: label.label_id,
 | 
					 | 
				
			||||||
    primary: label.name,
 | 
					 | 
				
			||||||
    secondary: label.description ?? "",
 | 
					 | 
				
			||||||
    icon: label.icon || undefined,
 | 
					 | 
				
			||||||
    icon_path: label.icon ? undefined : mdiLabel,
 | 
					 | 
				
			||||||
    sorting_label: label.name,
 | 
					 | 
				
			||||||
    search_labels: [label.name, label.label_id, label.description].filter(
 | 
					 | 
				
			||||||
      (v): v is string => Boolean(v)
 | 
					 | 
				
			||||||
    ),
 | 
					 | 
				
			||||||
  }));
 | 
					 | 
				
			||||||
 | 
					 | 
				
			||||||
  return items;
 | 
					 | 
				
			||||||
};
 | 
					 | 
				
			||||||
 
 | 
				
			|||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user