Compare commits
	
		
			187 Commits
		
	
	
		
			card_edito
			...
			grid_preci
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					a86bab98ad | ||
| 
						 | 
					8939dd2213 | ||
| 
						 | 
					fde1bb7d6a | ||
| 
						 | 
					07e5aa30c6 | ||
| 
						 | 
					3f0ec03a14 | ||
| 
						 | 
					1bb871b9ac | ||
| 
						 | 
					0e8783fb01 | ||
| 
						 | 
					1d88c4465b | ||
| 
						 | 
					af2d575bf0 | ||
| 
						 | 
					92165d776a | ||
| 
						 | 
					a8bbd8ab90 | ||
| 
						 | 
					43ac9dbea7 | ||
| 
						 | 
					bba9eca4e9 | ||
| 
						 | 
					40f65b1980 | ||
| 
						 | 
					23a33b10a1 | ||
| 
						 | 
					67a93013c7 | ||
| 
						 | 
					1f838d7529 | ||
| 
						 | 
					ffc0435144 | ||
| 
						 | 
					5877d69c87 | ||
| 
						 | 
					99035cea8f | ||
| 
						 | 
					1b441a7eec | ||
| 
						 | 
					ad49e9f7b0 | ||
| 
						 | 
					e32b15ede2 | ||
| 
						 | 
					a35b4376ea | ||
| 
						 | 
					619f9f76ee | ||
| 
						 | 
					f771bc10db | ||
| 
						 | 
					b8889a1183 | ||
| 
						 | 
					eb6b45eaed | ||
| 
						 | 
					31a748ed93 | ||
| 
						 | 
					0110bdd24a | ||
| 
						 | 
					365b712976 | ||
| 
						 | 
					7d97dbe15b | ||
| 
						 | 
					8bc0ea5a0b | ||
| 
						 | 
					44948a3474 | ||
| 
						 | 
					bc51b53b4a | ||
| 
						 | 
					67217b9dd0 | ||
| 
						 | 
					487795b7c4 | ||
| 
						 | 
					a30e0d33f9 | ||
| 
						 | 
					0c1b8abe03 | ||
| 
						 | 
					ce9c5149d5 | ||
| 
						 | 
					adbcdc62eb | ||
| 
						 | 
					faf872bfb8 | ||
| 
						 | 
					fe0fb2382a | ||
| 
						 | 
					cdd29295e5 | ||
| 
						 | 
					f7532f3476 | ||
| 
						 | 
					c8930cec87 | ||
| 
						 | 
					f9c336890d | ||
| 
						 | 
					c721de109f | ||
| 
						 | 
					1c95e8d6ec | ||
| 
						 | 
					57cf2c1341 | ||
| 
						 | 
					f7d5c5f850 | ||
| 
						 | 
					470f5127f4 | ||
| 
						 | 
					34e361601a | ||
| 
						 | 
					70d6cce8f8 | ||
| 
						 | 
					f9814f35d1 | ||
| 
						 | 
					f30603753e | ||
| 
						 | 
					ce9993fd36 | ||
| 
						 | 
					4c2044e70a | ||
| 
						 | 
					7f96c1fbe1 | ||
| 
						 | 
					75e24780c1 | ||
| 
						 | 
					95580bc4c0 | ||
| 
						 | 
					175f68e0cf | ||
| 
						 | 
					b6efedfc8d | ||
| 
						 | 
					23c21a35d8 | ||
| 
						 | 
					9c7324298b | ||
| 
						 | 
					e92be566a0 | ||
| 
						 | 
					4e96ad5f28 | ||
| 
						 | 
					f64a1500af | ||
| 
						 | 
					c9e8619c04 | ||
| 
						 | 
					7ab1133b45 | ||
| 
						 | 
					77abfd3e61 | ||
| 
						 | 
					d7aaa41aa4 | ||
| 
						 | 
					8223f6b155 | ||
| 
						 | 
					435eae77fa | ||
| 
						 | 
					ead54e445f | ||
| 
						 | 
					7ee5db2be5 | ||
| 
						 | 
					fef6f0ac94 | ||
| 
						 | 
					7a60763786 | ||
| 
						 | 
					94e321a364 | ||
| 
						 | 
					1c12c2b714 | ||
| 
						 | 
					442a8f11a7 | ||
| 
						 | 
					4e8b58cd6c | ||
| 
						 | 
					a92dab46c2 | ||
| 
						 | 
					468660d235 | ||
| 
						 | 
					c721afa137 | ||
| 
						 | 
					ac9654c1de | ||
| 
						 | 
					570ad38bac | ||
| 
						 | 
					e778a9aa1d | ||
| 
						 | 
					49576189af | ||
| 
						 | 
					5d71d4c0a1 | ||
| 
						 | 
					d334b1ca7b | ||
| 
						 | 
					5551e98388 | ||
| 
						 | 
					59945cb2f8 | ||
| 
						 | 
					500bc959f0 | ||
| 
						 | 
					deece20206 | ||
| 
						 | 
					fc8945be60 | ||
| 
						 | 
					3fbd5f07a9 | ||
| 
						 | 
					ff9af2f980 | ||
| 
						 | 
					62cba99491 | ||
| 
						 | 
					5a5005c09c | ||
| 
						 | 
					dd179e1f4e | ||
| 
						 | 
					27bdf80168 | ||
| 
						 | 
					f70ce7491a | ||
| 
						 | 
					9f17f6a8cf | ||
| 
						 | 
					4e51c7cf96 | ||
| 
						 | 
					291c026da0 | ||
| 
						 | 
					dd88d8633f | ||
| 
						 | 
					254ee8568b | ||
| 
						 | 
					cd631e8693 | ||
| 
						 | 
					765812331b | ||
| 
						 | 
					7462f8fbe3 | ||
| 
						 | 
					dc940f248c | ||
| 
						 | 
					2793ca65cd | ||
| 
						 | 
					e687ddab21 | ||
| 
						 | 
					4bd27e5055 | ||
| 
						 | 
					c6e2e07286 | ||
| 
						 | 
					e77508b8a8 | ||
| 
						 | 
					a5db44a167 | ||
| 
						 | 
					265bbfc95d | ||
| 
						 | 
					305cecb213 | ||
| 
						 | 
					813feff12e | ||
| 
						 | 
					cbce6f633f | ||
| 
						 | 
					1bbf45d35e | ||
| 
						 | 
					76e53e9738 | ||
| 
						 | 
					c30e4a6935 | ||
| 
						 | 
					c4a700a55c | ||
| 
						 | 
					a759767d79 | ||
| 
						 | 
					7f868c8140 | ||
| 
						 | 
					f7f37c24e2 | ||
| 
						 | 
					be02a8869f | ||
| 
						 | 
					3a9f09cb47 | ||
| 
						 | 
					0c2a9d85e0 | ||
| 
						 | 
					e72356033c | ||
| 
						 | 
					3c48559df6 | ||
| 
						 | 
					f36d68c677 | ||
| 
						 | 
					af46b8221e | ||
| 
						 | 
					d25f72524b | ||
| 
						 | 
					0840d8a10e | ||
| 
						 | 
					597bf5def0 | ||
| 
						 | 
					3478bd309b | ||
| 
						 | 
					64b8b7658d | ||
| 
						 | 
					a1af8718a0 | ||
| 
						 | 
					fd9e2b647d | ||
| 
						 | 
					caee4ba7bc | ||
| 
						 | 
					915036006d | ||
| 
						 | 
					48887f2066 | ||
| 
						 | 
					68d9ce7923 | ||
| 
						 | 
					a36f3c8fb1 | ||
| 
						 | 
					4dfadea9e9 | ||
| 
						 | 
					71dc26edab | ||
| 
						 | 
					f260c95add | ||
| 
						 | 
					dc6f1efffb | ||
| 
						 | 
					b7763882f4 | ||
| 
						 | 
					7de5c46f14 | ||
| 
						 | 
					5920efa2b2 | ||
| 
						 | 
					d2194d55f9 | ||
| 
						 | 
					c0043af4c9 | ||
| 
						 | 
					dcf763438b | ||
| 
						 | 
					858a00e28c | ||
| 
						 | 
					ab407e8274 | ||
| 
						 | 
					14f96a6262 | ||
| 
						 | 
					2b33c70e04 | ||
| 
						 | 
					717443e2d6 | ||
| 
						 | 
					2aba9099a0 | ||
| 
						 | 
					3079f126a8 | ||
| 
						 | 
					1cdfb746bf | ||
| 
						 | 
					39a1844991 | ||
| 
						 | 
					9e4dc0d39e | ||
| 
						 | 
					ab91a4b814 | ||
| 
						 | 
					ca66c02fb3 | ||
| 
						 | 
					97bb052d71 | ||
| 
						 | 
					137bb473c0 | ||
| 
						 | 
					326b57f91b | ||
| 
						 | 
					32feab6a70 | ||
| 
						 | 
					68a0d04f04 | ||
| 
						 | 
					9078ab4026 | ||
| 
						 | 
					8605684906 | ||
| 
						 | 
					9f17d17d6e | ||
| 
						 | 
					ba5f176d52 | ||
| 
						 | 
					7115d14699 | ||
| 
						 | 
					23e37daff3 | ||
| 
						 | 
					ed6c2dfe39 | ||
| 
						 | 
					b48a28f2a6 | ||
| 
						 | 
					3166fec7db | ||
| 
						 | 
					1a67bd0414 | ||
| 
						 | 
					d34c43e292 | ||
| 
						 | 
					c7cfbb5b6c | 
							
								
								
									
										8
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -21,12 +21,12 @@ jobs:
 | 
			
		||||
      url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          ref: dev
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -57,12 +57,12 @@ jobs:
 | 
			
		||||
      url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          ref: master
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -24,9 +24,9 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -37,7 +37,7 @@ jobs:
 | 
			
		||||
      - name: Build resources
 | 
			
		||||
        run: ./node_modules/.bin/gulp gen-icons-json build-translations build-locale-data gather-gallery-pages
 | 
			
		||||
      - name: Setup lint cache
 | 
			
		||||
        uses: actions/cache@v4.0.2
 | 
			
		||||
        uses: actions/cache@v4.1.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: |
 | 
			
		||||
            node_modules/.cache/prettier
 | 
			
		||||
@@ -58,9 +58,9 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -76,9 +76,9 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -100,9 +100,9 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -23,7 +23,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          # We must fetch at least the immediate parents so that if this is
 | 
			
		||||
          # a pull request then we can checkout the head.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										8
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -22,12 +22,12 @@ jobs:
 | 
			
		||||
      url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          ref: dev
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
@@ -58,12 +58,12 @@ jobs:
 | 
			
		||||
      url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
        with:
 | 
			
		||||
          ref: master
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -16,10 +16,10 @@ jobs:
 | 
			
		||||
      url: ${{ steps.deploy.outputs.NETLIFY_LIVE_URL || steps.deploy.outputs.NETLIFY_URL }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -21,10 +21,10 @@ jobs:
 | 
			
		||||
    if: github.repository == 'home-assistant/frontend' && contains(github.event.pull_request.labels.*.name, 'needs design preview')
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out files from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -20,7 +20,7 @@ jobs:
 | 
			
		||||
      contents: write
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
 | 
			
		||||
      - name: Set up Python ${{ env.PYTHON_VERSION }}
 | 
			
		||||
        uses: actions/setup-python@v5
 | 
			
		||||
@@ -28,7 +28,7 @@ jobs:
 | 
			
		||||
          python-version: ${{ env.PYTHON_VERSION }}
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -23,7 +23,7 @@ jobs:
 | 
			
		||||
      contents: write # Required to upload release assets
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
 | 
			
		||||
      - name: Verify version
 | 
			
		||||
        uses: home-assistant/actions/helpers/verify-version@master
 | 
			
		||||
@@ -34,7 +34,7 @@ jobs:
 | 
			
		||||
          python-version: ${{ env.PYTHON_VERSION }}
 | 
			
		||||
 | 
			
		||||
      - name: Setup Node
 | 
			
		||||
        uses: actions/setup-node@v4.0.3
 | 
			
		||||
        uses: actions/setup-node@v4.0.4
 | 
			
		||||
        with:
 | 
			
		||||
          node-version-file: ".nvmrc"
 | 
			
		||||
          cache: yarn
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/translations.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						@@ -13,7 +13,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout the repository
 | 
			
		||||
        uses: actions/checkout@v4.1.7
 | 
			
		||||
        uses: actions/checkout@v4.2.0
 | 
			
		||||
 | 
			
		||||
      - name: Upload Translations
 | 
			
		||||
        run: |
 | 
			
		||||
 
 | 
			
		||||
@@ -6,4 +6,4 @@ enableGlobalCache: false
 | 
			
		||||
 | 
			
		||||
nodeLinker: node-modules
 | 
			
		||||
 | 
			
		||||
yarnPath: .yarn/releases/yarn-4.4.1.cjs
 | 
			
		||||
yarnPath: .yarn/releases/yarn-4.5.0.cjs
 | 
			
		||||
 
 | 
			
		||||
@@ -27,3 +27,5 @@ A complete guide can be found at the following [link](https://www.home-assistant
 | 
			
		||||
Home Assistant is open-source and Apache 2 licensed. Feel free to browse the repository, learn and reuse parts in your own projects.
 | 
			
		||||
 | 
			
		||||
We use [BrowserStack](https://www.browserstack.com) to test Home Assistant on a large variety of devices.
 | 
			
		||||
 | 
			
		||||
[](https://www.openhomefoundation.org/)
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,12 @@ function copyPolyfills(staticDir) {
 | 
			
		||||
    npmPath("@webcomponents/webcomponentsjs/webcomponents-bundle.js.map"),
 | 
			
		||||
    staticPath("polyfills/")
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  // dialog-polyfill css
 | 
			
		||||
  copyFileDir(
 | 
			
		||||
    npmPath("dialog-polyfill/dialog-polyfill.css"),
 | 
			
		||||
    staticPath("polyfills/")
 | 
			
		||||
  );
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function copyLoaderJS(staticDir) {
 | 
			
		||||
 
 | 
			
		||||
@@ -139,7 +139,7 @@
 | 
			
		||||
        </p>
 | 
			
		||||
      </div>
 | 
			
		||||
 | 
			
		||||
      <div class="section-header">Wat does Home Assistant Cast do?</div>
 | 
			
		||||
      <div class="section-header">What does Home Assistant Cast do?</div>
 | 
			
		||||
      <div class="card-content">
 | 
			
		||||
        <p>
 | 
			
		||||
          Home Assistant Cast is a receiver application for the Chromecast. When
 | 
			
		||||
 
 | 
			
		||||
@@ -36,6 +36,7 @@ import { HassElement } from "../../../../src/state/hass-element";
 | 
			
		||||
import { castContext } from "../cast_context";
 | 
			
		||||
import "./hc-launch-screen";
 | 
			
		||||
import { getPanelTitleFromUrlPath } from "../../../../src/data/panel";
 | 
			
		||||
import { checkLovelaceConfig } from "../../../../src/panels/lovelace/common/check-lovelace-config";
 | 
			
		||||
 | 
			
		||||
const DEFAULT_CONFIG: LovelaceDashboardStrategyConfig = {
 | 
			
		||||
  strategy: {
 | 
			
		||||
@@ -365,7 +366,9 @@ export class HcMain extends HassElement {
 | 
			
		||||
      this._urlPath || "lovelace"
 | 
			
		||||
    );
 | 
			
		||||
    castContext.setApplicationState(title || "");
 | 
			
		||||
    this._lovelaceConfig = lovelaceConfig;
 | 
			
		||||
    this._lovelaceConfig = checkLovelaceConfig(
 | 
			
		||||
      lovelaceConfig
 | 
			
		||||
    ) as LovelaceConfig;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _handleShowDemo(_msg: ShowDemoMessage) {
 | 
			
		||||
 
 | 
			
		||||
@@ -111,9 +111,47 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
 | 
			
		||||
        friendly_name: "Living room Temperature",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "sensor.living_room_humidity": {
 | 
			
		||||
      entity_id: "sensor.living_room_humidity",
 | 
			
		||||
      state: "57",
 | 
			
		||||
      attributes: {
 | 
			
		||||
        state_class: "measurement",
 | 
			
		||||
        unit_of_measurement: "%",
 | 
			
		||||
        device_class: "humidity",
 | 
			
		||||
        friendly_name: "Living room Humidity",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "sensor.outdoor_temperature": {
 | 
			
		||||
      entity_id: "sensor.outdoor_temperature",
 | 
			
		||||
      state: "10.5",
 | 
			
		||||
      attributes: {
 | 
			
		||||
        state_class: "measurement",
 | 
			
		||||
        unit_of_measurement: "°C",
 | 
			
		||||
        device_class: "temperature",
 | 
			
		||||
        friendly_name: "Outdoor temperature",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "sensor.outdoor_humidity": {
 | 
			
		||||
      entity_id: "sensor.outdoor_humidity",
 | 
			
		||||
      state: "70.4",
 | 
			
		||||
      attributes: {
 | 
			
		||||
        state_class: "measurement",
 | 
			
		||||
        unit_of_measurement: "%",
 | 
			
		||||
        device_class: "humidity",
 | 
			
		||||
        friendly_name: "Outdoor humidity",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "device_tracker.car": {
 | 
			
		||||
      entity_id: "sensor.outdoor_humidity",
 | 
			
		||||
      state: "not_home",
 | 
			
		||||
      attributes: {
 | 
			
		||||
        friendly_name: "Car",
 | 
			
		||||
        icon: "mdi:car",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "media_player.living_room_nest_mini": {
 | 
			
		||||
      entity_id: "media_player.living_room_nest_mini",
 | 
			
		||||
      state: "on",
 | 
			
		||||
      state: "playing",
 | 
			
		||||
      attributes: {
 | 
			
		||||
        device_class: "speaker",
 | 
			
		||||
        volume_level: 0.18,
 | 
			
		||||
@@ -161,6 +199,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
 | 
			
		||||
        supported_features: 32,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "binary_sensor.kitchen_motion": {
 | 
			
		||||
      entity_id: "light.kitchen_motion",
 | 
			
		||||
      state: "on",
 | 
			
		||||
      attributes: {
 | 
			
		||||
        device_class: "motion",
 | 
			
		||||
        friendly_name: "Kitchen motion",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "light.worktop_spotlights": {
 | 
			
		||||
      entity_id: "light.worktop_spotlights",
 | 
			
		||||
      state: "off",
 | 
			
		||||
@@ -395,6 +441,14 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) =>
 | 
			
		||||
        supported_features: 64063,
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "switch.in_meeting": {
 | 
			
		||||
      entity_id: "switch.in_meeting",
 | 
			
		||||
      state: "on",
 | 
			
		||||
      attributes: {
 | 
			
		||||
        icon: "mdi:laptop-account",
 | 
			
		||||
        friendly_name: "In a meeting",
 | 
			
		||||
      },
 | 
			
		||||
    },
 | 
			
		||||
    "sensor.standing_desk_height": {
 | 
			
		||||
      entity_id: "sensor.standing_desk_height",
 | 
			
		||||
      state: "72",
 | 
			
		||||
 
 | 
			
		||||
@@ -9,17 +9,57 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
      title: isFrontpageEmbed ? "Home Assistant" : "Demo",
 | 
			
		||||
      path: "home",
 | 
			
		||||
      icon: "mdi:home-assistant",
 | 
			
		||||
      badges: [
 | 
			
		||||
        {
 | 
			
		||||
          type: "entity",
 | 
			
		||||
          entity: "sensor.outdoor_temperature",
 | 
			
		||||
          color: "red",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "entity",
 | 
			
		||||
          entity: "sensor.outdoor_humidity",
 | 
			
		||||
          color: "indigo",
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "entity",
 | 
			
		||||
          entity: "device_tracker.car",
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      sections: [
 | 
			
		||||
        ...(isFrontpageEmbed
 | 
			
		||||
          ? []
 | 
			
		||||
          : [
 | 
			
		||||
              {
 | 
			
		||||
                title: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
 | 
			
		||||
                cards: [{ type: "custom:ha-demo-card" }],
 | 
			
		||||
                cards: [
 | 
			
		||||
                  {
 | 
			
		||||
                    type: "heading",
 | 
			
		||||
                    heading: `${localize("ui.panel.page-demo.config.sections.titles.welcome")} 👋`,
 | 
			
		||||
                  },
 | 
			
		||||
                  { type: "custom:ha-demo-card" },
 | 
			
		||||
                ],
 | 
			
		||||
              },
 | 
			
		||||
            ]),
 | 
			
		||||
        {
 | 
			
		||||
          cards: [
 | 
			
		||||
            {
 | 
			
		||||
              type: "heading",
 | 
			
		||||
              heading: localize(
 | 
			
		||||
                "ui.panel.page-demo.config.sections.titles.living_room"
 | 
			
		||||
              ),
 | 
			
		||||
              icon: "mdi:sofa",
 | 
			
		||||
              badges: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: "entity",
 | 
			
		||||
                  entity: "sensor.living_room_temperature",
 | 
			
		||||
                  color: "red",
 | 
			
		||||
                },
 | 
			
		||||
                {
 | 
			
		||||
                  type: "entity",
 | 
			
		||||
                  entity: "sensor.living_room_humidity",
 | 
			
		||||
                  color: "indigo",
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "light.floor_lamp",
 | 
			
		||||
@@ -38,13 +78,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "light.bar_lamp",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              graph: "line",
 | 
			
		||||
              type: "sensor",
 | 
			
		||||
              entity: "sensor.living_room_temperature",
 | 
			
		||||
              detail: 1,
 | 
			
		||||
              name: "Temperature",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "cover.living_room_garden_shutter",
 | 
			
		||||
@@ -55,11 +88,25 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
              entity: "media_player.living_room_nest_mini",
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          title: `🛋️ ${localize("ui.panel.page-demo.config.sections.titles.living_room")} `,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "grid",
 | 
			
		||||
          cards: [
 | 
			
		||||
            {
 | 
			
		||||
              type: "heading",
 | 
			
		||||
              heading: localize(
 | 
			
		||||
                "ui.panel.page-demo.config.sections.titles.kitchen"
 | 
			
		||||
              ),
 | 
			
		||||
              icon: "mdi:fridge",
 | 
			
		||||
              badges: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: "entity",
 | 
			
		||||
                  entity: "binary_sensor.kitchen_motion",
 | 
			
		||||
                  show_state: false,
 | 
			
		||||
                  color: "blue",
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "cover.kitchen_shutter",
 | 
			
		||||
@@ -90,11 +137,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
              entity: "media_player.kitchen_nest_audio",
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          title: `👩🍳 ${localize("ui.panel.page-demo.config.sections.titles.kitchen")}`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "grid",
 | 
			
		||||
          cards: [
 | 
			
		||||
            {
 | 
			
		||||
              type: "heading",
 | 
			
		||||
              heading: localize(
 | 
			
		||||
                "ui.panel.page-demo.config.sections.titles.energy"
 | 
			
		||||
              ),
 | 
			
		||||
              icon: "mdi:transmission-tower",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "binary_sensor.tesla_wall_connector_vehicle_connected",
 | 
			
		||||
@@ -132,11 +185,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
              color: "dark-grey",
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          title: `⚡️ ${localize("ui.panel.page-demo.config.sections.titles.energy")}`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "grid",
 | 
			
		||||
          cards: [
 | 
			
		||||
            {
 | 
			
		||||
              type: "heading",
 | 
			
		||||
              heading: localize(
 | 
			
		||||
                "ui.panel.page-demo.config.sections.titles.climate"
 | 
			
		||||
              ),
 | 
			
		||||
              icon: "mdi:thermometer",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "sun.sun",
 | 
			
		||||
@@ -169,16 +228,38 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
              state_content: ["preset_mode", "current_temperature"],
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          title: `🌤️ ${localize("ui.panel.page-demo.config.sections.titles.climate")}`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "grid",
 | 
			
		||||
          cards: [
 | 
			
		||||
            {
 | 
			
		||||
              type: "heading",
 | 
			
		||||
              heading: localize(
 | 
			
		||||
                "ui.panel.page-demo.config.sections.titles.study"
 | 
			
		||||
              ),
 | 
			
		||||
              icon: "mdi:desk-lamp",
 | 
			
		||||
              badges: [
 | 
			
		||||
                {
 | 
			
		||||
                  type: "entity",
 | 
			
		||||
                  entity: "switch.in_meeting",
 | 
			
		||||
                  state: "on",
 | 
			
		||||
                  state_content: "name",
 | 
			
		||||
                  visibility: [
 | 
			
		||||
                    {
 | 
			
		||||
                      condition: "state",
 | 
			
		||||
                      state: "on",
 | 
			
		||||
                      entity: "switch.in_meeting",
 | 
			
		||||
                    },
 | 
			
		||||
                  ],
 | 
			
		||||
                },
 | 
			
		||||
              ],
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "cover.study_shutter",
 | 
			
		||||
              name: "Shutter",
 | 
			
		||||
            },
 | 
			
		||||
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "light.study_spotlights",
 | 
			
		||||
@@ -195,12 +276,23 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
              color: "brown",
 | 
			
		||||
              icon: "mdi:desk",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "switch.in_meeting",
 | 
			
		||||
              name: "Meeting mode",
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          title: `🧑💻 ${localize("ui.panel.page-demo.config.sections.titles.study")}`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "grid",
 | 
			
		||||
          cards: [
 | 
			
		||||
            {
 | 
			
		||||
              type: "heading",
 | 
			
		||||
              heading: localize(
 | 
			
		||||
                "ui.panel.page-demo.config.sections.titles.outdoor"
 | 
			
		||||
              ),
 | 
			
		||||
              icon: "mdi:tree",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "light.outdoor_light",
 | 
			
		||||
@@ -230,11 +322,17 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
              name: "Illuminance",
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          title: `🌳 ${localize("ui.panel.page-demo.config.sections.titles.outdoor")}`,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          type: "grid",
 | 
			
		||||
          cards: [
 | 
			
		||||
            {
 | 
			
		||||
              type: "heading",
 | 
			
		||||
              heading: localize(
 | 
			
		||||
                "ui.panel.page-demo.config.sections.titles.updates"
 | 
			
		||||
              ),
 | 
			
		||||
              icon: "mdi:update",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              type: "tile",
 | 
			
		||||
              entity: "automation.home_assistant_auto_update",
 | 
			
		||||
@@ -260,7 +358,6 @@ export const demoLovelaceSections: DemoConfig["lovelace"] = (localize) => ({
 | 
			
		||||
              icon: "mdi:home-assistant",
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          title: `🎉 ${localize("ui.panel.page-demo.config.sections.titles.updates")}`,
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										9
									
								
								demo/src/stubs/config.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,9 @@
 | 
			
		||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
 | 
			
		||||
 | 
			
		||||
export const mockConfig = (hass: MockHomeAssistant) => {
 | 
			
		||||
  hass.mockWS("validate_config", () => ({
 | 
			
		||||
    actions: { valid: true },
 | 
			
		||||
    conditions: { valid: true },
 | 
			
		||||
    triggers: { valid: true },
 | 
			
		||||
  }));
 | 
			
		||||
};
 | 
			
		||||
							
								
								
									
										6
									
								
								demo/src/stubs/tags.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,6 @@
 | 
			
		||||
import { Tag } from "../../../src/data/tag";
 | 
			
		||||
import { MockHomeAssistant } from "../../../src/fake_data/provide_hass";
 | 
			
		||||
 | 
			
		||||
export const mockTags = (hass: MockHomeAssistant) => {
 | 
			
		||||
  hass.mockWS("tag/list", () => [{ id: "my-tag", name: "My Tag" }] as Tag[]);
 | 
			
		||||
};
 | 
			
		||||
@@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = {
 | 
			
		||||
      id: "1615419646544",
 | 
			
		||||
      alias: "Ensure Party mode",
 | 
			
		||||
      description: "",
 | 
			
		||||
      trigger: [
 | 
			
		||||
      triggers: [
 | 
			
		||||
        {
 | 
			
		||||
          platform: "state",
 | 
			
		||||
          trigger: "state",
 | 
			
		||||
          entity_id: "input_boolean.toggle_1",
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      condition: [
 | 
			
		||||
      conditions: [
 | 
			
		||||
        {
 | 
			
		||||
          condition: "template",
 | 
			
		||||
          alias: "Test if Paulus is home",
 | 
			
		||||
          value_template: "{{ true }}",
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      action: [
 | 
			
		||||
      actions: [
 | 
			
		||||
        {
 | 
			
		||||
          service: "input_boolean.toggle",
 | 
			
		||||
          action: "input_boolean.toggle",
 | 
			
		||||
          target: {
 | 
			
		||||
            entity_id: "input_boolean.toggle_4",
 | 
			
		||||
          },
 | 
			
		||||
@@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = {
 | 
			
		||||
          ],
 | 
			
		||||
          default: [
 | 
			
		||||
            {
 | 
			
		||||
              service: "input_boolean.toggle",
 | 
			
		||||
              action: "input_boolean.toggle",
 | 
			
		||||
              alias: "Toggle 2",
 | 
			
		||||
              target: {
 | 
			
		||||
                entity_id: "input_boolean.toggle_2",
 | 
			
		||||
@@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = {
 | 
			
		||||
          ],
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          service: "input_boolean.toggle",
 | 
			
		||||
          action: "input_boolean.toggle",
 | 
			
		||||
          target: {
 | 
			
		||||
            entity_id: "input_boolean.toggle_4",
 | 
			
		||||
          },
 | 
			
		||||
 
 | 
			
		||||
@@ -31,8 +31,8 @@ export const mockDemoTrace = (
 | 
			
		||||
      ],
 | 
			
		||||
    },
 | 
			
		||||
    config: {
 | 
			
		||||
      trigger: [],
 | 
			
		||||
      action: [],
 | 
			
		||||
      triggers: [],
 | 
			
		||||
      actions: [],
 | 
			
		||||
    },
 | 
			
		||||
    context: {
 | 
			
		||||
      id: "abcd",
 | 
			
		||||
 
 | 
			
		||||
@@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = {
 | 
			
		||||
    config: {
 | 
			
		||||
      mode: "restart",
 | 
			
		||||
      max_exceeded: "silent",
 | 
			
		||||
      trigger: [
 | 
			
		||||
      triggers: [
 | 
			
		||||
        {
 | 
			
		||||
          platform: "state",
 | 
			
		||||
          trigger: "state",
 | 
			
		||||
          entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use",
 | 
			
		||||
          from: "off",
 | 
			
		||||
          to: "on",
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
      action: [
 | 
			
		||||
      actions: [
 | 
			
		||||
        {
 | 
			
		||||
          service: "light.turn_on",
 | 
			
		||||
          action: "light.turn_on",
 | 
			
		||||
          target: {
 | 
			
		||||
            entity_id: "light.elgato_key_light_air",
 | 
			
		||||
          },
 | 
			
		||||
@@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = {
 | 
			
		||||
          delay: 0,
 | 
			
		||||
        },
 | 
			
		||||
        {
 | 
			
		||||
          service: "light.turn_off",
 | 
			
		||||
          action: "light.turn_off",
 | 
			
		||||
          target: {
 | 
			
		||||
            entity_id: "light.elgato_key_light_air",
 | 
			
		||||
          },
 | 
			
		||||
 
 | 
			
		||||
@@ -48,7 +48,7 @@ const ACTIONS = [
 | 
			
		||||
  {
 | 
			
		||||
    wait_for_trigger: [
 | 
			
		||||
      {
 | 
			
		||||
        platform: "state",
 | 
			
		||||
        trigger: "state",
 | 
			
		||||
        entity_id: "input_boolean.toggle_1",
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
@@ -121,7 +121,7 @@ const ACTIONS = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const initialAction: Action = {
 | 
			
		||||
  service: "light.turn_on",
 | 
			
		||||
  action: "light.turn_on",
 | 
			
		||||
  target: {
 | 
			
		||||
    entity_id: "light.kitchen",
 | 
			
		||||
  },
 | 
			
		||||
@@ -142,7 +142,7 @@ export class DemoAutomationDescribeAction extends LitElement {
 | 
			
		||||
        <div class="action">
 | 
			
		||||
          <span>
 | 
			
		||||
            ${this._action
 | 
			
		||||
              ? describeAction(this.hass, [], [], [], this._action)
 | 
			
		||||
              ? describeAction(this.hass, [], [], this._action)
 | 
			
		||||
              : "<invalid YAML>"}
 | 
			
		||||
          </span>
 | 
			
		||||
          <ha-yaml-editor
 | 
			
		||||
@@ -155,7 +155,7 @@ export class DemoAutomationDescribeAction extends LitElement {
 | 
			
		||||
        ${ACTIONS.map(
 | 
			
		||||
          (conf) => html`
 | 
			
		||||
            <div class="action">
 | 
			
		||||
              <span>${describeAction(this.hass, [], [], [], conf as any)}</span>
 | 
			
		||||
              <span>${describeAction(this.hass, [], [], conf as any)}</span>
 | 
			
		||||
              <pre>${dump(conf)}</pre>
 | 
			
		||||
            </div>
 | 
			
		||||
          `
 | 
			
		||||
 
 | 
			
		||||
@@ -22,46 +22,52 @@ const ENTITIES = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const triggers = [
 | 
			
		||||
  { platform: "state", entity_id: "light.kitchen", from: "off", to: "on" },
 | 
			
		||||
  { platform: "mqtt" },
 | 
			
		||||
  { trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" },
 | 
			
		||||
  { trigger: "mqtt" },
 | 
			
		||||
  {
 | 
			
		||||
    platform: "geo_location",
 | 
			
		||||
    trigger: "geo_location",
 | 
			
		||||
    source: "test_source",
 | 
			
		||||
    zone: "zone.home",
 | 
			
		||||
    event: "enter",
 | 
			
		||||
  },
 | 
			
		||||
  { platform: "homeassistant", event: "start" },
 | 
			
		||||
  { trigger: "homeassistant", event: "start" },
 | 
			
		||||
  {
 | 
			
		||||
    platform: "numeric_state",
 | 
			
		||||
    trigger: "numeric_state",
 | 
			
		||||
    entity_id: "light.kitchen",
 | 
			
		||||
    attribute: "brightness",
 | 
			
		||||
    below: 80,
 | 
			
		||||
    above: 20,
 | 
			
		||||
  },
 | 
			
		||||
  { platform: "sun", event: "sunset" },
 | 
			
		||||
  { platform: "time_pattern" },
 | 
			
		||||
  { platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
 | 
			
		||||
  { platform: "webhook" },
 | 
			
		||||
  { platform: "persistent_notification" },
 | 
			
		||||
  { trigger: "sun", event: "sunset" },
 | 
			
		||||
  { trigger: "time_pattern" },
 | 
			
		||||
  { trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" },
 | 
			
		||||
  { trigger: "webhook" },
 | 
			
		||||
  { trigger: "persistent_notification" },
 | 
			
		||||
  {
 | 
			
		||||
    platform: "zone",
 | 
			
		||||
    trigger: "zone",
 | 
			
		||||
    entity_id: "person.person",
 | 
			
		||||
    zone: "zone.home",
 | 
			
		||||
    event: "enter",
 | 
			
		||||
  },
 | 
			
		||||
  { platform: "tag" },
 | 
			
		||||
  { platform: "time", at: "15:32" },
 | 
			
		||||
  { platform: "template" },
 | 
			
		||||
  { platform: "conversation", command: "Turn on the lights" },
 | 
			
		||||
  { trigger: "tag" },
 | 
			
		||||
  { trigger: "time", at: "15:32" },
 | 
			
		||||
  { trigger: "template" },
 | 
			
		||||
  { trigger: "conversation", command: "Turn on the lights" },
 | 
			
		||||
  {
 | 
			
		||||
    platform: "conversation",
 | 
			
		||||
    trigger: "conversation",
 | 
			
		||||
    command: ["Turn on the lights", "Turn the lights on"],
 | 
			
		||||
  },
 | 
			
		||||
  { platform: "event", event_type: "homeassistant_started" },
 | 
			
		||||
  { trigger: "event", event_type: "homeassistant_started" },
 | 
			
		||||
  {
 | 
			
		||||
    triggers: [
 | 
			
		||||
      { trigger: "state", entity_id: "light.kitchen", to: "on" },
 | 
			
		||||
      { trigger: "state", entity_id: "light.kitchen", to: "off" },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
const initialTrigger: Trigger = {
 | 
			
		||||
  platform: "state",
 | 
			
		||||
  trigger: "state",
 | 
			
		||||
  entity_id: "light.kitchen",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,9 @@ import { mockEntityRegistry } from "../../../../demo/src/stubs/entity_registry";
 | 
			
		||||
import { mockDeviceRegistry } from "../../../../demo/src/stubs/device_registry";
 | 
			
		||||
import { mockAreaRegistry } from "../../../../demo/src/stubs/area_registry";
 | 
			
		||||
import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervisor";
 | 
			
		||||
import { mockConfig } from "../../../../demo/src/stubs/config";
 | 
			
		||||
import { mockTags } from "../../../../demo/src/stubs/tags";
 | 
			
		||||
import { mockAuth } from "../../../../demo/src/stubs/auth";
 | 
			
		||||
import type { Trigger } from "../../../../src/data/automation";
 | 
			
		||||
import { HaGeolocationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-geo_location";
 | 
			
		||||
import { HaEventTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-event";
 | 
			
		||||
@@ -26,6 +29,7 @@ import { HaStateTrigger } from "../../../../src/panels/config/automation/trigger
 | 
			
		||||
import { HaMQTTTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-mqtt";
 | 
			
		||||
import "../../../../src/panels/config/automation/trigger/ha-automation-trigger";
 | 
			
		||||
import { HaConversationTrigger } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-conversation";
 | 
			
		||||
import { HaTriggerList } from "../../../../src/panels/config/automation/trigger/types/ha-automation-trigger-list";
 | 
			
		||||
 | 
			
		||||
const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
 | 
			
		||||
  {
 | 
			
		||||
@@ -111,11 +115,15 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [
 | 
			
		||||
    triggers: [
 | 
			
		||||
      { ...HaConversationTrigger.defaultConfig },
 | 
			
		||||
      {
 | 
			
		||||
        platform: "conversation",
 | 
			
		||||
        trigger: "conversation",
 | 
			
		||||
        command: ["Turn on the lights", "Turn the lights on"],
 | 
			
		||||
      },
 | 
			
		||||
    ],
 | 
			
		||||
  },
 | 
			
		||||
  {
 | 
			
		||||
    name: "Trigger list",
 | 
			
		||||
    triggers: [{ ...HaTriggerList.defaultConfig }],
 | 
			
		||||
  },
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
@customElement("demo-automation-editor-trigger")
 | 
			
		||||
@@ -135,6 +143,9 @@ export class DemoAutomationEditorTrigger extends LitElement {
 | 
			
		||||
    mockDeviceRegistry(hass);
 | 
			
		||||
    mockAreaRegistry(hass);
 | 
			
		||||
    mockHassioSupervisor(hass);
 | 
			
		||||
    mockConfig(hass);
 | 
			
		||||
    mockTags(hass);
 | 
			
		||||
    mockAuth(hass);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render(): TemplateResult {
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ import { LocalizeFunc } from "../../../src/common/translations/localize";
 | 
			
		||||
import "../../../src/components/ha-checkbox";
 | 
			
		||||
import "../../../src/components/ha-formfield";
 | 
			
		||||
import "../../../src/components/ha-textfield";
 | 
			
		||||
import "../../../src/components/ha-password-field";
 | 
			
		||||
import "../../../src/components/ha-radio";
 | 
			
		||||
import type { HaRadio } from "../../../src/components/ha-radio";
 | 
			
		||||
import {
 | 
			
		||||
@@ -261,23 +262,21 @@ export class SupervisorBackupContent extends LitElement {
 | 
			
		||||
        : ""}
 | 
			
		||||
      ${this.backupHasPassword
 | 
			
		||||
        ? html`
 | 
			
		||||
            <ha-textfield
 | 
			
		||||
            <ha-password-field
 | 
			
		||||
              .label=${this._localize("password")}
 | 
			
		||||
              type="password"
 | 
			
		||||
              name="backupPassword"
 | 
			
		||||
              .value=${this.backupPassword}
 | 
			
		||||
              @change=${this._handleTextValueChanged}
 | 
			
		||||
            >
 | 
			
		||||
            </ha-textfield>
 | 
			
		||||
            </ha-password-field>
 | 
			
		||||
            ${!this.backup
 | 
			
		||||
              ? html`<ha-textfield
 | 
			
		||||
              ? html`<ha-password-field
 | 
			
		||||
                  .label=${this._localize("confirm_password")}
 | 
			
		||||
                  type="password"
 | 
			
		||||
                  name="confirmBackupPassword"
 | 
			
		||||
                  .value=${this.confirmBackupPassword}
 | 
			
		||||
                  @change=${this._handleTextValueChanged}
 | 
			
		||||
                >
 | 
			
		||||
                </ha-textfield>`
 | 
			
		||||
                </ha-password-field>`
 | 
			
		||||
              : ""}
 | 
			
		||||
          `
 | 
			
		||||
        : ""}
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,12 @@ import "../../../../src/components/ha-circular-progress";
 | 
			
		||||
import "../../../../src/components/ha-dialog";
 | 
			
		||||
import "../../../../src/components/ha-expansion-panel";
 | 
			
		||||
import "../../../../src/components/ha-formfield";
 | 
			
		||||
import "../../../../src/components/ha-textfield";
 | 
			
		||||
import "../../../../src/components/ha-header-bar";
 | 
			
		||||
import "../../../../src/components/ha-icon-button";
 | 
			
		||||
import "../../../../src/components/ha-password-field";
 | 
			
		||||
import "../../../../src/components/ha-radio";
 | 
			
		||||
import "../../../../src/components/ha-textfield";
 | 
			
		||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
 | 
			
		||||
import { extractApiErrorMessage } from "../../../../src/data/hassio/common";
 | 
			
		||||
import {
 | 
			
		||||
  AccessPoints,
 | 
			
		||||
@@ -34,7 +36,6 @@ import { HassDialog } from "../../../../src/dialogs/make-dialog-manager";
 | 
			
		||||
import { haStyleDialog } from "../../../../src/resources/styles";
 | 
			
		||||
import type { HomeAssistant } from "../../../../src/types";
 | 
			
		||||
import { HassioNetworkDialogParams } from "./show-dialog-network";
 | 
			
		||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
 | 
			
		||||
 | 
			
		||||
const IP_VERSIONS = ["ipv4", "ipv6"];
 | 
			
		||||
 | 
			
		||||
@@ -246,9 +247,8 @@ export class DialogHassioNetwork
 | 
			
		||||
                      ${this._wifiConfiguration.auth === "wpa-psk" ||
 | 
			
		||||
                      this._wifiConfiguration.auth === "wep"
 | 
			
		||||
                        ? html`
 | 
			
		||||
                            <ha-textfield
 | 
			
		||||
                            <ha-password-field
 | 
			
		||||
                              class="flex-auto"
 | 
			
		||||
                              type="password"
 | 
			
		||||
                              id="psk"
 | 
			
		||||
                              .label=${this.supervisor.localize(
 | 
			
		||||
                                "dialog.network.wifi_password"
 | 
			
		||||
@@ -256,7 +256,7 @@ export class DialogHassioNetwork
 | 
			
		||||
                              version="wifi"
 | 
			
		||||
                              @change=${this._handleInputValueChangedWifi}
 | 
			
		||||
                            >
 | 
			
		||||
                            </ha-textfield>
 | 
			
		||||
                            </ha-password-field>
 | 
			
		||||
                          `
 | 
			
		||||
                        : ""}
 | 
			
		||||
                    `
 | 
			
		||||
 
 | 
			
		||||
@@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types";
 | 
			
		||||
import { HassioRepositoryDialogParams } from "./show-dialog-repositories";
 | 
			
		||||
import type { HaTextField } from "../../../../src/components/ha-textfield";
 | 
			
		||||
import "../../../../src/components/ha-textfield";
 | 
			
		||||
import "../../../../src/components/ha-list-new";
 | 
			
		||||
import "../../../../src/components/ha-list-item-new";
 | 
			
		||||
import "../../../../src/components/ha-md-list";
 | 
			
		||||
import "../../../../src/components/ha-md-list-item";
 | 
			
		||||
 | 
			
		||||
@customElement("dialog-hassio-repositories")
 | 
			
		||||
class HassioRepositoriesDialog extends LitElement {
 | 
			
		||||
@@ -107,11 +107,11 @@ class HassioRepositoriesDialog extends LitElement {
 | 
			
		||||
          ? html`<ha-alert alert-type="error">${this._error}</ha-alert>`
 | 
			
		||||
          : ""}
 | 
			
		||||
        <div class="form">
 | 
			
		||||
          <ha-list-new>
 | 
			
		||||
          <ha-md-list>
 | 
			
		||||
            ${repositories.length
 | 
			
		||||
              ? repositories.map(
 | 
			
		||||
                  (repo) => html`
 | 
			
		||||
                    <ha-list-item-new class="option">
 | 
			
		||||
                    <ha-md-list-item class="option">
 | 
			
		||||
                      ${repo.name}
 | 
			
		||||
                      <div slot="supporting-text">
 | 
			
		||||
                        <div>${repo.maintainer}</div>
 | 
			
		||||
@@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement {
 | 
			
		||||
                          )}
 | 
			
		||||
                        </simple-tooltip>
 | 
			
		||||
                      </div>
 | 
			
		||||
                    </ha-list-item-new>
 | 
			
		||||
                    </ha-md-list-item>
 | 
			
		||||
                  `
 | 
			
		||||
                )
 | 
			
		||||
              : html`<ha-list-item-new> No repositories </ha-list-item-new>`}
 | 
			
		||||
          </ha-list-new>
 | 
			
		||||
              : html`<ha-md-list-item> No repositories </ha-md-list-item>`}
 | 
			
		||||
          </ha-md-list>
 | 
			
		||||
          <div class="layout horizontal bottom">
 | 
			
		||||
            <ha-textfield
 | 
			
		||||
              class="flex-auto"
 | 
			
		||||
@@ -209,7 +209,7 @@ class HassioRepositoriesDialog extends LitElement {
 | 
			
		||||
        div.delete ha-icon-button {
 | 
			
		||||
          color: var(--error-color);
 | 
			
		||||
        }
 | 
			
		||||
        ha-list-item-new {
 | 
			
		||||
        ha-md-list-item {
 | 
			
		||||
          position: relative;
 | 
			
		||||
        }
 | 
			
		||||
      `,
 | 
			
		||||
 
 | 
			
		||||
@@ -13,10 +13,11 @@
 | 
			
		||||
      <% for (const entry of es5EntryJS) { %>
 | 
			
		||||
        loadES5("<%= entry %>");
 | 
			
		||||
      <% } %>
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    <% for (const entry of es5EntryJS) { %>
 | 
			
		||||
      loadES5("<%= entry %>");
 | 
			
		||||
    <% } %>
 | 
			
		||||
  }
 | 
			
		||||
  }
 | 
			
		||||
})();
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										78
									
								
								package.json
									
									
									
									
									
								
							
							
						
						@@ -25,15 +25,15 @@
 | 
			
		||||
  "license": "Apache-2.0",
 | 
			
		||||
  "type": "module",
 | 
			
		||||
  "dependencies": {
 | 
			
		||||
    "@babel/runtime": "7.25.6",
 | 
			
		||||
    "@babel/runtime": "7.25.7",
 | 
			
		||||
    "@braintree/sanitize-url": "7.1.0",
 | 
			
		||||
    "@codemirror/autocomplete": "6.18.0",
 | 
			
		||||
    "@codemirror/commands": "6.6.1",
 | 
			
		||||
    "@codemirror/language": "6.10.2",
 | 
			
		||||
    "@codemirror/autocomplete": "6.18.1",
 | 
			
		||||
    "@codemirror/commands": "6.7.0",
 | 
			
		||||
    "@codemirror/language": "6.10.3",
 | 
			
		||||
    "@codemirror/legacy-modes": "6.4.1",
 | 
			
		||||
    "@codemirror/search": "6.5.6",
 | 
			
		||||
    "@codemirror/state": "6.4.1",
 | 
			
		||||
    "@codemirror/view": "6.33.0",
 | 
			
		||||
    "@codemirror/view": "6.34.1",
 | 
			
		||||
    "@egjs/hammerjs": "2.0.17",
 | 
			
		||||
    "@formatjs/intl-datetimeformat": "6.12.5",
 | 
			
		||||
    "@formatjs/intl-displaynames": "6.6.8",
 | 
			
		||||
@@ -80,16 +80,17 @@
 | 
			
		||||
    "@material/mwc-top-app-bar": "0.27.0",
 | 
			
		||||
    "@material/mwc-top-app-bar-fixed": "0.27.0",
 | 
			
		||||
    "@material/top-app-bar": "=14.0.0-canary.53b3cad2f.0",
 | 
			
		||||
    "@material/web": "2.1.0",
 | 
			
		||||
    "@material/web": "2.2.0",
 | 
			
		||||
    "@mdi/js": "7.4.47",
 | 
			
		||||
    "@mdi/svg": "7.4.47",
 | 
			
		||||
    "@polymer/paper-item": "3.0.1",
 | 
			
		||||
    "@polymer/paper-listbox": "3.0.1",
 | 
			
		||||
    "@polymer/paper-tabs": "3.1.0",
 | 
			
		||||
    "@polymer/polymer": "3.5.1",
 | 
			
		||||
    "@replit/codemirror-indentation-markers": "6.5.3",
 | 
			
		||||
    "@thomasloven/round-slider": "0.6.0",
 | 
			
		||||
    "@vaadin/combo-box": "24.4.7",
 | 
			
		||||
    "@vaadin/vaadin-themable-mixin": "24.4.7",
 | 
			
		||||
    "@vaadin/combo-box": "24.4.11",
 | 
			
		||||
    "@vaadin/vaadin-themable-mixin": "24.4.11",
 | 
			
		||||
    "@vibrant/color": "3.2.1-alpha.1",
 | 
			
		||||
    "@vibrant/core": "3.2.1-alpha.1",
 | 
			
		||||
    "@vibrant/quantizer-mmcq": "3.2.1-alpha.1",
 | 
			
		||||
@@ -102,10 +103,11 @@
 | 
			
		||||
    "comlink": "4.4.1",
 | 
			
		||||
    "core-js": "3.38.1",
 | 
			
		||||
    "cropperjs": "1.6.2",
 | 
			
		||||
    "date-fns": "3.6.0",
 | 
			
		||||
    "date-fns-tz": "3.1.3",
 | 
			
		||||
    "date-fns": "4.1.0",
 | 
			
		||||
    "date-fns-tz": "3.2.0",
 | 
			
		||||
    "deep-clone-simple": "1.1.1",
 | 
			
		||||
    "deep-freeze": "0.0.1",
 | 
			
		||||
    "dialog-polyfill": "0.5.6",
 | 
			
		||||
    "element-internals-polyfill": "1.3.11",
 | 
			
		||||
    "fuse.js": "7.0.0",
 | 
			
		||||
    "google-timezones-json": "1.2.0",
 | 
			
		||||
@@ -115,10 +117,10 @@
 | 
			
		||||
    "intl-messageformat": "10.5.14",
 | 
			
		||||
    "js-yaml": "4.1.0",
 | 
			
		||||
    "leaflet": "1.9.4",
 | 
			
		||||
    "leaflet-draw": "1.0.4",
 | 
			
		||||
    "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch",
 | 
			
		||||
    "lit": "2.8.0",
 | 
			
		||||
    "luxon": "3.5.0",
 | 
			
		||||
    "marked": "14.1.1",
 | 
			
		||||
    "marked": "14.1.2",
 | 
			
		||||
    "memoize-one": "6.0.0",
 | 
			
		||||
    "node-vibrant": "3.2.1-alpha.1",
 | 
			
		||||
    "proxy-polyfill": "0.3.2",
 | 
			
		||||
@@ -127,13 +129,13 @@
 | 
			
		||||
    "qrcode": "1.5.4",
 | 
			
		||||
    "roboto-fontface": "0.10.0",
 | 
			
		||||
    "rrule": "2.8.1",
 | 
			
		||||
    "sortablejs": "1.15.3",
 | 
			
		||||
    "sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
 | 
			
		||||
    "stacktrace-js": "2.0.2",
 | 
			
		||||
    "superstruct": "2.0.2",
 | 
			
		||||
    "tinykeys": "3.0.0",
 | 
			
		||||
    "tsparticles-engine": "2.12.0",
 | 
			
		||||
    "tsparticles-preset-links": "2.12.0",
 | 
			
		||||
    "ua-parser-js": "1.0.38",
 | 
			
		||||
    "ua-parser-js": "1.0.39",
 | 
			
		||||
    "unfetch": "5.0.0",
 | 
			
		||||
    "vis-data": "7.1.9",
 | 
			
		||||
    "vis-network": "9.1.9",
 | 
			
		||||
@@ -149,28 +151,28 @@
 | 
			
		||||
    "xss": "1.0.15"
 | 
			
		||||
  },
 | 
			
		||||
  "devDependencies": {
 | 
			
		||||
    "@babel/core": "7.25.2",
 | 
			
		||||
    "@babel/core": "7.25.7",
 | 
			
		||||
    "@babel/helper-define-polyfill-provider": "0.6.2",
 | 
			
		||||
    "@babel/plugin-proposal-decorators": "7.24.7",
 | 
			
		||||
    "@babel/plugin-transform-runtime": "7.25.4",
 | 
			
		||||
    "@babel/preset-env": "7.25.4",
 | 
			
		||||
    "@babel/preset-typescript": "7.24.7",
 | 
			
		||||
    "@bundle-stats/plugin-webpack-filter": "4.15.0",
 | 
			
		||||
    "@babel/plugin-proposal-decorators": "7.25.7",
 | 
			
		||||
    "@babel/plugin-transform-runtime": "7.25.7",
 | 
			
		||||
    "@babel/preset-env": "7.25.7",
 | 
			
		||||
    "@babel/preset-typescript": "7.25.7",
 | 
			
		||||
    "@bundle-stats/plugin-webpack-filter": "4.15.1",
 | 
			
		||||
    "@koa/cors": "5.0.0",
 | 
			
		||||
    "@lokalise/node-api": "12.7.0",
 | 
			
		||||
    "@octokit/auth-oauth-device": "7.1.1",
 | 
			
		||||
    "@octokit/plugin-retry": "7.1.1",
 | 
			
		||||
    "@octokit/plugin-retry": "7.1.2",
 | 
			
		||||
    "@octokit/rest": "21.0.2",
 | 
			
		||||
    "@open-wc/dev-server-hmr": "0.1.4",
 | 
			
		||||
    "@rollup/plugin-babel": "6.0.4",
 | 
			
		||||
    "@rollup/plugin-commonjs": "26.0.1",
 | 
			
		||||
    "@rollup/plugin-json": "6.1.0",
 | 
			
		||||
    "@rollup/plugin-node-resolve": "15.2.3",
 | 
			
		||||
    "@rollup/plugin-node-resolve": "15.2.4",
 | 
			
		||||
    "@rollup/plugin-replace": "5.0.7",
 | 
			
		||||
    "@types/babel__plugin-transform-runtime": "7.9.5",
 | 
			
		||||
    "@types/chromecast-caf-receiver": "6.0.17",
 | 
			
		||||
    "@types/chromecast-caf-sender": "1.0.10",
 | 
			
		||||
    "@types/color-name": "1.1.4",
 | 
			
		||||
    "@types/color-name": "2.0.0",
 | 
			
		||||
    "@types/glob": "8.1.0",
 | 
			
		||||
    "@types/html-minifier-terser": "7.0.2",
 | 
			
		||||
    "@types/js-yaml": "4.0.9",
 | 
			
		||||
@@ -189,21 +191,21 @@
 | 
			
		||||
    "@typescript-eslint/parser": "7.18.0",
 | 
			
		||||
    "@web/dev-server": "0.1.38",
 | 
			
		||||
    "@web/dev-server-rollup": "0.4.1",
 | 
			
		||||
    "babel-loader": "9.1.3",
 | 
			
		||||
    "babel-loader": "9.2.1",
 | 
			
		||||
    "babel-plugin-template-html-minifier": "4.1.0",
 | 
			
		||||
    "browserslist-useragent-regexp": "4.1.3",
 | 
			
		||||
    "chai": "5.1.1",
 | 
			
		||||
    "del": "7.1.0",
 | 
			
		||||
    "eslint": "8.57.0",
 | 
			
		||||
    "del": "8.0.0",
 | 
			
		||||
    "eslint": "8.57.1",
 | 
			
		||||
    "eslint-config-airbnb-base": "15.0.0",
 | 
			
		||||
    "eslint-config-airbnb-typescript": "18.0.0",
 | 
			
		||||
    "eslint-config-prettier": "9.1.0",
 | 
			
		||||
    "eslint-import-resolver-webpack": "0.13.9",
 | 
			
		||||
    "eslint-plugin-import": "2.30.0",
 | 
			
		||||
    "eslint-plugin-lit": "1.14.0",
 | 
			
		||||
    "eslint-plugin-import": "2.31.0",
 | 
			
		||||
    "eslint-plugin-lit": "1.15.0",
 | 
			
		||||
    "eslint-plugin-lit-a11y": "4.1.4",
 | 
			
		||||
    "eslint-plugin-unused-imports": "4.1.3",
 | 
			
		||||
    "eslint-plugin-wc": "2.1.1",
 | 
			
		||||
    "eslint-plugin-unused-imports": "4.1.4",
 | 
			
		||||
    "eslint-plugin-wc": "2.2.0",
 | 
			
		||||
    "fancy-log": "2.0.0",
 | 
			
		||||
    "fs-extra": "11.2.0",
 | 
			
		||||
    "glob": "11.0.0",
 | 
			
		||||
@@ -213,7 +215,7 @@
 | 
			
		||||
    "gulp-rename": "2.0.0",
 | 
			
		||||
    "gulp-zopfli-green": "6.0.2",
 | 
			
		||||
    "html-minifier-terser": "7.2.0",
 | 
			
		||||
    "husky": "9.1.5",
 | 
			
		||||
    "husky": "9.1.6",
 | 
			
		||||
    "instant-mocha": "1.5.2",
 | 
			
		||||
    "jszip": "3.10.1",
 | 
			
		||||
    "lint-staged": "15.2.10",
 | 
			
		||||
@@ -227,19 +229,19 @@
 | 
			
		||||
    "open": "10.1.0",
 | 
			
		||||
    "pinst": "3.0.0",
 | 
			
		||||
    "prettier": "3.3.3",
 | 
			
		||||
    "rollup": "2.79.1",
 | 
			
		||||
    "rollup": "2.79.2",
 | 
			
		||||
    "rollup-plugin-string": "3.0.0",
 | 
			
		||||
    "rollup-plugin-terser": "7.0.2",
 | 
			
		||||
    "rollup-plugin-visualizer": "5.12.0",
 | 
			
		||||
    "serve-handler": "6.1.5",
 | 
			
		||||
    "sinon": "18.0.0",
 | 
			
		||||
    "sinon": "19.0.2",
 | 
			
		||||
    "systemjs": "6.15.1",
 | 
			
		||||
    "tar": "7.4.3",
 | 
			
		||||
    "terser-webpack-plugin": "5.3.10",
 | 
			
		||||
    "transform-async-modules-webpack-plugin": "1.1.1",
 | 
			
		||||
    "ts-lit-plugin": "2.0.2",
 | 
			
		||||
    "typescript": "5.5.4",
 | 
			
		||||
    "webpack": "5.94.0",
 | 
			
		||||
    "typescript": "5.6.2",
 | 
			
		||||
    "webpack": "5.95.0",
 | 
			
		||||
    "webpack-cli": "5.1.4",
 | 
			
		||||
    "webpack-dev-server": "5.1.0",
 | 
			
		||||
    "webpack-manifest-plugin": "5.0.0",
 | 
			
		||||
@@ -254,9 +256,7 @@
 | 
			
		||||
    "lit": "2.8.0",
 | 
			
		||||
    "clean-css": "5.3.3",
 | 
			
		||||
    "@lit/reactive-element": "1.6.3",
 | 
			
		||||
    "@fullcalendar/daygrid": "6.1.15",
 | 
			
		||||
    "sortablejs@1.15.3": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch",
 | 
			
		||||
    "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch"
 | 
			
		||||
    "@fullcalendar/daygrid": "6.1.15"
 | 
			
		||||
  },
 | 
			
		||||
  "packageManager": "yarn@4.4.1"
 | 
			
		||||
  "packageManager": "yarn@4.5.0"
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										
											BIN
										
									
								
								public/static/icons/casita/loading.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 3.9 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/static/icons/casita/loving.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.5 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/static/icons/casita/normal.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.6 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/static/icons/casita/sad.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.3 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/static/icons/casita/sleeping.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/static/icons/casita/smiling.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 4.4 KiB  | 
							
								
								
									
										
											BIN
										
									
								
								public/static/images/logo_nabu_casa.png
									
									
									
									
									
										Normal file
									
								
							
							
						
						| 
		 After Width: | Height: | Size: 2.9 KiB  | 
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
 | 
			
		||||
 | 
			
		||||
[project]
 | 
			
		||||
name         = "home-assistant-frontend"
 | 
			
		||||
version      = "20240909.1"
 | 
			
		||||
version      = "20241010.0"
 | 
			
		||||
license      = {text = "Apache-2.0"}
 | 
			
		||||
description  = "The Home Assistant frontend"
 | 
			
		||||
readme       = "README.md"
 | 
			
		||||
 
 | 
			
		||||
@@ -18,5 +18,9 @@ if [[ -n "$DEVCONTAINER" ]]; then
 | 
			
		||||
    fi
 | 
			
		||||
fi
 | 
			
		||||
 | 
			
		||||
if ! command -v yarn &> /dev/null; then
 | 
			
		||||
    echo "Error: yarn not found. Please install it following the official instructions: https://yarnpkg.com/getting-started/install" >&2
 | 
			
		||||
    exit 1
 | 
			
		||||
fi
 | 
			
		||||
# Install node modules
 | 
			
		||||
    yarn install
 | 
			
		||||
yarn install
 | 
			
		||||
 
 | 
			
		||||
@@ -1,36 +1,36 @@
 | 
			
		||||
import { theme2hex } from "./convert-color";
 | 
			
		||||
 | 
			
		||||
export const COLORS = [
 | 
			
		||||
  "#44739e",
 | 
			
		||||
  "#984ea3",
 | 
			
		||||
  "#00d2d5",
 | 
			
		||||
  "#ff7f00",
 | 
			
		||||
  "#af8d00",
 | 
			
		||||
  "#7f80cd",
 | 
			
		||||
  "#b3e900",
 | 
			
		||||
  "#c42e60",
 | 
			
		||||
  "#a65628",
 | 
			
		||||
  "#f781bf",
 | 
			
		||||
  "#8dd3c7",
 | 
			
		||||
  "#bebada",
 | 
			
		||||
  "#fb8072",
 | 
			
		||||
  "#80b1d3",
 | 
			
		||||
  "#fdb462",
 | 
			
		||||
  "#fccde5",
 | 
			
		||||
  "#bc80bd",
 | 
			
		||||
  "#ffed6f",
 | 
			
		||||
  "#c4eaff",
 | 
			
		||||
  "#cf8c00",
 | 
			
		||||
  "#1b9e77",
 | 
			
		||||
  "#d95f02",
 | 
			
		||||
  "#e7298a",
 | 
			
		||||
  "#e6ab02",
 | 
			
		||||
  "#a6761d",
 | 
			
		||||
  "#0097ff",
 | 
			
		||||
  "#00d067",
 | 
			
		||||
  "#f43600",
 | 
			
		||||
  "#4ba93b",
 | 
			
		||||
  "#5779bb",
 | 
			
		||||
  "#4269d0",
 | 
			
		||||
  "#f4bd4a",
 | 
			
		||||
  "#ff725c",
 | 
			
		||||
  "#6cc5b0",
 | 
			
		||||
  "#a463f2",
 | 
			
		||||
  "#ff8ab7",
 | 
			
		||||
  "#9c6b4e",
 | 
			
		||||
  "#97bbf5",
 | 
			
		||||
  "#01ab63",
 | 
			
		||||
  "#9498a0",
 | 
			
		||||
  "#094bad",
 | 
			
		||||
  "#c99000",
 | 
			
		||||
  "#d84f3e",
 | 
			
		||||
  "#49a28f",
 | 
			
		||||
  "#048732",
 | 
			
		||||
  "#d96895",
 | 
			
		||||
  "#8043ce",
 | 
			
		||||
  "#7599d1",
 | 
			
		||||
  "#7a4c31",
 | 
			
		||||
  "#74787f",
 | 
			
		||||
  "#6989f4",
 | 
			
		||||
  "#ffd444",
 | 
			
		||||
  "#ff957c",
 | 
			
		||||
  "#8fe9d3",
 | 
			
		||||
  "#62cc71",
 | 
			
		||||
  "#ffadda",
 | 
			
		||||
  "#c884ff",
 | 
			
		||||
  "#badeff",
 | 
			
		||||
  "#bf8b6d",
 | 
			
		||||
  "#b6bac2",
 | 
			
		||||
  "#927acc",
 | 
			
		||||
  "#97ee3f",
 | 
			
		||||
  "#bf3947",
 | 
			
		||||
 
 | 
			
		||||
@@ -234,7 +234,12 @@ export const SENSOR_ENTITIES = [
 | 
			
		||||
  "weather",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const ASSIST_ENTITIES = ["conversation", "stt", "tts"];
 | 
			
		||||
export const ASSIST_ENTITIES = [
 | 
			
		||||
  "assist_satellite",
 | 
			
		||||
  "conversation",
 | 
			
		||||
  "stt",
 | 
			
		||||
  "tts",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
/** Domains that render an input element instead of a text value when displayed in a row.
 | 
			
		||||
 *  Those rows should then not show a cursor pointer when hovered (which would normally
 | 
			
		||||
 
 | 
			
		||||
@@ -20,6 +20,15 @@ function findNestedItem(
 | 
			
		||||
  }, obj);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
function updateNestedItem(obj: any, path: ItemPath): any {
 | 
			
		||||
  const lastKey = path.pop()!;
 | 
			
		||||
  const parent = findNestedItem(obj, path);
 | 
			
		||||
  parent[lastKey] = Array.isArray(parent[lastKey])
 | 
			
		||||
    ? [...parent[lastKey]]
 | 
			
		||||
    : [parent[lastKey]];
 | 
			
		||||
  return obj;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export function nestedArrayMove<A>(
 | 
			
		||||
  obj: A,
 | 
			
		||||
  oldIndex: number,
 | 
			
		||||
@@ -27,14 +36,18 @@ export function nestedArrayMove<A>(
 | 
			
		||||
  oldPath?: ItemPath,
 | 
			
		||||
  newPath?: ItemPath
 | 
			
		||||
): A {
 | 
			
		||||
  const newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
 | 
			
		||||
  let newObj = (Array.isArray(obj) ? [...obj] : { ...obj }) as A;
 | 
			
		||||
 | 
			
		||||
  if (oldPath) {
 | 
			
		||||
    newObj = updateNestedItem(newObj, [...oldPath]);
 | 
			
		||||
  }
 | 
			
		||||
  if (newPath) {
 | 
			
		||||
    newObj = updateNestedItem(newObj, [...newPath]);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const from = oldPath ? findNestedItem(newObj, oldPath) : newObj;
 | 
			
		||||
  const to = newPath ? findNestedItem(newObj, newPath, true) : newObj;
 | 
			
		||||
 | 
			
		||||
  if (!Array.isArray(from) || !Array.isArray(to)) {
 | 
			
		||||
    return obj;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const item = from.splice(oldIndex, 1)[0];
 | 
			
		||||
  to.splice(newIndex, 0, item);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -204,6 +204,29 @@ export class HaDataTable extends LitElement {
 | 
			
		||||
    this._checkedRowsChanged();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public select(ids: string[], clear?: boolean): void {
 | 
			
		||||
    if (clear) {
 | 
			
		||||
      this._checkedRows = [];
 | 
			
		||||
    }
 | 
			
		||||
    ids.forEach((id) => {
 | 
			
		||||
      const row = this._filteredData.find((data) => data[this.id] === id);
 | 
			
		||||
      if (row?.selectable !== false && !this._checkedRows.includes(id)) {
 | 
			
		||||
        this._checkedRows.push(id);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this._checkedRowsChanged();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public unselect(ids: string[]): void {
 | 
			
		||||
    ids.forEach((id) => {
 | 
			
		||||
      const index = this._checkedRows.indexOf(id);
 | 
			
		||||
      if (index > -1) {
 | 
			
		||||
        this._checkedRows.splice(index, 1);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
    this._checkedRowsChanged();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public connectedCallback() {
 | 
			
		||||
    super.connectedCallback();
 | 
			
		||||
    if (this._filteredData.length) {
 | 
			
		||||
@@ -1011,6 +1034,7 @@ export class HaDataTable extends LitElement {
 | 
			
		||||
          /* @noflip */
 | 
			
		||||
          padding-inline-end: initial;
 | 
			
		||||
          width: 60px;
 | 
			
		||||
          min-width: 60px;
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        .mdc-data-table__table {
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> {
 | 
			
		||||
      fetchDeviceTriggers,
 | 
			
		||||
      (deviceId?: string) => ({
 | 
			
		||||
        device_id: deviceId || "",
 | 
			
		||||
        platform: "device",
 | 
			
		||||
        trigger: "device",
 | 
			
		||||
        domain: "",
 | 
			
		||||
        entity_id: "",
 | 
			
		||||
      })
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
import type { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { isValidEntityId } from "../../common/entity/valid_entity_id";
 | 
			
		||||
import type { ValueChangedEvent, HomeAssistant } from "../../types";
 | 
			
		||||
import type { HomeAssistant, ValueChangedEvent } from "../../types";
 | 
			
		||||
import "./ha-entity-picker";
 | 
			
		||||
import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker";
 | 
			
		||||
 | 
			
		||||
@@ -98,10 +97,7 @@ class HaEntitiesPickerLight extends LitElement {
 | 
			
		||||
              .excludeEntities=${this.excludeEntities}
 | 
			
		||||
              .includeDeviceClasses=${this.includeDeviceClasses}
 | 
			
		||||
              .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
 | 
			
		||||
              .entityFilter=${this._getEntityFilter(
 | 
			
		||||
                this.value,
 | 
			
		||||
                this.entityFilter
 | 
			
		||||
              )}
 | 
			
		||||
              .entityFilter=${this.entityFilter}
 | 
			
		||||
              .value=${entityId}
 | 
			
		||||
              .label=${this.pickedEntityLabel}
 | 
			
		||||
              .disabled=${this.disabled}
 | 
			
		||||
@@ -118,10 +114,13 @@ class HaEntitiesPickerLight extends LitElement {
 | 
			
		||||
          .includeDomains=${this.includeDomains}
 | 
			
		||||
          .excludeDomains=${this.excludeDomains}
 | 
			
		||||
          .includeEntities=${this.includeEntities}
 | 
			
		||||
          .excludeEntities=${this.excludeEntities}
 | 
			
		||||
          .excludeEntities=${this._excludeEntities(
 | 
			
		||||
            this.value,
 | 
			
		||||
            this.excludeEntities
 | 
			
		||||
          )}
 | 
			
		||||
          .includeDeviceClasses=${this.includeDeviceClasses}
 | 
			
		||||
          .includeUnitOfMeasurement=${this.includeUnitOfMeasurement}
 | 
			
		||||
          .entityFilter=${this._getEntityFilter(this.value, this.entityFilter)}
 | 
			
		||||
          .entityFilter=${this.entityFilter}
 | 
			
		||||
          .label=${this.pickEntityLabel}
 | 
			
		||||
          .helper=${this.helper}
 | 
			
		||||
          .disabled=${this.disabled}
 | 
			
		||||
@@ -133,14 +132,16 @@ class HaEntitiesPickerLight extends LitElement {
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _getEntityFilter = memoizeOne(
 | 
			
		||||
  private _excludeEntities = memoizeOne(
 | 
			
		||||
    (
 | 
			
		||||
      value: string[] | undefined,
 | 
			
		||||
      entityFilter: HaEntityPickerEntityFilterFunc | undefined
 | 
			
		||||
    ): HaEntityPickerEntityFilterFunc =>
 | 
			
		||||
      (stateObj: HassEntity) =>
 | 
			
		||||
        (!value || !value.includes(stateObj.entity_id)) &&
 | 
			
		||||
        (!entityFilter || entityFilter(stateObj))
 | 
			
		||||
      excludeEntities: string[] | undefined
 | 
			
		||||
    ): string[] | undefined => {
 | 
			
		||||
      if (value === undefined) {
 | 
			
		||||
        return excludeEntities;
 | 
			
		||||
      }
 | 
			
		||||
      return [...(excludeEntities || []), ...value];
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
  private get _currentEntities() {
 | 
			
		||||
 
 | 
			
		||||
@@ -87,7 +87,7 @@ export class HaEntityPicker extends LitElement {
 | 
			
		||||
  public includeUnitOfMeasurement?: string[];
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * List of allowed entities to show. Will ignore all other filters.
 | 
			
		||||
   * List of allowed entities to show.
 | 
			
		||||
   * @type {Array}
 | 
			
		||||
   * @attr include-entities
 | 
			
		||||
   */
 | 
			
		||||
@@ -220,30 +220,13 @@ export class HaEntityPicker extends LitElement {
 | 
			
		||||
 | 
			
		||||
      if (includeEntities) {
 | 
			
		||||
        entityIds = entityIds.filter((entityId) =>
 | 
			
		||||
          this.includeEntities!.includes(entityId)
 | 
			
		||||
          includeEntities.includes(entityId)
 | 
			
		||||
        );
 | 
			
		||||
 | 
			
		||||
        return entityIds
 | 
			
		||||
          .map((key) => {
 | 
			
		||||
            const friendly_name = computeStateName(hass!.states[key]) || key;
 | 
			
		||||
            return {
 | 
			
		||||
              ...hass!.states[key],
 | 
			
		||||
              friendly_name,
 | 
			
		||||
              strings: [key, friendly_name],
 | 
			
		||||
            };
 | 
			
		||||
          })
 | 
			
		||||
          .sort((entityA, entityB) =>
 | 
			
		||||
            caseInsensitiveStringCompare(
 | 
			
		||||
              entityA.friendly_name,
 | 
			
		||||
              entityB.friendly_name,
 | 
			
		||||
              this.hass.locale.language
 | 
			
		||||
            )
 | 
			
		||||
          );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (excludeEntities) {
 | 
			
		||||
        entityIds = entityIds.filter(
 | 
			
		||||
          (entityId) => !excludeEntities!.includes(entityId)
 | 
			
		||||
          (entityId) => !excludeEntities.includes(entityId)
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -173,6 +173,7 @@ class HaEntityStatePicker extends LitElement {
 | 
			
		||||
              no-style
 | 
			
		||||
              @item-moved=${this._moveItem}
 | 
			
		||||
              .disabled=${this.disabled}
 | 
			
		||||
              filter="button.trailing.action"
 | 
			
		||||
            >
 | 
			
		||||
              <ha-chip-set>
 | 
			
		||||
                ${repeat(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
import { mdiTextureBox } from "@mdi/js";
 | 
			
		||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
			
		||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
			
		||||
import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { LitElement, PropertyValues, TemplateResult, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { styleMap } from "lit/directives/style-map";
 | 
			
		||||
@@ -20,12 +20,7 @@ import {
 | 
			
		||||
  getDeviceEntityDisplayLookup,
 | 
			
		||||
} from "../data/device_registry";
 | 
			
		||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
			
		||||
import {
 | 
			
		||||
  FloorRegistryEntry,
 | 
			
		||||
  getFloorAreaLookup,
 | 
			
		||||
  subscribeFloorRegistry,
 | 
			
		||||
} from "../data/floor_registry";
 | 
			
		||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
 | 
			
		||||
import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry";
 | 
			
		||||
import { HomeAssistant, ValueChangedEvent } from "../types";
 | 
			
		||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
			
		||||
import "./ha-combo-box";
 | 
			
		||||
@@ -50,7 +45,7 @@ interface FloorAreaEntry {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@customElement("ha-area-floor-picker")
 | 
			
		||||
export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
export class HaAreaFloorPicker extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public label?: string;
 | 
			
		||||
@@ -111,22 +106,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public required = false;
 | 
			
		||||
 | 
			
		||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  @state() private _opened?: boolean;
 | 
			
		||||
 | 
			
		||||
  @query("ha-combo-box", true) public comboBox!: HaComboBox;
 | 
			
		||||
 | 
			
		||||
  private _init = false;
 | 
			
		||||
 | 
			
		||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
			
		||||
    return [
 | 
			
		||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
			
		||||
        this._floors = floors;
 | 
			
		||||
      }),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public async open() {
 | 
			
		||||
    await this.updateComplete;
 | 
			
		||||
    await this.comboBox?.open();
 | 
			
		||||
@@ -431,12 +416,12 @@ export class HaAreaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues) {
 | 
			
		||||
    if (
 | 
			
		||||
      (!this._init && this.hass && this._floors) ||
 | 
			
		||||
      (!this._init && this.hass) ||
 | 
			
		||||
      (this._init && changedProps.has("_opened") && this._opened)
 | 
			
		||||
    ) {
 | 
			
		||||
      this._init = true;
 | 
			
		||||
      const areas = this._getAreas(
 | 
			
		||||
        this._floors!,
 | 
			
		||||
        Object.values(this.hass.floors),
 | 
			
		||||
        Object.values(this.hass.areas),
 | 
			
		||||
        Object.values(this.hass.devices),
 | 
			
		||||
        Object.values(this.hass.entities),
 | 
			
		||||
 
 | 
			
		||||
@@ -124,9 +124,12 @@ export class HaCodeEditor extends ReactiveElement {
 | 
			
		||||
    const transactions: TransactionSpec[] = [];
 | 
			
		||||
    if (changedProps.has("mode")) {
 | 
			
		||||
      transactions.push({
 | 
			
		||||
        effects: this._loadedCodeMirror!.langCompartment!.reconfigure(
 | 
			
		||||
          this._mode
 | 
			
		||||
        ),
 | 
			
		||||
        effects: [
 | 
			
		||||
          this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode),
 | 
			
		||||
          this._loadedCodeMirror!.foldingCompartment.reconfigure(
 | 
			
		||||
            this._getFoldingExtensions()
 | 
			
		||||
          ),
 | 
			
		||||
        ],
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    if (changedProps.has("readOnly")) {
 | 
			
		||||
@@ -177,6 +180,14 @@ export class HaCodeEditor extends ReactiveElement {
 | 
			
		||||
      this._loadedCodeMirror.crosshairCursor(),
 | 
			
		||||
      this._loadedCodeMirror.highlightSelectionMatches(),
 | 
			
		||||
      this._loadedCodeMirror.highlightActiveLine(),
 | 
			
		||||
      this._loadedCodeMirror.indentationMarkers({
 | 
			
		||||
        thickness: 0,
 | 
			
		||||
        activeThickness: 1,
 | 
			
		||||
        colors: {
 | 
			
		||||
          activeLight: "var(--secondary-text-color)",
 | 
			
		||||
          activeDark: "var(--secondary-text-color)",
 | 
			
		||||
        },
 | 
			
		||||
      }),
 | 
			
		||||
      this._loadedCodeMirror.keymap.of([
 | 
			
		||||
        ...this._loadedCodeMirror.defaultKeymap,
 | 
			
		||||
        ...this._loadedCodeMirror.searchKeymap,
 | 
			
		||||
@@ -194,6 +205,9 @@ export class HaCodeEditor extends ReactiveElement {
 | 
			
		||||
        this.linewrap ? this._loadedCodeMirror.EditorView.lineWrapping : []
 | 
			
		||||
      ),
 | 
			
		||||
      this._loadedCodeMirror.EditorView.updateListener.of(this._onUpdate),
 | 
			
		||||
      this._loadedCodeMirror.foldingCompartment.of(
 | 
			
		||||
        this._getFoldingExtensions()
 | 
			
		||||
      ),
 | 
			
		||||
    ];
 | 
			
		||||
 | 
			
		||||
    if (!this.readOnly) {
 | 
			
		||||
@@ -311,6 +325,17 @@ export class HaCodeEditor extends ReactiveElement {
 | 
			
		||||
    fireEvent(this, "value-changed", { value: this._value });
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  private _getFoldingExtensions = (): Extension => {
 | 
			
		||||
    if (this.mode === "yaml") {
 | 
			
		||||
      return [
 | 
			
		||||
        this._loadedCodeMirror!.foldGutter(),
 | 
			
		||||
        this._loadedCodeMirror!.foldingOnIndent,
 | 
			
		||||
      ];
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    return [];
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResultGroup {
 | 
			
		||||
    return css`
 | 
			
		||||
      :host(.error-state) .cm-gutters {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,16 @@
 | 
			
		||||
import "@material/mwc-list/mwc-list-item";
 | 
			
		||||
import { mdiInvertColorsOff, mdiPalette } from "@mdi/js";
 | 
			
		||||
import { css, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
import { customElement, property, query } from "lit/decorators";
 | 
			
		||||
import { styleMap } from "lit/directives/style-map";
 | 
			
		||||
import { computeCssColor, THEME_COLORS } from "../common/color/compute-color";
 | 
			
		||||
import { fireEvent } from "../common/dom/fire_event";
 | 
			
		||||
import { stopPropagation } from "../common/dom/stop_propagation";
 | 
			
		||||
import "./ha-select";
 | 
			
		||||
import "./ha-list-item";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import { LocalizeKeys } from "../common/translations/localize";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import "./ha-list-item";
 | 
			
		||||
import "./ha-md-divider";
 | 
			
		||||
import "./ha-select";
 | 
			
		||||
import type { HaSelect } from "./ha-select";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-color-picker")
 | 
			
		||||
export class HaColorPicker extends LitElement {
 | 
			
		||||
@@ -20,43 +22,97 @@ export class HaColorPicker extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property() public value?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public defaultColor = false;
 | 
			
		||||
  @property({ type: String, attribute: "default_color" })
 | 
			
		||||
  public defaultColor?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "include_state" })
 | 
			
		||||
  public includeState = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "include_none" })
 | 
			
		||||
  public includeNone = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public disabled = false;
 | 
			
		||||
 | 
			
		||||
  _valueSelected(ev) {
 | 
			
		||||
  @query("ha-select") private _select?: HaSelect;
 | 
			
		||||
 | 
			
		||||
  connectedCallback(): void {
 | 
			
		||||
    super.connectedCallback();
 | 
			
		||||
    // Refresh layout options when the field is connected to the DOM to ensure current value displayed
 | 
			
		||||
    this._select?.layoutOptions();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _valueSelected(ev) {
 | 
			
		||||
    ev.stopPropagation();
 | 
			
		||||
    if (!this.isConnected) return;
 | 
			
		||||
    const value = ev.target.value;
 | 
			
		||||
    if (value) {
 | 
			
		||||
      fireEvent(this, "value-changed", {
 | 
			
		||||
        value: value !== "default" ? value : undefined,
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
    this.value = value === this.defaultColor ? undefined : value;
 | 
			
		||||
    fireEvent(this, "value-changed", {
 | 
			
		||||
      value: this.value,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  render() {
 | 
			
		||||
    const value = this.value || this.defaultColor || "";
 | 
			
		||||
 | 
			
		||||
    const isCustom = !(
 | 
			
		||||
      THEME_COLORS.has(value) ||
 | 
			
		||||
      value === "none" ||
 | 
			
		||||
      value === "state"
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-select
 | 
			
		||||
        .icon=${Boolean(this.value)}
 | 
			
		||||
        .icon=${Boolean(value)}
 | 
			
		||||
        .label=${this.label}
 | 
			
		||||
        .value=${this.value || "default"}
 | 
			
		||||
        .value=${value}
 | 
			
		||||
        .helper=${this.helper}
 | 
			
		||||
        .disabled=${this.disabled}
 | 
			
		||||
        @closed=${stopPropagation}
 | 
			
		||||
        @selected=${this._valueSelected}
 | 
			
		||||
        fixedMenuPosition
 | 
			
		||||
        naturalMenuWidth
 | 
			
		||||
        .clearable=${!this.defaultColor}
 | 
			
		||||
      >
 | 
			
		||||
        ${this.value
 | 
			
		||||
        ${value
 | 
			
		||||
          ? html`
 | 
			
		||||
              <span slot="icon">
 | 
			
		||||
                ${this.renderColorCircle(this.value || "grey")}
 | 
			
		||||
                ${value === "none"
 | 
			
		||||
                  ? html`
 | 
			
		||||
                      <ha-svg-icon path=${mdiInvertColorsOff}></ha-svg-icon>
 | 
			
		||||
                    `
 | 
			
		||||
                  : value === "state"
 | 
			
		||||
                    ? html`<ha-svg-icon path=${mdiPalette}></ha-svg-icon>`
 | 
			
		||||
                    : this.renderColorCircle(value || "grey")}
 | 
			
		||||
              </span>
 | 
			
		||||
            `
 | 
			
		||||
          : nothing}
 | 
			
		||||
        ${this.defaultColor
 | 
			
		||||
          ? html` <ha-list-item value="default">
 | 
			
		||||
              ${this.hass.localize(`ui.components.color-picker.default_color`)}
 | 
			
		||||
            </ha-list-item>`
 | 
			
		||||
        ${this.includeNone
 | 
			
		||||
          ? html`
 | 
			
		||||
              <ha-list-item value="none" graphic="icon">
 | 
			
		||||
                ${this.hass.localize("ui.components.color-picker.none")}
 | 
			
		||||
                ${this.defaultColor === "none"
 | 
			
		||||
                  ? ` (${this.hass.localize("ui.components.color-picker.default")})`
 | 
			
		||||
                  : nothing}
 | 
			
		||||
                <ha-svg-icon
 | 
			
		||||
                  slot="graphic"
 | 
			
		||||
                  path=${mdiInvertColorsOff}
 | 
			
		||||
                ></ha-svg-icon>
 | 
			
		||||
              </ha-list-item>
 | 
			
		||||
            `
 | 
			
		||||
          : nothing}
 | 
			
		||||
        ${this.includeState
 | 
			
		||||
          ? html`
 | 
			
		||||
              <ha-list-item value="state" graphic="icon">
 | 
			
		||||
                ${this.hass.localize("ui.components.color-picker.state")}
 | 
			
		||||
                ${this.defaultColor === "state"
 | 
			
		||||
                  ? ` (${this.hass.localize("ui.components.color-picker.default")})`
 | 
			
		||||
                  : nothing}
 | 
			
		||||
                <ha-svg-icon slot="graphic" path=${mdiPalette}></ha-svg-icon>
 | 
			
		||||
              </ha-list-item>
 | 
			
		||||
            `
 | 
			
		||||
          : nothing}
 | 
			
		||||
        ${this.includeState || this.includeNone
 | 
			
		||||
          ? html`<ha-md-divider role="separator" tabindex="-1"></ha-md-divider>`
 | 
			
		||||
          : nothing}
 | 
			
		||||
        ${Array.from(THEME_COLORS).map(
 | 
			
		||||
          (color) => html`
 | 
			
		||||
@@ -64,10 +120,21 @@ export class HaColorPicker extends LitElement {
 | 
			
		||||
              ${this.hass.localize(
 | 
			
		||||
                `ui.components.color-picker.colors.${color}` as LocalizeKeys
 | 
			
		||||
              ) || color}
 | 
			
		||||
              ${this.defaultColor === color
 | 
			
		||||
                ? ` (${this.hass.localize("ui.components.color-picker.default")})`
 | 
			
		||||
                : nothing}
 | 
			
		||||
              <span slot="graphic">${this.renderColorCircle(color)}</span>
 | 
			
		||||
            </ha-list-item>
 | 
			
		||||
          `
 | 
			
		||||
        )}
 | 
			
		||||
        ${isCustom
 | 
			
		||||
          ? html`
 | 
			
		||||
              <ha-list-item .value=${value} graphic="icon">
 | 
			
		||||
                ${value}
 | 
			
		||||
                <span slot="graphic">${this.renderColorCircle(value)}</span>
 | 
			
		||||
              </ha-list-item>
 | 
			
		||||
            `
 | 
			
		||||
          : nothing}
 | 
			
		||||
      </ha-select>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
@@ -87,10 +154,11 @@ export class HaColorPicker extends LitElement {
 | 
			
		||||
    return css`
 | 
			
		||||
      .circle-color {
 | 
			
		||||
        display: block;
 | 
			
		||||
        background-color: var(--circle-color);
 | 
			
		||||
        background-color: var(--circle-color, var(--divider-color));
 | 
			
		||||
        border-radius: 10px;
 | 
			
		||||
        width: 20px;
 | 
			
		||||
        height: 20px;
 | 
			
		||||
        box-sizing: border-box;
 | 
			
		||||
      }
 | 
			
		||||
      ha-select {
 | 
			
		||||
        width: 100%;
 | 
			
		||||
 
 | 
			
		||||
@@ -10,8 +10,13 @@ export class HaDialogHeader extends LitElement {
 | 
			
		||||
          <section class="header-navigation-icon">
 | 
			
		||||
            <slot name="navigationIcon"></slot>
 | 
			
		||||
          </section>
 | 
			
		||||
          <section class="header-title">
 | 
			
		||||
            <slot name="title"></slot>
 | 
			
		||||
          <section class="header-content">
 | 
			
		||||
            <div class="header-title">
 | 
			
		||||
              <slot name="title"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
            <div class="header-subtitle">
 | 
			
		||||
              <slot name="subtitle"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
          </section>
 | 
			
		||||
          <section class="header-action-items">
 | 
			
		||||
            <slot name="actionItems"></slot>
 | 
			
		||||
@@ -39,17 +44,24 @@ export class HaDialogHeader extends LitElement {
 | 
			
		||||
          padding: 4px;
 | 
			
		||||
          box-sizing: border-box;
 | 
			
		||||
        }
 | 
			
		||||
        .header-title {
 | 
			
		||||
        .header-content {
 | 
			
		||||
          flex: 1;
 | 
			
		||||
          font-size: 22px;
 | 
			
		||||
          line-height: 28px;
 | 
			
		||||
          font-weight: 400;
 | 
			
		||||
          padding: 10px 4px;
 | 
			
		||||
          min-width: 0;
 | 
			
		||||
          overflow: hidden;
 | 
			
		||||
          text-overflow: ellipsis;
 | 
			
		||||
          white-space: nowrap;
 | 
			
		||||
        }
 | 
			
		||||
        .header-title {
 | 
			
		||||
          font-size: 22px;
 | 
			
		||||
          line-height: 28px;
 | 
			
		||||
          font-weight: 400;
 | 
			
		||||
        }
 | 
			
		||||
        .header-subtitle {
 | 
			
		||||
          font-size: 14px;
 | 
			
		||||
          line-height: 20px;
 | 
			
		||||
          color: var(--secondary-text-color);
 | 
			
		||||
        }
 | 
			
		||||
        @media all and (min-width: 450px) and (min-height: 500px) {
 | 
			
		||||
          .header-bar {
 | 
			
		||||
            padding: 12px;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
import "@material/mwc-menu/mwc-menu-surface";
 | 
			
		||||
import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js";
 | 
			
		||||
import { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
			
		||||
import {
 | 
			
		||||
  CSSResultGroup,
 | 
			
		||||
  LitElement,
 | 
			
		||||
@@ -15,13 +14,8 @@ import { repeat } from "lit/directives/repeat";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { fireEvent } from "../common/dom/fire_event";
 | 
			
		||||
import { computeRTL } from "../common/util/compute_rtl";
 | 
			
		||||
import {
 | 
			
		||||
  FloorRegistryEntry,
 | 
			
		||||
  getFloorAreaLookup,
 | 
			
		||||
  subscribeFloorRegistry,
 | 
			
		||||
} from "../data/floor_registry";
 | 
			
		||||
import { getFloorAreaLookup } from "../data/floor_registry";
 | 
			
		||||
import { RelatedResult, findRelated } from "../data/search";
 | 
			
		||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
 | 
			
		||||
import { haStyleScrollbar } from "../resources/styles";
 | 
			
		||||
import type { HomeAssistant } from "../types";
 | 
			
		||||
import "./ha-check-list-item";
 | 
			
		||||
@@ -31,7 +25,7 @@ import "./ha-svg-icon";
 | 
			
		||||
import "./ha-tree-indicator";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-filter-floor-areas")
 | 
			
		||||
export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
			
		||||
export class HaFilterFloorAreas extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public value?: {
 | 
			
		||||
@@ -47,8 +41,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
			
		||||
 | 
			
		||||
  @state() private _shouldRender = false;
 | 
			
		||||
 | 
			
		||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  public willUpdate(properties: PropertyValues) {
 | 
			
		||||
    super.willUpdate(properties);
 | 
			
		||||
 | 
			
		||||
@@ -60,7 +52,7 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const areas = this._areas(this.hass.areas, this._floors);
 | 
			
		||||
    const areas = this._areas(this.hass.areas, this.hass.floors);
 | 
			
		||||
 | 
			
		||||
    return html`
 | 
			
		||||
      <ha-expansion-panel
 | 
			
		||||
@@ -189,14 +181,6 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
			
		||||
    this._findRelated();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
			
		||||
    return [
 | 
			
		||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
			
		||||
        this._floors = floors;
 | 
			
		||||
      }),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected updated(changed) {
 | 
			
		||||
    if (changed.has("expanded") && this.expanded) {
 | 
			
		||||
      setTimeout(() => {
 | 
			
		||||
@@ -220,9 +204,9 @@ export class HaFilterFloorAreas extends SubscribeMixin(LitElement) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _areas = memoizeOne(
 | 
			
		||||
    (areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => {
 | 
			
		||||
    (areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => {
 | 
			
		||||
      const areas = Object.values(areaReg);
 | 
			
		||||
 | 
			
		||||
      const floors = Object.values(floorReg);
 | 
			
		||||
      const floorAreaLookup = getFloorAreaLookup(areas);
 | 
			
		||||
 | 
			
		||||
      const unassisgnedAreas = areas.filter(
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,5 @@
 | 
			
		||||
import { ComboBoxLitRenderer } from "@vaadin/combo-box/lit";
 | 
			
		||||
import { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
			
		||||
import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { LitElement, PropertyValues, TemplateResult, html } from "lit";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { classMap } from "lit/directives/class-map";
 | 
			
		||||
@@ -24,10 +24,8 @@ import {
 | 
			
		||||
  FloorRegistryEntry,
 | 
			
		||||
  createFloorRegistryEntry,
 | 
			
		||||
  getFloorAreaLookup,
 | 
			
		||||
  subscribeFloorRegistry,
 | 
			
		||||
} from "../data/floor_registry";
 | 
			
		||||
import { showAlertDialog } from "../dialogs/generic/show-dialog-box";
 | 
			
		||||
import { SubscribeMixin } from "../mixins/subscribe-mixin";
 | 
			
		||||
import { showFloorRegistryDetailDialog } from "../panels/config/areas/show-dialog-floor-registry-detail";
 | 
			
		||||
import { HomeAssistant, ValueChangedEvent } from "../types";
 | 
			
		||||
import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker";
 | 
			
		||||
@@ -53,7 +51,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) =>
 | 
			
		||||
  </ha-list-item>`;
 | 
			
		||||
 | 
			
		||||
@customElement("ha-floor-picker")
 | 
			
		||||
export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
export class HaFloorPicker extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass!: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property() public label?: string;
 | 
			
		||||
@@ -111,8 +109,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
 | 
			
		||||
  @state() private _opened?: boolean;
 | 
			
		||||
 | 
			
		||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  @query("ha-combo-box", true) public comboBox!: HaComboBox;
 | 
			
		||||
 | 
			
		||||
  private _suggestion?: string;
 | 
			
		||||
@@ -129,14 +125,6 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
    await this.comboBox?.focus();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
			
		||||
    return [
 | 
			
		||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
			
		||||
        this._floors = floors;
 | 
			
		||||
      }),
 | 
			
		||||
    ];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _getFloors = memoizeOne(
 | 
			
		||||
    (
 | 
			
		||||
      floors: FloorRegistryEntry[],
 | 
			
		||||
@@ -320,12 +308,12 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
 | 
			
		||||
  protected updated(changedProps: PropertyValues) {
 | 
			
		||||
    if (
 | 
			
		||||
      (!this._init && this.hass && this._floors) ||
 | 
			
		||||
      (!this._init && this.hass) ||
 | 
			
		||||
      (this._init && changedProps.has("_opened") && this._opened)
 | 
			
		||||
    ) {
 | 
			
		||||
      this._init = true;
 | 
			
		||||
      const floors = this._getFloors(
 | 
			
		||||
        this._floors!,
 | 
			
		||||
        Object.values(this.hass.floors),
 | 
			
		||||
        Object.values(this.hass.areas),
 | 
			
		||||
        Object.values(this.hass.devices),
 | 
			
		||||
        Object.values(this.hass.entities),
 | 
			
		||||
@@ -360,8 +348,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
          ? this.hass.localize("ui.components.floor-picker.floor")
 | 
			
		||||
          : this.label}
 | 
			
		||||
        .placeholder=${this.placeholder
 | 
			
		||||
          ? this._floors?.find((floor) => floor.floor_id === this.placeholder)
 | 
			
		||||
              ?.name
 | 
			
		||||
          ? this.hass.floors[this.placeholder]?.name
 | 
			
		||||
          : undefined}
 | 
			
		||||
        .renderer=${rowRenderer}
 | 
			
		||||
        @filter-changed=${this._filterChanged}
 | 
			
		||||
@@ -460,7 +447,7 @@ export class HaFloorPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
              floor_id: floor.floor_id,
 | 
			
		||||
            });
 | 
			
		||||
          });
 | 
			
		||||
          const floors = [...this._floors!, floor];
 | 
			
		||||
          const floors = [...Object.values(this.hass.floors), floor];
 | 
			
		||||
          this.comboBox.filteredItems = this._getFloors(
 | 
			
		||||
            floors,
 | 
			
		||||
            Object.values(this.hass.areas)!,
 | 
			
		||||
 
 | 
			
		||||
@@ -30,6 +30,10 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
 | 
			
		||||
    options?: { path?: string[] }
 | 
			
		||||
  ) => string;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public localizeValue?: (
 | 
			
		||||
    key: string
 | 
			
		||||
  ) => string;
 | 
			
		||||
 | 
			
		||||
  private _renderDescription() {
 | 
			
		||||
    const description = this.computeHelper?.(this.schema);
 | 
			
		||||
    return description ? html`<p>${description}</p>` : nothing;
 | 
			
		||||
@@ -86,6 +90,7 @@ export class HaFormExpendable extends LitElement implements HaFormElement {
 | 
			
		||||
            .disabled=${this.disabled}
 | 
			
		||||
            .computeLabel=${this._computeLabel}
 | 
			
		||||
            .computeHelper=${this._computeHelper}
 | 
			
		||||
            .localizeValue=${this.localizeValue}
 | 
			
		||||
          ></ha-form>
 | 
			
		||||
        </div>
 | 
			
		||||
      </ha-expansion-panel>
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,10 @@ export class HaFormGrid extends LitElement implements HaFormElement {
 | 
			
		||||
    schema: HaFormSchema
 | 
			
		||||
  ) => string;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: false }) public localizeValue?: (
 | 
			
		||||
    key: string
 | 
			
		||||
  ) => string;
 | 
			
		||||
 | 
			
		||||
  public async focus() {
 | 
			
		||||
    await this.updateComplete;
 | 
			
		||||
    this.renderRoot.querySelector("ha-form")?.focus();
 | 
			
		||||
@@ -65,6 +69,7 @@ export class HaFormGrid extends LitElement implements HaFormElement {
 | 
			
		||||
            .disabled=${this.disabled}
 | 
			
		||||
            .computeLabel=${this.computeLabel}
 | 
			
		||||
            .computeHelper=${this.computeHelper}
 | 
			
		||||
            .localizeValue=${this.localizeValue}
 | 
			
		||||
          ></ha-form>
 | 
			
		||||
        `
 | 
			
		||||
      )}
 | 
			
		||||
 
 | 
			
		||||
@@ -163,6 +163,7 @@ export class HaForm extends LitElement implements HaFormElement {
 | 
			
		||||
                  localize: this.hass?.localize,
 | 
			
		||||
                  computeLabel: this.computeLabel,
 | 
			
		||||
                  computeHelper: this.computeHelper,
 | 
			
		||||
                  localizeValue: this.localizeValue,
 | 
			
		||||
                  context: this._generateContext(item),
 | 
			
		||||
                  ...this.getFormProperties(),
 | 
			
		||||
                })}
 | 
			
		||||
 
 | 
			
		||||
@@ -68,7 +68,7 @@ export class HaGridSizeEditor extends LitElement {
 | 
			
		||||
          .min=${columnMin}
 | 
			
		||||
          .max=${columnMax}
 | 
			
		||||
          .range=${this.columns}
 | 
			
		||||
          .value=${fullWidth ? this.columns : columnValue}
 | 
			
		||||
          .value=${fullWidth ? this.columns : this.value?.columns}
 | 
			
		||||
          @value-changed=${this._valueChanged}
 | 
			
		||||
          @slider-moved=${this._sliderMoved}
 | 
			
		||||
          .disabled=${disabledColumns}
 | 
			
		||||
@@ -83,7 +83,7 @@ export class HaGridSizeEditor extends LitElement {
 | 
			
		||||
          .max=${rowMax}
 | 
			
		||||
          .range=${this.rows}
 | 
			
		||||
          vertical
 | 
			
		||||
          .value=${rowValue}
 | 
			
		||||
          .value=${autoHeight ? rowMin : this.value?.rows}
 | 
			
		||||
          @value-changed=${this._valueChanged}
 | 
			
		||||
          @slider-moved=${this._sliderMoved}
 | 
			
		||||
          .disabled=${disabledRows}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										58
									
								
								src/components/ha-heading-badge.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,58 @@
 | 
			
		||||
import { css, CSSResultGroup, html, LitElement } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
 | 
			
		||||
type HeadingBadgeType = "text" | "button";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-heading-badge")
 | 
			
		||||
export class HaBadge extends LitElement {
 | 
			
		||||
  @property() public type: HeadingBadgeType = "text";
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    return html`
 | 
			
		||||
      <div
 | 
			
		||||
        class="heading-badge"
 | 
			
		||||
        role=${ifDefined(this.type === "button" ? "button" : undefined)}
 | 
			
		||||
        tabindex=${ifDefined(this.type === "button" ? "0" : undefined)}
 | 
			
		||||
      >
 | 
			
		||||
        <slot name="icon"></slot>
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
      </div>
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static get styles(): CSSResultGroup {
 | 
			
		||||
    return css`
 | 
			
		||||
      :host {
 | 
			
		||||
        color: var(--secondary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
      [role="button"] {
 | 
			
		||||
        cursor: pointer;
 | 
			
		||||
      }
 | 
			
		||||
      .heading-badge {
 | 
			
		||||
        display: flex;
 | 
			
		||||
        flex-direction: row;
 | 
			
		||||
        white-space: nowrap;
 | 
			
		||||
        align-items: center;
 | 
			
		||||
        gap: 3px;
 | 
			
		||||
        font-family: Roboto;
 | 
			
		||||
        font-size: 14px;
 | 
			
		||||
        font-style: normal;
 | 
			
		||||
        font-weight: 400;
 | 
			
		||||
        line-height: 20px;
 | 
			
		||||
        letter-spacing: 0.1px;
 | 
			
		||||
        --mdc-icon-size: 14px;
 | 
			
		||||
      }
 | 
			
		||||
      ::slotted([slot="icon"]) {
 | 
			
		||||
        --ha-icon-display: block;
 | 
			
		||||
        color: var(--icon-color, inherit);
 | 
			
		||||
      }
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-heading-badge": HaBadge;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
import { Button } from "@material/mwc-button";
 | 
			
		||||
import { Corner } from "@material/web/menu/menu";
 | 
			
		||||
import { css, CSSResultGroup, html, LitElement, TemplateResult } from "lit";
 | 
			
		||||
import { customElement, property, query } from "lit/decorators";
 | 
			
		||||
import { FOCUS_TARGET } from "../dialogs/make-dialog-manager";
 | 
			
		||||
@@ -7,28 +6,16 @@ import type { HaIconButton } from "./ha-icon-button";
 | 
			
		||||
import "./ha-menu";
 | 
			
		||||
import type { HaMenu } from "./ha-menu";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-button-menu-new")
 | 
			
		||||
export class HaButtonMenuNew extends LitElement {
 | 
			
		||||
@customElement("ha-md-button-menu")
 | 
			
		||||
export class HaMdButtonMenu extends LitElement {
 | 
			
		||||
  protected readonly [FOCUS_TARGET];
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public disabled = false;
 | 
			
		||||
 | 
			
		||||
  @property() public positioning?: "fixed" | "absolute" | "popover";
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "no-horizontal-flip" })
 | 
			
		||||
  public noHorizontalFlip = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "no-vertical-flip" })
 | 
			
		||||
  public noVerticalFlip = false;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: "anchor-corner" })
 | 
			
		||||
  public anchorCorner: Corner = Corner.END_START;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: "menu-corner" })
 | 
			
		||||
  public menuCorner: Corner = Corner.START_START;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, attribute: "has-overflow" })
 | 
			
		||||
  public hasOverflow = false;
 | 
			
		||||
  @property({ type: Boolean, attribute: "has-overflow" }) public hasOverflow =
 | 
			
		||||
    false;
 | 
			
		||||
 | 
			
		||||
  @query("ha-menu", true) private _menu!: HaMenu;
 | 
			
		||||
 | 
			
		||||
@@ -52,10 +39,6 @@ export class HaButtonMenuNew extends LitElement {
 | 
			
		||||
      <ha-menu
 | 
			
		||||
        .positioning=${this.positioning}
 | 
			
		||||
        .hasOverflow=${this.hasOverflow}
 | 
			
		||||
        .anchorCorner=${this.anchorCorner}
 | 
			
		||||
        .menuCorner=${this.menuCorner}
 | 
			
		||||
        .noVerticalFlip=${this.noVerticalFlip}
 | 
			
		||||
        .noHorizontalFlip=${this.noHorizontalFlip}
 | 
			
		||||
      >
 | 
			
		||||
        <slot></slot>
 | 
			
		||||
      </ha-menu>
 | 
			
		||||
@@ -101,6 +84,6 @@ export class HaButtonMenuNew extends LitElement {
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-button-menu-new": HaButtonMenuNew;
 | 
			
		||||
    "ha-md-button-menu": HaMdButtonMenu;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										250
									
								
								src/components/ha-md-dialog.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,250 @@
 | 
			
		||||
import { MdDialog } from "@material/web/dialog/dialog";
 | 
			
		||||
import {
 | 
			
		||||
  type DialogAnimation,
 | 
			
		||||
  DIALOG_DEFAULT_CLOSE_ANIMATION,
 | 
			
		||||
  DIALOG_DEFAULT_OPEN_ANIMATION,
 | 
			
		||||
} from "@material/web/dialog/internal/animations";
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
 | 
			
		||||
// workaround to be able to overlay an dialog with another dialog
 | 
			
		||||
MdDialog.addInitializer(async (instance) => {
 | 
			
		||||
  await instance.updateComplete;
 | 
			
		||||
 | 
			
		||||
  const dialogInstance = instance as MdDialog;
 | 
			
		||||
 | 
			
		||||
  // @ts-expect-error dialog is private
 | 
			
		||||
  dialogInstance.dialog.prepend(dialogInstance.scrim);
 | 
			
		||||
  // @ts-expect-error scrim is private
 | 
			
		||||
  dialogInstance.scrim.style.inset = 0;
 | 
			
		||||
  // @ts-expect-error scrim is private
 | 
			
		||||
  dialogInstance.scrim.style.zIndex = 0;
 | 
			
		||||
 | 
			
		||||
  const { getOpenAnimation, getCloseAnimation } = dialogInstance;
 | 
			
		||||
  dialogInstance.getOpenAnimation = () => {
 | 
			
		||||
    const animations = getOpenAnimation.call(this);
 | 
			
		||||
    animations.container = [
 | 
			
		||||
      ...(animations.container ?? []),
 | 
			
		||||
      ...(animations.dialog ?? []),
 | 
			
		||||
    ];
 | 
			
		||||
    animations.dialog = [];
 | 
			
		||||
    return animations;
 | 
			
		||||
  };
 | 
			
		||||
  dialogInstance.getCloseAnimation = () => {
 | 
			
		||||
    const animations = getCloseAnimation.call(this);
 | 
			
		||||
    animations.container = [
 | 
			
		||||
      ...(animations.container ?? []),
 | 
			
		||||
      ...(animations.dialog ?? []),
 | 
			
		||||
    ];
 | 
			
		||||
    animations.dialog = [];
 | 
			
		||||
    return animations;
 | 
			
		||||
  };
 | 
			
		||||
});
 | 
			
		||||
 | 
			
		||||
let DIALOG_POLYFILL: Promise<typeof import("dialog-polyfill")>;
 | 
			
		||||
 | 
			
		||||
/**
 | 
			
		||||
 * Based on the home assistant design: https://design.home-assistant.io/#components/ha-dialogs
 | 
			
		||||
 *
 | 
			
		||||
 */
 | 
			
		||||
@customElement("ha-md-dialog")
 | 
			
		||||
export class HaMdDialog extends MdDialog {
 | 
			
		||||
  /**
 | 
			
		||||
   * When true the dialog will not close when the user presses the esc key or press out of the dialog.
 | 
			
		||||
   */
 | 
			
		||||
  @property({ attribute: "disable-cancel-action", type: Boolean })
 | 
			
		||||
  public disableCancelAction = false;
 | 
			
		||||
 | 
			
		||||
  private _polyfillDialogRegistered = false;
 | 
			
		||||
 | 
			
		||||
  constructor() {
 | 
			
		||||
    super();
 | 
			
		||||
    this.addEventListener("cancel", this._handleCancel);
 | 
			
		||||
 | 
			
		||||
    if (typeof HTMLDialogElement !== "function") {
 | 
			
		||||
      this.addEventListener("open", this._handleOpen);
 | 
			
		||||
 | 
			
		||||
      if (!DIALOG_POLYFILL) {
 | 
			
		||||
        DIALOG_POLYFILL = import("dialog-polyfill");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // if browser doesn't support animate API disable open/close animations
 | 
			
		||||
    if (this.animate === undefined) {
 | 
			
		||||
      this.quick = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // if browser doesn't support animate API disable open/close animations
 | 
			
		||||
    if (this.animate === undefined) {
 | 
			
		||||
      this.quick = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // prevent open in older browsers and wait for polyfill to load
 | 
			
		||||
  private async _handleOpen(openEvent: Event) {
 | 
			
		||||
    openEvent.preventDefault();
 | 
			
		||||
 | 
			
		||||
    if (this._polyfillDialogRegistered) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._polyfillDialogRegistered = true;
 | 
			
		||||
    this._loadPolyfillStylesheet("/static/polyfills/dialog-polyfill.css");
 | 
			
		||||
    const dialog = this.shadowRoot?.querySelector(
 | 
			
		||||
      "dialog"
 | 
			
		||||
    ) as HTMLDialogElement;
 | 
			
		||||
 | 
			
		||||
    const dialogPolyfill = await DIALOG_POLYFILL;
 | 
			
		||||
    dialogPolyfill.default.registerDialog(dialog);
 | 
			
		||||
    this.removeEventListener("open", this._handleOpen);
 | 
			
		||||
 | 
			
		||||
    this.show();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _loadPolyfillStylesheet(href) {
 | 
			
		||||
    const link = document.createElement("link");
 | 
			
		||||
    link.rel = "stylesheet";
 | 
			
		||||
    link.href = href;
 | 
			
		||||
 | 
			
		||||
    return new Promise<void>((resolve, reject) => {
 | 
			
		||||
      link.onload = () => resolve();
 | 
			
		||||
      link.onerror = () =>
 | 
			
		||||
        reject(new Error(`Stylesheet failed to load: ${href}`));
 | 
			
		||||
 | 
			
		||||
      this.shadowRoot?.appendChild(link);
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  _handleCancel(closeEvent: Event) {
 | 
			
		||||
    if (this.disableCancelAction) {
 | 
			
		||||
      closeEvent.preventDefault();
 | 
			
		||||
      const dialogElement = this.shadowRoot?.querySelector("dialog .container");
 | 
			
		||||
      if (this.animate !== undefined) {
 | 
			
		||||
        dialogElement?.animate(
 | 
			
		||||
          [
 | 
			
		||||
            {
 | 
			
		||||
              transform: "rotate(-1deg)",
 | 
			
		||||
              "animation-timing-function": "ease-in",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              transform: "rotate(1.5deg)",
 | 
			
		||||
              "animation-timing-function": "ease-out",
 | 
			
		||||
            },
 | 
			
		||||
            {
 | 
			
		||||
              transform: "rotate(0deg)",
 | 
			
		||||
              "animation-timing-function": "ease-in",
 | 
			
		||||
            },
 | 
			
		||||
          ],
 | 
			
		||||
          {
 | 
			
		||||
            duration: 200,
 | 
			
		||||
            iterations: 2,
 | 
			
		||||
          }
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static override styles = [
 | 
			
		||||
    ...super.styles,
 | 
			
		||||
    css`
 | 
			
		||||
      :host {
 | 
			
		||||
        --md-dialog-container-color: var(--card-background-color);
 | 
			
		||||
        --md-dialog-headline-color: var(--primary-text-color);
 | 
			
		||||
        --md-dialog-supporting-text-color: var(--primary-text-color);
 | 
			
		||||
        --md-sys-color-scrim: #000000;
 | 
			
		||||
 | 
			
		||||
        --md-dialog-headline-weight: 400;
 | 
			
		||||
        --md-dialog-headline-size: 1.574rem;
 | 
			
		||||
        --md-dialog-supporting-text-size: 1rem;
 | 
			
		||||
        --md-dialog-supporting-text-line-height: 1.5rem;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      :host([type="alert"]) {
 | 
			
		||||
        min-width: 320px;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      :host(:not([type="alert"])) {
 | 
			
		||||
        @media all and (max-width: 450px), all and (max-height: 500px) {
 | 
			
		||||
          min-width: calc(
 | 
			
		||||
            100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
 | 
			
		||||
          );
 | 
			
		||||
          max-width: calc(
 | 
			
		||||
            100vw - env(safe-area-inset-right) - env(safe-area-inset-left)
 | 
			
		||||
          );
 | 
			
		||||
          min-height: 100%;
 | 
			
		||||
          max-height: 100%;
 | 
			
		||||
          --md-dialog-container-shape: 0;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      :host ::slotted(ha-dialog-header) {
 | 
			
		||||
        display: contents;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      slot[name="content"]::slotted(*) {
 | 
			
		||||
        padding: var(--dialog-content-padding, 24px);
 | 
			
		||||
      }
 | 
			
		||||
      .scrim {
 | 
			
		||||
        z-index: 10; // overlay navigation
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// by default the dialog open/close animation will be from/to the top
 | 
			
		||||
// but if we have a special mobile dialog which is at the bottom of the screen, an from bottom animation can be used:
 | 
			
		||||
const OPEN_FROM_BOTTOM_ANIMATION: DialogAnimation = {
 | 
			
		||||
  ...DIALOG_DEFAULT_OPEN_ANIMATION,
 | 
			
		||||
  dialog: [
 | 
			
		||||
    [
 | 
			
		||||
      // Dialog slide up
 | 
			
		||||
      [{ transform: "translateY(50px)" }, { transform: "translateY(0)" }],
 | 
			
		||||
      { duration: 500, easing: "cubic-bezier(.3,0,0,1)" },
 | 
			
		||||
    ],
 | 
			
		||||
  ],
 | 
			
		||||
  container: [
 | 
			
		||||
    [
 | 
			
		||||
      // Container fade in
 | 
			
		||||
      [{ opacity: 0 }, { opacity: 1 }],
 | 
			
		||||
      { duration: 50, easing: "linear", pseudoElement: "::before" },
 | 
			
		||||
    ],
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const CLOSE_TO_BOTTOM_ANIMATION: DialogAnimation = {
 | 
			
		||||
  ...DIALOG_DEFAULT_CLOSE_ANIMATION,
 | 
			
		||||
  dialog: [
 | 
			
		||||
    [
 | 
			
		||||
      // Dialog slide down
 | 
			
		||||
      [{ transform: "translateY(0)" }, { transform: "translateY(50px)" }],
 | 
			
		||||
      { duration: 150, easing: "cubic-bezier(.3,0,0,1)" },
 | 
			
		||||
    ],
 | 
			
		||||
  ],
 | 
			
		||||
  container: [
 | 
			
		||||
    [
 | 
			
		||||
      // Container fade out
 | 
			
		||||
      [{ opacity: "1" }, { opacity: "0" }],
 | 
			
		||||
      { delay: 100, duration: 50, easing: "linear", pseudoElement: "::before" },
 | 
			
		||||
    ],
 | 
			
		||||
  ],
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getMobileOpenFromBottomAnimation = () => {
 | 
			
		||||
  const matches = window.matchMedia(
 | 
			
		||||
    "all and (max-width: 450px), all and (max-height: 500px)"
 | 
			
		||||
  ).matches;
 | 
			
		||||
  return matches ? OPEN_FROM_BOTTOM_ANIMATION : DIALOG_DEFAULT_OPEN_ANIMATION;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getMobileCloseToBottomAnimation = () => {
 | 
			
		||||
  const matches = window.matchMedia(
 | 
			
		||||
    "all and (max-width: 450px), all and (max-height: 500px)"
 | 
			
		||||
  ).matches;
 | 
			
		||||
  return matches ? CLOSE_TO_BOTTOM_ANIMATION : DIALOG_DEFAULT_CLOSE_ANIMATION;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-md-dialog": HaMdDialog;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										21
									
								
								src/components/ha-md-divider.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,21 @@
 | 
			
		||||
import { MdDivider } from "@material/web/divider/divider";
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-md-divider")
 | 
			
		||||
export class HaMdDivider extends MdDivider {
 | 
			
		||||
  static override styles = [
 | 
			
		||||
    ...super.styles,
 | 
			
		||||
    css`
 | 
			
		||||
      :host {
 | 
			
		||||
        --md-divider-color: var(--divider-color);
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-md-divider": HaMdDivider;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item";
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-list-item-new")
 | 
			
		||||
export class HaListItemNew extends MdListItem {
 | 
			
		||||
@customElement("ha-md-list-item")
 | 
			
		||||
export class HaMdListItem extends MdListItem {
 | 
			
		||||
  static override styles = [
 | 
			
		||||
    ...super.styles,
 | 
			
		||||
    css`
 | 
			
		||||
@@ -21,6 +21,6 @@ export class HaListItemNew extends MdListItem {
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-list-item-new": HaListItemNew;
 | 
			
		||||
    "ha-md-list-item": HaMdListItem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list";
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-list-new")
 | 
			
		||||
export class HaListNew extends MdList {
 | 
			
		||||
@customElement("ha-md-list")
 | 
			
		||||
export class HaMdList extends MdList {
 | 
			
		||||
  static override styles = [
 | 
			
		||||
    ...super.styles,
 | 
			
		||||
    css`
 | 
			
		||||
@@ -16,6 +16,6 @@ export class HaListNew extends MdList {
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-list-new": HaListNew;
 | 
			
		||||
    "ha-md-list": HaMdList;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -2,8 +2,8 @@ import { MdMenuItem } from "@material/web/menu/menu-item";
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-menu-item")
 | 
			
		||||
export class HaMenuItem extends MdMenuItem {
 | 
			
		||||
@customElement("ha-md-menu-item")
 | 
			
		||||
export class HaMdMenuItem extends MdMenuItem {
 | 
			
		||||
  @property({ attribute: false }) clickAction?: (item?: HTMLElement) => void;
 | 
			
		||||
 | 
			
		||||
  static override styles = [
 | 
			
		||||
@@ -41,6 +41,6 @@ export class HaMenuItem extends MdMenuItem {
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-menu-item": HaMenuItem;
 | 
			
		||||
    "ha-md-menu-item": HaMdMenuItem;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -6,7 +6,7 @@ import {
 | 
			
		||||
} from "@material/web/menu/internal/controllers/shared";
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators";
 | 
			
		||||
import type { HaMenuItem } from "./ha-menu-item";
 | 
			
		||||
import type { HaMdMenuItem } from "./ha-md-menu-item";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-menu")
 | 
			
		||||
export class HaMenu extends MdMenu {
 | 
			
		||||
@@ -22,7 +22,7 @@ export class HaMenu extends MdMenu {
 | 
			
		||||
    ) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    (ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator);
 | 
			
		||||
    (ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static override styles = [
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,11 @@ export class HaOutlinedField extends MdOutlinedField {
 | 
			
		||||
      }
 | 
			
		||||
      .with-start .start {
 | 
			
		||||
        margin-inline-end: var(--ha-outlined-field-start-margin, 4px);
 | 
			
		||||
        margin-inline-start: initial;
 | 
			
		||||
      }
 | 
			
		||||
      .with-end .end {
 | 
			
		||||
        margin-inline-start: var(--ha-outlined-field-end-margin, 4px);
 | 
			
		||||
        margin-inline-end: initial;
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
 
 | 
			
		||||
@@ -1,22 +0,0 @@
 | 
			
		||||
import { MdOutlinedSegmentedButtonSet } from "@material/web/labs/segmentedbuttonset/outlined-segmented-button-set";
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-outlined-segmented-button-set")
 | 
			
		||||
export class HaOutlinedSegmentedButtonSet extends MdOutlinedSegmentedButtonSet {
 | 
			
		||||
  static override styles = [
 | 
			
		||||
    ...super.styles,
 | 
			
		||||
    css`
 | 
			
		||||
      :host {
 | 
			
		||||
        --ha-icon-display: block;
 | 
			
		||||
        --md-outlined-segmented-button-container-height: 32px;
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-outlined-segmented-button-set": HaOutlinedSegmentedButtonSet;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
import { MdOutlinedSegmentedButton } from "@material/web/labs/segmentedbutton/outlined-segmented-button";
 | 
			
		||||
import { css } from "lit";
 | 
			
		||||
import { customElement } from "lit/decorators";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-outlined-segmented-button")
 | 
			
		||||
export class HaOutlinedSegmentedButton extends MdOutlinedSegmentedButton {
 | 
			
		||||
  static override styles = [
 | 
			
		||||
    ...super.styles,
 | 
			
		||||
    css`
 | 
			
		||||
      :host {
 | 
			
		||||
        --ha-icon-display: block;
 | 
			
		||||
        --md-outlined-segmented-button-selected-container-color: var(
 | 
			
		||||
          --light-primary-color
 | 
			
		||||
        );
 | 
			
		||||
        --md-outlined-segmented-button-container-height: 32px;
 | 
			
		||||
        --md-outlined-segmented-button-disabled-label-text-color: var(
 | 
			
		||||
          --disabled-text-color
 | 
			
		||||
        );
 | 
			
		||||
        --md-outlined-segmented-button-disabled-icon-color: var(
 | 
			
		||||
          --disabled-text-color
 | 
			
		||||
        );
 | 
			
		||||
        --md-outlined-segmented-button-disabled-outline-color: var(
 | 
			
		||||
          --disabled-text-color
 | 
			
		||||
        );
 | 
			
		||||
      }
 | 
			
		||||
    `,
 | 
			
		||||
  ];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-outlined-segmented-button": HaOutlinedSegmentedButton;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
							
								
								
									
										185
									
								
								src/components/ha-password-field.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,185 @@
 | 
			
		||||
import { TextAreaCharCounter } from "@material/mwc-textfield/mwc-textfield-base";
 | 
			
		||||
import { mdiEye, mdiEyeOff } from "@mdi/js";
 | 
			
		||||
import { LitElement, css, html } from "lit";
 | 
			
		||||
import {
 | 
			
		||||
  customElement,
 | 
			
		||||
  eventOptions,
 | 
			
		||||
  property,
 | 
			
		||||
  query,
 | 
			
		||||
  state,
 | 
			
		||||
} from "lit/decorators";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import "./ha-icon-button";
 | 
			
		||||
import "./ha-textfield";
 | 
			
		||||
import type { HaTextField } from "./ha-textfield";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-password-field")
 | 
			
		||||
export class HaPasswordField extends LitElement {
 | 
			
		||||
  @property({ attribute: false }) public hass?: HomeAssistant;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public invalid?: boolean;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: "error-message" }) public errorMessage?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public icon = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) public iconTrailing = false;
 | 
			
		||||
 | 
			
		||||
  @property() public autocomplete?: string;
 | 
			
		||||
 | 
			
		||||
  @property() public autocorrect?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: "input-spellcheck" })
 | 
			
		||||
  public inputSpellcheck?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) value = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) placeholder = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) label = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, reflect: true }) disabled = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) required = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Number }) minLength = -1;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Number }) maxLength = -1;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, reflect: true }) outlined = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) helper = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) validateOnInitialRender = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) validationMessage = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) autoValidate = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) pattern = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: Number }) size: number | null = null;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) helperPersistent = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) charCounter: boolean | TextAreaCharCounter =
 | 
			
		||||
    false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) endAligned = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) prefix = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) suffix = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) name = "";
 | 
			
		||||
 | 
			
		||||
  @property({ type: String, attribute: "input-mode" })
 | 
			
		||||
  inputMode!: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean }) readOnly = false;
 | 
			
		||||
 | 
			
		||||
  @property({ type: String }) autocapitalize = "";
 | 
			
		||||
 | 
			
		||||
  @state() private _unmaskedPassword = false;
 | 
			
		||||
 | 
			
		||||
  @query("ha-textfield") private _textField!: HaTextField;
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    return html`<ha-textfield
 | 
			
		||||
        .invalid=${this.invalid}
 | 
			
		||||
        .errorMessage=${this.errorMessage}
 | 
			
		||||
        .icon=${this.icon}
 | 
			
		||||
        .iconTrailing=${this.iconTrailing}
 | 
			
		||||
        .autocomplete=${this.autocomplete}
 | 
			
		||||
        .autocorrect=${this.autocorrect}
 | 
			
		||||
        .inputSpellcheck=${this.inputSpellcheck}
 | 
			
		||||
        .value=${this.value}
 | 
			
		||||
        .placeholder=${this.placeholder}
 | 
			
		||||
        .label=${this.label}
 | 
			
		||||
        .disabled=${this.disabled}
 | 
			
		||||
        .required=${this.required}
 | 
			
		||||
        .minLength=${this.minLength}
 | 
			
		||||
        .maxLength=${this.maxLength}
 | 
			
		||||
        .outlined=${this.outlined}
 | 
			
		||||
        .helper=${this.helper}
 | 
			
		||||
        .validateOnInitialRender=${this.validateOnInitialRender}
 | 
			
		||||
        .validationMessage=${this.validationMessage}
 | 
			
		||||
        .autoValidate=${this.autoValidate}
 | 
			
		||||
        .pattern=${this.pattern}
 | 
			
		||||
        .size=${this.size}
 | 
			
		||||
        .helperPersistent=${this.helperPersistent}
 | 
			
		||||
        .charCounter=${this.charCounter}
 | 
			
		||||
        .endAligned=${this.endAligned}
 | 
			
		||||
        .prefix=${this.prefix}
 | 
			
		||||
        .name=${this.name}
 | 
			
		||||
        .inputMode=${this.inputMode}
 | 
			
		||||
        .readOnly=${this.readOnly}
 | 
			
		||||
        .autocapitalize=${this.autocapitalize}
 | 
			
		||||
        .type=${this._unmaskedPassword ? "text" : "password"}
 | 
			
		||||
        .suffix=${html`<div style="width: 24px"></div>`}
 | 
			
		||||
        @input=${this._handleInputChange}
 | 
			
		||||
      ></ha-textfield>
 | 
			
		||||
      <ha-icon-button
 | 
			
		||||
        toggles
 | 
			
		||||
        .label=${this.hass?.localize(
 | 
			
		||||
          this._unmaskedPassword
 | 
			
		||||
            ? "ui.components.selectors.text.hide_password"
 | 
			
		||||
            : "ui.components.selectors.text.show_password"
 | 
			
		||||
        ) || (this._unmaskedPassword ? "Hide password" : "Show password")}
 | 
			
		||||
        @click=${this._toggleUnmaskedPassword}
 | 
			
		||||
        .path=${this._unmaskedPassword ? mdiEyeOff : mdiEye}
 | 
			
		||||
      ></ha-icon-button>`;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public checkValidity(): boolean {
 | 
			
		||||
    return this._textField.checkValidity();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public reportValidity(): boolean {
 | 
			
		||||
    return this._textField.reportValidity();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public setCustomValidity(message: string): void {
 | 
			
		||||
    return this._textField.setCustomValidity(message);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public layout(): Promise<void> {
 | 
			
		||||
    return this._textField.layout();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _toggleUnmaskedPassword(): void {
 | 
			
		||||
    this._unmaskedPassword = !this._unmaskedPassword;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  @eventOptions({ passive: true })
 | 
			
		||||
  private _handleInputChange(ev) {
 | 
			
		||||
    this.value = ev.target.value;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static styles = css`
 | 
			
		||||
    :host {
 | 
			
		||||
      display: block;
 | 
			
		||||
      position: relative;
 | 
			
		||||
    }
 | 
			
		||||
    ha-textfield {
 | 
			
		||||
      width: 100%;
 | 
			
		||||
    }
 | 
			
		||||
    ha-icon-button {
 | 
			
		||||
      position: absolute;
 | 
			
		||||
      top: 8px;
 | 
			
		||||
      right: 8px;
 | 
			
		||||
      inset-inline-start: initial;
 | 
			
		||||
      inset-inline-end: 8px;
 | 
			
		||||
      --mdc-icon-button-size: 40px;
 | 
			
		||||
      --mdc-icon-size: 20px;
 | 
			
		||||
      color: var(--secondary-text-color);
 | 
			
		||||
      direction: var(--direction);
 | 
			
		||||
    }
 | 
			
		||||
  `;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
declare global {
 | 
			
		||||
  interface HTMLElementTagNameMap {
 | 
			
		||||
    "ha-password-field": HaPasswordField;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
import { Action } from "../../data/script";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { Action, migrateAutomationAction } from "../../data/script";
 | 
			
		||||
import { ActionSelector } from "../../data/selector";
 | 
			
		||||
import "../../panels/config/automation/action/ha-automation-action";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
@@ -17,12 +18,19 @@ export class HaActionSelector extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, reflect: true }) public disabled = false;
 | 
			
		||||
 | 
			
		||||
  private _actions = memoizeOne((action: Action | undefined) => {
 | 
			
		||||
    if (!action) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    return migrateAutomationAction(action);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    return html`
 | 
			
		||||
      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
			
		||||
      <ha-automation-action
 | 
			
		||||
        .disabled=${this.disabled}
 | 
			
		||||
        .actions=${this.value || []}
 | 
			
		||||
        .actions=${this._actions(this.value)}
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .path=${this.selector.action?.path}
 | 
			
		||||
      ></ha-automation-action>
 | 
			
		||||
 
 | 
			
		||||
@@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement {
 | 
			
		||||
        .label=${this.label || ""}
 | 
			
		||||
        .required=${this.required}
 | 
			
		||||
        .helper=${this.helper}
 | 
			
		||||
        .disalbled=${this.disabled}
 | 
			
		||||
        .disabled=${this.disabled}
 | 
			
		||||
        @change=${this._valueChanged}
 | 
			
		||||
      ></ha-textfield>
 | 
			
		||||
    `;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,12 +7,7 @@ import "../ha-code-editor";
 | 
			
		||||
import "../ha-input-helper-text";
 | 
			
		||||
import "../ha-alert";
 | 
			
		||||
 | 
			
		||||
const WARNING_STRINGS = [
 | 
			
		||||
  "template:",
 | 
			
		||||
  "sensor:",
 | 
			
		||||
  "state:",
 | 
			
		||||
  "platform: template",
 | 
			
		||||
];
 | 
			
		||||
const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"];
 | 
			
		||||
 | 
			
		||||
@customElement("ha-selector-template")
 | 
			
		||||
export class HaTemplateSelector extends LitElement {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
import { css, CSSResultGroup, html, LitElement, nothing } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
import { Trigger } from "../../data/automation";
 | 
			
		||||
import memoizeOne from "memoize-one";
 | 
			
		||||
import { migrateAutomationTrigger, Trigger } from "../../data/automation";
 | 
			
		||||
import { TriggerSelector } from "../../data/selector";
 | 
			
		||||
import "../../panels/config/automation/trigger/ha-automation-trigger";
 | 
			
		||||
import { HomeAssistant } from "../../types";
 | 
			
		||||
@@ -17,12 +18,19 @@ export class HaTriggerSelector extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @property({ type: Boolean, reflect: true }) public disabled = false;
 | 
			
		||||
 | 
			
		||||
  private _triggers = memoizeOne((trigger: Trigger | undefined) => {
 | 
			
		||||
    if (!trigger) {
 | 
			
		||||
      return [];
 | 
			
		||||
    }
 | 
			
		||||
    return migrateAutomationTrigger(trigger);
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    return html`
 | 
			
		||||
      ${this.label ? html`<label>${this.label}</label>` : nothing}
 | 
			
		||||
      <ha-automation-trigger
 | 
			
		||||
        .disabled=${this.disabled}
 | 
			
		||||
        .triggers=${this.value || []}
 | 
			
		||||
        .triggers=${this._triggers(this.value)}
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .path=${this.selector.trigger?.path}
 | 
			
		||||
      ></ha-automation-trigger>
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,8 @@ export class HaSelectorUiColor extends LitElement {
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .value=${this.value}
 | 
			
		||||
        .helper=${this.helper}
 | 
			
		||||
        .includeNone=${this.selector.ui_color?.include_none}
 | 
			
		||||
        .includeState=${this.selector.ui_color?.include_state}
 | 
			
		||||
        .defaultColor=${this.selector.ui_color?.default_color}
 | 
			
		||||
        @value-changed=${this._valueChanged}
 | 
			
		||||
      ></ha-color-picker>
 | 
			
		||||
 
 | 
			
		||||
@@ -240,12 +240,24 @@ export class HaServiceControl extends LitElement {
 | 
			
		||||
        ...value,
 | 
			
		||||
        selector: value.selector as Selector | undefined,
 | 
			
		||||
      }));
 | 
			
		||||
 | 
			
		||||
      const hasSelector: string[] = [];
 | 
			
		||||
      fields.forEach((field) => {
 | 
			
		||||
        if ((field as any).fields) {
 | 
			
		||||
          Object.entries((field as any).fields).forEach(([key, subField]) => {
 | 
			
		||||
            if ((subField as any).selector) {
 | 
			
		||||
              hasSelector.push(key);
 | 
			
		||||
            }
 | 
			
		||||
          });
 | 
			
		||||
        } else if (field.selector) {
 | 
			
		||||
          hasSelector.push(field.key);
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
 | 
			
		||||
      return {
 | 
			
		||||
        ...serviceDomains[domain][serviceName],
 | 
			
		||||
        fields,
 | 
			
		||||
        hasSelector: fields.length
 | 
			
		||||
          ? fields.filter((field) => field.selector).map((field) => field.key)
 | 
			
		||||
          : [],
 | 
			
		||||
        hasSelector,
 | 
			
		||||
      };
 | 
			
		||||
    }
 | 
			
		||||
  );
 | 
			
		||||
@@ -793,7 +805,8 @@ export class HaServiceControl extends LitElement {
 | 
			
		||||
    const value = ev.detail.value;
 | 
			
		||||
    if (
 | 
			
		||||
      this._value?.data?.[key] === value ||
 | 
			
		||||
      (!this._value?.data?.[key] && (value === "" || value === undefined))
 | 
			
		||||
      ((!this._value?.data || !(key in this._value.data)) &&
 | 
			
		||||
        (value === "" || value === undefined))
 | 
			
		||||
    ) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -43,6 +43,13 @@ export class HaSortable extends LitElement {
 | 
			
		||||
  @property({ type: String, attribute: "handle-selector" })
 | 
			
		||||
  public handleSelector?: string;
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Selectors that do not lead to dragging (String or Function)
 | 
			
		||||
   * https://github.com/SortableJS/Sortable?tab=readme-ov-file#filter-option
 | 
			
		||||
   * */
 | 
			
		||||
  @property({ type: String, attribute: "filter" })
 | 
			
		||||
  public filter?: string;
 | 
			
		||||
 | 
			
		||||
  @property({ type: String })
 | 
			
		||||
  public group?: string | SortableInstance.GroupOptions;
 | 
			
		||||
 | 
			
		||||
@@ -145,6 +152,9 @@ export class HaSortable extends LitElement {
 | 
			
		||||
    if (this.group) {
 | 
			
		||||
      options.group = this.group;
 | 
			
		||||
    }
 | 
			
		||||
    if (this.filter) {
 | 
			
		||||
      options.filter = this.filter;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this._sortable = new Sortable(container, options);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -35,10 +35,6 @@ import {
 | 
			
		||||
  computeDeviceName,
 | 
			
		||||
} from "../data/device_registry";
 | 
			
		||||
import { EntityRegistryDisplayEntry } from "../data/entity_registry";
 | 
			
		||||
import {
 | 
			
		||||
  FloorRegistryEntry,
 | 
			
		||||
  subscribeFloorRegistry,
 | 
			
		||||
} from "../data/floor_registry";
 | 
			
		||||
import {
 | 
			
		||||
  LabelRegistryEntry,
 | 
			
		||||
  subscribeLabelRegistry,
 | 
			
		||||
@@ -103,17 +99,12 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
 | 
			
		||||
  @query(".add-container", true) private _addContainer?: HTMLDivElement;
 | 
			
		||||
 | 
			
		||||
  @state() private _floors?: FloorRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  @state() private _labels?: LabelRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  private _opened = false;
 | 
			
		||||
 | 
			
		||||
  protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] {
 | 
			
		||||
    return [
 | 
			
		||||
      subscribeFloorRegistry(this.hass.connection, (floors) => {
 | 
			
		||||
        this._floors = floors;
 | 
			
		||||
      }),
 | 
			
		||||
      subscribeLabelRegistry(this.hass.connection, (labels) => {
 | 
			
		||||
        this._labels = labels;
 | 
			
		||||
      }),
 | 
			
		||||
@@ -132,9 +123,7 @@ export class HaTargetPicker extends SubscribeMixin(LitElement) {
 | 
			
		||||
      <div class="mdc-chip-set items">
 | 
			
		||||
        ${this.value?.floor_id
 | 
			
		||||
          ? ensureArray(this.value.floor_id).map((floor_id) => {
 | 
			
		||||
              const floor = this._floors?.find(
 | 
			
		||||
                (flr) => flr.floor_id === floor_id
 | 
			
		||||
              );
 | 
			
		||||
              const floor = this.hass.floors[floor_id];
 | 
			
		||||
              return this._renderChip(
 | 
			
		||||
                "floor_id",
 | 
			
		||||
                floor_id,
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ import { mainWindow } from "../common/dom/get_main_window";
 | 
			
		||||
 | 
			
		||||
@customElement("ha-textfield")
 | 
			
		||||
export class HaTextField extends TextFieldBase {
 | 
			
		||||
  @property({ type: Boolean }) public invalid = false;
 | 
			
		||||
  @property({ type: Boolean }) public invalid?: boolean;
 | 
			
		||||
 | 
			
		||||
  @property({ attribute: "error-message" }) public errorMessage?: string;
 | 
			
		||||
 | 
			
		||||
@@ -28,14 +28,24 @@ export class HaTextField extends TextFieldBase {
 | 
			
		||||
  override updated(changedProperties: PropertyValues) {
 | 
			
		||||
    super.updated(changedProperties);
 | 
			
		||||
    if (
 | 
			
		||||
      (changedProperties.has("invalid") &&
 | 
			
		||||
        (this.invalid || changedProperties.get("invalid") !== undefined)) ||
 | 
			
		||||
      changedProperties.has("invalid") ||
 | 
			
		||||
      changedProperties.has("errorMessage")
 | 
			
		||||
    ) {
 | 
			
		||||
      this.setCustomValidity(
 | 
			
		||||
        this.invalid ? this.errorMessage || "Invalid" : ""
 | 
			
		||||
        this.invalid
 | 
			
		||||
          ? this.errorMessage || this.validationMessage || "Invalid"
 | 
			
		||||
          : ""
 | 
			
		||||
      );
 | 
			
		||||
      this.reportValidity();
 | 
			
		||||
      if (
 | 
			
		||||
        this.invalid ||
 | 
			
		||||
        this.validateOnInitialRender ||
 | 
			
		||||
        (changedProperties.has("invalid") &&
 | 
			
		||||
          changedProperties.get("invalid") !== undefined)
 | 
			
		||||
      ) {
 | 
			
		||||
        // Only report validity if the field is invalid or the invalid state has changed from
 | 
			
		||||
        // true to false to prevent setting empty required fields to invalid on first render
 | 
			
		||||
        this.reportValidity();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (changedProperties.has("autocomplete")) {
 | 
			
		||||
      if (this.autocomplete) {
 | 
			
		||||
@@ -109,7 +119,7 @@ export class HaTextField extends TextFieldBase {
 | 
			
		||||
        color: var(--secondary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      .mdc-text-field__icon {
 | 
			
		||||
      .mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon {
 | 
			
		||||
        color: var(--secondary-text-color);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
/* eslint-disable no-console */
 | 
			
		||||
import {
 | 
			
		||||
  css,
 | 
			
		||||
  CSSResultGroup,
 | 
			
		||||
@@ -6,11 +7,14 @@ import {
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
  TemplateResult,
 | 
			
		||||
} from "lit";
 | 
			
		||||
import { customElement, property, state, query } from "lit/decorators";
 | 
			
		||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { ifDefined } from "lit/directives/if-defined";
 | 
			
		||||
import { fireEvent } from "../common/dom/fire_event";
 | 
			
		||||
import { handleWebRtcOffer, WebRtcAnswer } from "../data/camera";
 | 
			
		||||
import { fetchWebRtcSettings } from "../data/rtsp_to_webrtc";
 | 
			
		||||
import {
 | 
			
		||||
  fetchWebRtcClientConfiguration,
 | 
			
		||||
  handleWebRtcOffer,
 | 
			
		||||
  WebRtcAnswer,
 | 
			
		||||
} from "../data/camera";
 | 
			
		||||
import type { HomeAssistant } from "../types";
 | 
			
		||||
import "./ha-alert";
 | 
			
		||||
 | 
			
		||||
@@ -37,12 +41,11 @@ class HaWebRtcPlayer extends LitElement {
 | 
			
		||||
  @property({ type: Boolean, attribute: "playsinline" })
 | 
			
		||||
  public playsInline = false;
 | 
			
		||||
 | 
			
		||||
  @property() public posterUrl!: string;
 | 
			
		||||
  @property({ attribute: "poster-url" }) public posterUrl?: string;
 | 
			
		||||
 | 
			
		||||
  @state() private _error?: string;
 | 
			
		||||
 | 
			
		||||
  // don't cache this, as we remove it on disconnects
 | 
			
		||||
  @query("#remote-stream") private _videoEl!: HTMLVideoElement;
 | 
			
		||||
  @query("#remote-stream", true) private _videoEl!: HTMLVideoElement;
 | 
			
		||||
 | 
			
		||||
  private _peerConnection?: RTCPeerConnection;
 | 
			
		||||
 | 
			
		||||
@@ -59,7 +62,7 @@ class HaWebRtcPlayer extends LitElement {
 | 
			
		||||
        .muted=${this.muted}
 | 
			
		||||
        ?playsinline=${this.playsInline}
 | 
			
		||||
        ?controls=${this.controls}
 | 
			
		||||
        .poster=${this.posterUrl}
 | 
			
		||||
        poster=${ifDefined(this.posterUrl)}
 | 
			
		||||
        @loadeddata=${this._loadedData}
 | 
			
		||||
      ></video>
 | 
			
		||||
    `;
 | 
			
		||||
@@ -81,20 +84,30 @@ class HaWebRtcPlayer extends LitElement {
 | 
			
		||||
    if (!changedProperties.has("entityid")) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (!this._videoEl) {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this._startWebRtc();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _startWebRtc(): Promise<void> {
 | 
			
		||||
    console.time("WebRTC");
 | 
			
		||||
 | 
			
		||||
    this._error = undefined;
 | 
			
		||||
 | 
			
		||||
    const configuration = await this._fetchPeerConfiguration();
 | 
			
		||||
    const peerConnection = new RTCPeerConnection(configuration);
 | 
			
		||||
    // Some cameras (such as nest) require a data channel to establish a stream
 | 
			
		||||
    // however, not used by any integrations.
 | 
			
		||||
    peerConnection.createDataChannel("dataSendChannel");
 | 
			
		||||
    console.timeLog("WebRTC", "start clientConfig");
 | 
			
		||||
 | 
			
		||||
    const clientConfig = await fetchWebRtcClientConfiguration(
 | 
			
		||||
      this.hass,
 | 
			
		||||
      this.entityid
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    console.timeLog("WebRTC", "end clientConfig", clientConfig);
 | 
			
		||||
 | 
			
		||||
    const peerConnection = new RTCPeerConnection(clientConfig.configuration);
 | 
			
		||||
 | 
			
		||||
    if (clientConfig.dataChannel) {
 | 
			
		||||
      // Some cameras (such as nest) require a data channel to establish a stream
 | 
			
		||||
      // however, not used by any integrations.
 | 
			
		||||
      peerConnection.createDataChannel(clientConfig.dataChannel);
 | 
			
		||||
    }
 | 
			
		||||
    peerConnection.addTransceiver("audio", { direction: "recvonly" });
 | 
			
		||||
    peerConnection.addTransceiver("video", { direction: "recvonly" });
 | 
			
		||||
 | 
			
		||||
@@ -102,30 +115,48 @@ class HaWebRtcPlayer extends LitElement {
 | 
			
		||||
      offerToReceiveAudio: true,
 | 
			
		||||
      offerToReceiveVideo: true,
 | 
			
		||||
    };
 | 
			
		||||
 | 
			
		||||
    console.timeLog("WebRTC", "start createOffer", offerOptions);
 | 
			
		||||
 | 
			
		||||
    const offer: RTCSessionDescriptionInit =
 | 
			
		||||
      await peerConnection.createOffer(offerOptions);
 | 
			
		||||
 | 
			
		||||
    console.timeLog("WebRTC", "end createOffer", offer);
 | 
			
		||||
 | 
			
		||||
    console.timeLog("WebRTC", "start setLocalDescription");
 | 
			
		||||
 | 
			
		||||
    await peerConnection.setLocalDescription(offer);
 | 
			
		||||
 | 
			
		||||
    console.timeLog("WebRTC", "end setLocalDescription");
 | 
			
		||||
 | 
			
		||||
    console.timeLog("WebRTC", "start iceResolver");
 | 
			
		||||
 | 
			
		||||
    let candidates = ""; // Build an Offer SDP string with ice candidates
 | 
			
		||||
    const iceResolver = new Promise<void>((resolve) => {
 | 
			
		||||
      peerConnection.addEventListener("icecandidate", async (event) => {
 | 
			
		||||
      peerConnection.addEventListener("icecandidate", (event) => {
 | 
			
		||||
        if (!event.candidate?.candidate) {
 | 
			
		||||
          resolve(); // Gathering complete
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        console.timeLog("WebRTC", "iceResolver candidate", event.candidate);
 | 
			
		||||
        candidates += `a=${event.candidate.candidate}\r\n`;
 | 
			
		||||
      });
 | 
			
		||||
    });
 | 
			
		||||
    await iceResolver;
 | 
			
		||||
 | 
			
		||||
    console.timeLog("WebRTC", "end iceResolver", candidates);
 | 
			
		||||
 | 
			
		||||
    const offer_sdp = offer.sdp! + candidates;
 | 
			
		||||
 | 
			
		||||
    let webRtcAnswer: WebRtcAnswer;
 | 
			
		||||
    try {
 | 
			
		||||
      console.timeLog("WebRTC", "start WebRTCOffer", offer_sdp);
 | 
			
		||||
      webRtcAnswer = await handleWebRtcOffer(
 | 
			
		||||
        this.hass,
 | 
			
		||||
        this.entityid,
 | 
			
		||||
        offer_sdp
 | 
			
		||||
      );
 | 
			
		||||
      console.timeLog("WebRTC", "end webRtcOffer", webRtcAnswer);
 | 
			
		||||
    } catch (err: any) {
 | 
			
		||||
      this._error = "Failed to start WebRTC stream: " + err.message;
 | 
			
		||||
      peerConnection.close();
 | 
			
		||||
@@ -135,6 +166,7 @@ class HaWebRtcPlayer extends LitElement {
 | 
			
		||||
    // Setup callbacks to render remote stream once media tracks are discovered.
 | 
			
		||||
    const remoteStream = new MediaStream();
 | 
			
		||||
    peerConnection.addEventListener("track", (event) => {
 | 
			
		||||
      console.timeLog("WebRTC", "track", event);
 | 
			
		||||
      remoteStream.addTrack(event.track);
 | 
			
		||||
      this._videoEl.srcObject = remoteStream;
 | 
			
		||||
    });
 | 
			
		||||
@@ -146,7 +178,9 @@ class HaWebRtcPlayer extends LitElement {
 | 
			
		||||
      sdp: webRtcAnswer.answer,
 | 
			
		||||
    });
 | 
			
		||||
    try {
 | 
			
		||||
      console.timeLog("WebRTC", "start setRemoteDescription", remoteDesc);
 | 
			
		||||
      await peerConnection.setRemoteDescription(remoteDesc);
 | 
			
		||||
      console.timeLog("WebRTC", "end setRemoteDescription");
 | 
			
		||||
    } catch (err: any) {
 | 
			
		||||
      this._error = "Failed to connect WebRTC stream: " + err.message;
 | 
			
		||||
      peerConnection.close();
 | 
			
		||||
@@ -155,23 +189,6 @@ class HaWebRtcPlayer extends LitElement {
 | 
			
		||||
    this._peerConnection = peerConnection;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private async _fetchPeerConfiguration(): Promise<RTCConfiguration> {
 | 
			
		||||
    if (!isComponentLoaded(this.hass!, "rtsp_to_webrtc")) {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
    const settings = await fetchWebRtcSettings(this.hass!);
 | 
			
		||||
    if (!settings || !settings.stun_server) {
 | 
			
		||||
      return {};
 | 
			
		||||
    }
 | 
			
		||||
    return {
 | 
			
		||||
      iceServers: [
 | 
			
		||||
        {
 | 
			
		||||
          urls: [`stun:${settings.stun_server!}`],
 | 
			
		||||
        },
 | 
			
		||||
      ],
 | 
			
		||||
    };
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _cleanUp() {
 | 
			
		||||
    if (this._remoteStream) {
 | 
			
		||||
      this._remoteStream.getTracks().forEach((track) => {
 | 
			
		||||
@@ -190,6 +207,8 @@ class HaWebRtcPlayer extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  private _loadedData() {
 | 
			
		||||
    console.timeLog("WebRTC", "loadedData");
 | 
			
		||||
    console.timeEnd("WebRTC");
 | 
			
		||||
    // @ts-ignore
 | 
			
		||||
    fireEvent(this, "load");
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,16 +7,18 @@ import {
 | 
			
		||||
  nothing,
 | 
			
		||||
  PropertyValues,
 | 
			
		||||
} from "lit";
 | 
			
		||||
import { customElement, property, state } from "lit/decorators";
 | 
			
		||||
import { customElement, property, query, state } from "lit/decorators";
 | 
			
		||||
import { fireEvent } from "../common/dom/fire_event";
 | 
			
		||||
import type { HomeAssistant } from "../types";
 | 
			
		||||
import { haStyle } from "../resources/styles";
 | 
			
		||||
import "./ha-code-editor";
 | 
			
		||||
import { showToast } from "../util/toast";
 | 
			
		||||
import { copyToClipboard } from "../common/util/copy-clipboard";
 | 
			
		||||
import type { HaCodeEditor } from "./ha-code-editor";
 | 
			
		||||
import "./ha-button";
 | 
			
		||||
 | 
			
		||||
const isEmpty = (obj: Record<string, unknown>): boolean => {
 | 
			
		||||
  if (typeof obj !== "object") {
 | 
			
		||||
  if (typeof obj !== "object" || obj === null) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  for (const key in obj) {
 | 
			
		||||
@@ -53,16 +55,17 @@ export class HaYamlEditor extends LitElement {
 | 
			
		||||
 | 
			
		||||
  @state() private _yaml = "";
 | 
			
		||||
 | 
			
		||||
  @query("ha-code-editor") _codeEditor?: HaCodeEditor;
 | 
			
		||||
 | 
			
		||||
  public setValue(value): void {
 | 
			
		||||
    try {
 | 
			
		||||
      this._yaml =
 | 
			
		||||
        value && !isEmpty(value)
 | 
			
		||||
          ? dump(value, {
 | 
			
		||||
              schema: this.yamlSchema,
 | 
			
		||||
              quotingType: '"',
 | 
			
		||||
              noRefs: true,
 | 
			
		||||
            })
 | 
			
		||||
          : "";
 | 
			
		||||
      this._yaml = !isEmpty(value)
 | 
			
		||||
        ? dump(value, {
 | 
			
		||||
            schema: this.yamlSchema,
 | 
			
		||||
            quotingType: '"',
 | 
			
		||||
            noRefs: true,
 | 
			
		||||
          })
 | 
			
		||||
        : "";
 | 
			
		||||
    } catch (err: any) {
 | 
			
		||||
      // eslint-disable-next-line no-console
 | 
			
		||||
      console.error(err, value);
 | 
			
		||||
@@ -71,7 +74,7 @@ export class HaYamlEditor extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected firstUpdated(): void {
 | 
			
		||||
    if (this.defaultValue) {
 | 
			
		||||
    if (this.defaultValue !== undefined) {
 | 
			
		||||
      this.setValue(this.defaultValue);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -83,6 +86,12 @@ export class HaYamlEditor extends LitElement {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  public focus(): void {
 | 
			
		||||
    if (this._codeEditor?.codemirror) {
 | 
			
		||||
      this._codeEditor?.codemirror.focus();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    if (this._yaml === undefined) {
 | 
			
		||||
      return nothing;
 | 
			
		||||
@@ -90,7 +99,7 @@ export class HaYamlEditor extends LitElement {
 | 
			
		||||
    return html`
 | 
			
		||||
      ${this.label
 | 
			
		||||
        ? html`<p>${this.label}${this.required ? " *" : ""}</p>`
 | 
			
		||||
        : ""}
 | 
			
		||||
        : nothing}
 | 
			
		||||
      <ha-code-editor
 | 
			
		||||
        .hass=${this.hass}
 | 
			
		||||
        .value=${this._yaml}
 | 
			
		||||
@@ -103,16 +112,20 @@ export class HaYamlEditor extends LitElement {
 | 
			
		||||
        dir="ltr"
 | 
			
		||||
      ></ha-code-editor>
 | 
			
		||||
      ${this.copyClipboard || this.hasExtraActions
 | 
			
		||||
        ? html`<div class="card-actions">
 | 
			
		||||
            ${this.copyClipboard
 | 
			
		||||
              ? html` <mwc-button @click=${this._copyYaml}>
 | 
			
		||||
                  ${this.hass.localize(
 | 
			
		||||
                    "ui.components.yaml-editor.copy_to_clipboard"
 | 
			
		||||
                  )}
 | 
			
		||||
                </mwc-button>`
 | 
			
		||||
              : nothing}
 | 
			
		||||
            <slot name="extra-actions"></slot>
 | 
			
		||||
          </div>`
 | 
			
		||||
        ? html`
 | 
			
		||||
            <div class="card-actions">
 | 
			
		||||
              ${this.copyClipboard
 | 
			
		||||
                ? html`
 | 
			
		||||
                    <ha-button @click=${this._copyYaml}>
 | 
			
		||||
                      ${this.hass.localize(
 | 
			
		||||
                        "ui.components.yaml-editor.copy_to_clipboard"
 | 
			
		||||
                      )}
 | 
			
		||||
                    </ha-button>
 | 
			
		||||
                  `
 | 
			
		||||
                : nothing}
 | 
			
		||||
              <slot name="extra-actions"></slot>
 | 
			
		||||
            </div>
 | 
			
		||||
          `
 | 
			
		||||
        : nothing}
 | 
			
		||||
    `;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ import { LitElement, PropertyValues, css, html, nothing } from "lit";
 | 
			
		||||
import { customElement, property } from "lit/decorators";
 | 
			
		||||
import { ensureArray } from "../../common/array/ensure-array";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { Condition, Trigger } from "../../data/automation";
 | 
			
		||||
import { Condition, Trigger, flattenTriggers } from "../../data/automation";
 | 
			
		||||
import {
 | 
			
		||||
  Action,
 | 
			
		||||
  ChooseAction,
 | 
			
		||||
@@ -94,7 +94,7 @@ export class HatScriptGraph extends LitElement {
 | 
			
		||||
        @focus=${this.selectNode(config, path)}
 | 
			
		||||
        ?active=${this.selected === path}
 | 
			
		||||
        .iconPath=${mdiAsterisk}
 | 
			
		||||
        .notEnabled=${config.enabled === false}
 | 
			
		||||
        .notEnabled=${"enabled" in config && config.enabled === false}
 | 
			
		||||
        .error=${this.trace.trace[path]?.some((tr) => tr.error)}
 | 
			
		||||
        tabindex=${track ? "0" : "-1"}
 | 
			
		||||
      ></hat-graph-node>
 | 
			
		||||
@@ -569,11 +569,16 @@ export class HatScriptGraph extends LitElement {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    const triggerKey = "triggers" in this.trace.config ? "triggers" : "trigger";
 | 
			
		||||
    const conditionKey =
 | 
			
		||||
      "conditions" in this.trace.config ? "conditions" : "condition";
 | 
			
		||||
    const actionKey = "actions" in this.trace.config ? "actions" : "action";
 | 
			
		||||
 | 
			
		||||
    const paths = Object.keys(this.trackedNodes);
 | 
			
		||||
    const trigger_nodes =
 | 
			
		||||
      "trigger" in this.trace.config
 | 
			
		||||
        ? ensureArray(this.trace.config.trigger).map((trigger, i) =>
 | 
			
		||||
            this.render_trigger(trigger, i)
 | 
			
		||||
      triggerKey in this.trace.config
 | 
			
		||||
        ? flattenTriggers(ensureArray(this.trace.config[triggerKey])).map(
 | 
			
		||||
            (trigger, i) => this.render_trigger(trigger, i)
 | 
			
		||||
          )
 | 
			
		||||
        : undefined;
 | 
			
		||||
    try {
 | 
			
		||||
@@ -584,14 +589,14 @@ export class HatScriptGraph extends LitElement {
 | 
			
		||||
                ${trigger_nodes}
 | 
			
		||||
              </hat-graph-branch>`
 | 
			
		||||
            : ""}
 | 
			
		||||
          ${"condition" in this.trace.config
 | 
			
		||||
            ? html`${ensureArray(this.trace.config.condition)?.map(
 | 
			
		||||
          ${conditionKey in this.trace.config
 | 
			
		||||
            ? html`${ensureArray(this.trace.config[conditionKey])?.map(
 | 
			
		||||
                (condition, i) => this.render_condition(condition, i)
 | 
			
		||||
              )}`
 | 
			
		||||
            : ""}
 | 
			
		||||
          ${"action" in this.trace.config
 | 
			
		||||
            ? html`${ensureArray(this.trace.config.action).map((action, i) =>
 | 
			
		||||
                this.render_action_node(action, `action/${i}`)
 | 
			
		||||
          ${actionKey in this.trace.config
 | 
			
		||||
            ? html`${ensureArray(this.trace.config[actionKey]).map(
 | 
			
		||||
                (action, i) => this.render_action_node(action, `action/${i}`)
 | 
			
		||||
              )}`
 | 
			
		||||
            : ""}
 | 
			
		||||
          ${"sequence" in this.trace.config
 | 
			
		||||
 
 | 
			
		||||
@@ -22,13 +22,8 @@ import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_tim
 | 
			
		||||
import { relativeTime } from "../../common/datetime/relative_time";
 | 
			
		||||
import { fireEvent } from "../../common/dom/fire_event";
 | 
			
		||||
import { toggleAttribute } from "../../common/dom/toggle_attribute";
 | 
			
		||||
import {
 | 
			
		||||
  floorsContext,
 | 
			
		||||
  fullEntitiesContext,
 | 
			
		||||
  labelsContext,
 | 
			
		||||
} from "../../data/context";
 | 
			
		||||
import { fullEntitiesContext, labelsContext } from "../../data/context";
 | 
			
		||||
import { EntityRegistryEntry } from "../../data/entity_registry";
 | 
			
		||||
import { FloorRegistryEntry } from "../../data/floor_registry";
 | 
			
		||||
import { LabelRegistryEntry } from "../../data/label_registry";
 | 
			
		||||
import { LogbookEntry } from "../../data/logbook";
 | 
			
		||||
import {
 | 
			
		||||
@@ -206,7 +201,6 @@ class ActionRenderer {
 | 
			
		||||
    private hass: HomeAssistant,
 | 
			
		||||
    private entityReg: EntityRegistryEntry[],
 | 
			
		||||
    private labelReg: LabelRegistryEntry[],
 | 
			
		||||
    private floorReg: FloorRegistryEntry[],
 | 
			
		||||
    private entries: TemplateResult[],
 | 
			
		||||
    private trace: AutomationTraceExtended,
 | 
			
		||||
    private logbookRenderer: LogbookRenderer,
 | 
			
		||||
@@ -325,7 +319,6 @@ class ActionRenderer {
 | 
			
		||||
        this.hass,
 | 
			
		||||
        this.entityReg,
 | 
			
		||||
        this.labelReg,
 | 
			
		||||
        this.floorReg,
 | 
			
		||||
        data,
 | 
			
		||||
        actionType
 | 
			
		||||
      ),
 | 
			
		||||
@@ -493,13 +486,7 @@ class ActionRenderer {
 | 
			
		||||
 | 
			
		||||
    const name =
 | 
			
		||||
      repeatConfig.alias ||
 | 
			
		||||
      describeAction(
 | 
			
		||||
        this.hass,
 | 
			
		||||
        this.entityReg,
 | 
			
		||||
        this.labelReg,
 | 
			
		||||
        this.floorReg,
 | 
			
		||||
        repeatConfig
 | 
			
		||||
      );
 | 
			
		||||
      describeAction(this.hass, this.entityReg, this.labelReg, repeatConfig);
 | 
			
		||||
 | 
			
		||||
    this._renderEntry(repeatPath, name, undefined, disabled);
 | 
			
		||||
 | 
			
		||||
@@ -597,7 +584,6 @@ class ActionRenderer {
 | 
			
		||||
          this.hass,
 | 
			
		||||
          this.entityReg,
 | 
			
		||||
          this.labelReg,
 | 
			
		||||
          this.floorReg,
 | 
			
		||||
          sequenceConfig,
 | 
			
		||||
          "sequence"
 | 
			
		||||
        ),
 | 
			
		||||
@@ -694,10 +680,6 @@ export class HaAutomationTracer extends LitElement {
 | 
			
		||||
  @consume({ context: labelsContext, subscribe: true })
 | 
			
		||||
  _labelReg!: LabelRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  @state()
 | 
			
		||||
  @consume({ context: floorsContext, subscribe: true })
 | 
			
		||||
  _floorReg!: FloorRegistryEntry[];
 | 
			
		||||
 | 
			
		||||
  protected render() {
 | 
			
		||||
    if (!this.trace) {
 | 
			
		||||
      return nothing;
 | 
			
		||||
@@ -715,7 +697,6 @@ export class HaAutomationTracer extends LitElement {
 | 
			
		||||
      this.hass,
 | 
			
		||||
      this._entityReg,
 | 
			
		||||
      this._labelReg,
 | 
			
		||||
      this._floorReg,
 | 
			
		||||
      entries,
 | 
			
		||||
      this.trace,
 | 
			
		||||
      logbookRenderer,
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										81
									
								
								src/data/assist_satellite.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						@@ -0,0 +1,81 @@
 | 
			
		||||
import { HassEntity } from "home-assistant-js-websocket";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import { supportsFeature } from "../common/entity/supports-feature";
 | 
			
		||||
import { UNAVAILABLE } from "./entity";
 | 
			
		||||
 | 
			
		||||
export const enum AssistSatelliteEntityFeature {
 | 
			
		||||
  ANNOUNCE = 1,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WakeWordInterceptMessage {
 | 
			
		||||
  wake_word_phrase: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WakeWordOption {
 | 
			
		||||
  id: string;
 | 
			
		||||
  wake_word: string;
 | 
			
		||||
  trained_languages: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AssistSatelliteConfiguration {
 | 
			
		||||
  active_wake_words: string[];
 | 
			
		||||
  available_wake_words: WakeWordOption[];
 | 
			
		||||
  max_active_wake_words: number;
 | 
			
		||||
  pipeline_entity_id: string;
 | 
			
		||||
  vad_entity_id: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const interceptWakeWord = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entity_id: string,
 | 
			
		||||
  callback: (result: WakeWordInterceptMessage) => void
 | 
			
		||||
) =>
 | 
			
		||||
  hass.connection.subscribeMessage(callback, {
 | 
			
		||||
    type: "assist_satellite/intercept_wake_word",
 | 
			
		||||
    entity_id,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export const testAssistSatelliteConnection = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entity_id: string
 | 
			
		||||
) =>
 | 
			
		||||
  hass.callWS<{
 | 
			
		||||
    status: "success" | "timeout";
 | 
			
		||||
  }>({
 | 
			
		||||
    type: "assist_satellite/test_connection",
 | 
			
		||||
    entity_id,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export const assistSatelliteAnnounce = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entity_id: string,
 | 
			
		||||
  message: string
 | 
			
		||||
) =>
 | 
			
		||||
  hass.callService("assist_satellite", "announce", { message }, { entity_id });
 | 
			
		||||
 | 
			
		||||
export const fetchAssistSatelliteConfiguration = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entity_id: string
 | 
			
		||||
) =>
 | 
			
		||||
  hass.callWS<AssistSatelliteConfiguration>({
 | 
			
		||||
    type: "assist_satellite/get_configuration",
 | 
			
		||||
    entity_id,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export const setWakeWords = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entity_id: string,
 | 
			
		||||
  wake_word_ids: string[]
 | 
			
		||||
) =>
 | 
			
		||||
  hass.callWS({
 | 
			
		||||
    type: "assist_satellite/set_wake_words",
 | 
			
		||||
    entity_id,
 | 
			
		||||
    wake_word_ids,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export const assistSatelliteSupportsSetupFlow = (
 | 
			
		||||
  assistSatelliteEntity: HassEntity | undefined
 | 
			
		||||
) =>
 | 
			
		||||
  assistSatelliteEntity &&
 | 
			
		||||
  assistSatelliteEntity.state !== UNAVAILABLE &&
 | 
			
		||||
  supportsFeature(assistSatelliteEntity, AssistSatelliteEntityFeature.ANNOUNCE);
 | 
			
		||||
@@ -3,10 +3,12 @@ import {
 | 
			
		||||
  HassEntityBase,
 | 
			
		||||
} from "home-assistant-js-websocket";
 | 
			
		||||
import { navigate } from "../common/navigate";
 | 
			
		||||
import { ensureArray } from "../common/array/ensure-array";
 | 
			
		||||
import { Context, HomeAssistant } from "../types";
 | 
			
		||||
import { BlueprintInput } from "./blueprint";
 | 
			
		||||
import { DeviceCondition, DeviceTrigger } from "./device_automation";
 | 
			
		||||
import { Action, MODES, migrateAutomationAction } from "./script";
 | 
			
		||||
import { createSearchParam } from "../common/url/search-params";
 | 
			
		||||
 | 
			
		||||
export const AUTOMATION_DEFAULT_MODE: (typeof MODES)[number] = "single";
 | 
			
		||||
export const AUTOMATION_DEFAULT_MAX = 10;
 | 
			
		||||
@@ -26,8 +28,14 @@ export interface ManualAutomationConfig {
 | 
			
		||||
  id?: string;
 | 
			
		||||
  alias?: string;
 | 
			
		||||
  description?: string;
 | 
			
		||||
  trigger: Trigger | Trigger[];
 | 
			
		||||
  triggers: Trigger | Trigger[];
 | 
			
		||||
  /** @deprecated Use `triggers` instead */
 | 
			
		||||
  trigger?: Trigger | Trigger[];
 | 
			
		||||
  conditions?: Condition | Condition[];
 | 
			
		||||
  /** @deprecated Use `conditions` instead */
 | 
			
		||||
  condition?: Condition | Condition[];
 | 
			
		||||
  actions: Action | Action[];
 | 
			
		||||
  /** @deprecated Use `actions` instead */
 | 
			
		||||
  action?: Action | Action[];
 | 
			
		||||
  mode?: (typeof MODES)[number];
 | 
			
		||||
  max?: number;
 | 
			
		||||
@@ -62,16 +70,22 @@ export interface ContextConstraint {
 | 
			
		||||
  user_id?: string | string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TriggerList {
 | 
			
		||||
  triggers: Trigger | Trigger[] | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface BaseTrigger {
 | 
			
		||||
  alias?: string;
 | 
			
		||||
  platform: string;
 | 
			
		||||
  /** @deprecated Use `trigger` instead */
 | 
			
		||||
  platform?: string;
 | 
			
		||||
  trigger: string;
 | 
			
		||||
  id?: string;
 | 
			
		||||
  variables?: Record<string, unknown>;
 | 
			
		||||
  enabled?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StateTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "state";
 | 
			
		||||
  trigger: "state";
 | 
			
		||||
  entity_id: string | string[];
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
  from?: string | string[];
 | 
			
		||||
@@ -80,25 +94,25 @@ export interface StateTrigger extends BaseTrigger {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface MqttTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "mqtt";
 | 
			
		||||
  trigger: "mqtt";
 | 
			
		||||
  topic: string;
 | 
			
		||||
  payload?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface GeoLocationTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "geo_location";
 | 
			
		||||
  trigger: "geo_location";
 | 
			
		||||
  source: string;
 | 
			
		||||
  zone: string;
 | 
			
		||||
  event: "enter" | "leave";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface HassTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "homeassistant";
 | 
			
		||||
  trigger: "homeassistant";
 | 
			
		||||
  event: "start" | "shutdown";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface NumericStateTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "numeric_state";
 | 
			
		||||
  trigger: "numeric_state";
 | 
			
		||||
  entity_id: string | string[];
 | 
			
		||||
  attribute?: string;
 | 
			
		||||
  above?: number;
 | 
			
		||||
@@ -108,69 +122,69 @@ export interface NumericStateTrigger extends BaseTrigger {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ConversationTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "conversation";
 | 
			
		||||
  trigger: "conversation";
 | 
			
		||||
  command: string | string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface SunTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "sun";
 | 
			
		||||
  trigger: "sun";
 | 
			
		||||
  offset: number;
 | 
			
		||||
  event: "sunrise" | "sunset";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TimePatternTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "time_pattern";
 | 
			
		||||
  trigger: "time_pattern";
 | 
			
		||||
  hours?: number | string;
 | 
			
		||||
  minutes?: number | string;
 | 
			
		||||
  seconds?: number | string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface WebhookTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "webhook";
 | 
			
		||||
  trigger: "webhook";
 | 
			
		||||
  webhook_id: string;
 | 
			
		||||
  allowed_methods?: string[];
 | 
			
		||||
  local_only?: boolean;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface PersistentNotificationTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "persistent_notification";
 | 
			
		||||
  trigger: "persistent_notification";
 | 
			
		||||
  notification_id?: string;
 | 
			
		||||
  update_type?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface ZoneTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "zone";
 | 
			
		||||
  trigger: "zone";
 | 
			
		||||
  entity_id: string;
 | 
			
		||||
  zone: string;
 | 
			
		||||
  event: "enter" | "leave";
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TagTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "tag";
 | 
			
		||||
  trigger: "tag";
 | 
			
		||||
  tag_id: string;
 | 
			
		||||
  device_id?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TimeTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "time";
 | 
			
		||||
  trigger: "time";
 | 
			
		||||
  at: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface TemplateTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "template";
 | 
			
		||||
  trigger: "template";
 | 
			
		||||
  value_template: string;
 | 
			
		||||
  for?: string | number | ForDict;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface EventTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "event";
 | 
			
		||||
  trigger: "event";
 | 
			
		||||
  event_type: string;
 | 
			
		||||
  event_data?: any;
 | 
			
		||||
  context?: ContextConstraint;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface CalendarTrigger extends BaseTrigger {
 | 
			
		||||
  platform: "calendar";
 | 
			
		||||
  trigger: "calendar";
 | 
			
		||||
  event: "start" | "end";
 | 
			
		||||
  entity_id: string;
 | 
			
		||||
  offset: string;
 | 
			
		||||
@@ -193,7 +207,8 @@ export type Trigger =
 | 
			
		||||
  | TemplateTrigger
 | 
			
		||||
  | EventTrigger
 | 
			
		||||
  | DeviceTrigger
 | 
			
		||||
  | CalendarTrigger;
 | 
			
		||||
  | CalendarTrigger
 | 
			
		||||
  | TriggerList;
 | 
			
		||||
 | 
			
		||||
interface BaseCondition {
 | 
			
		||||
  condition: string;
 | 
			
		||||
@@ -357,25 +372,104 @@ export const normalizeAutomationConfig = <
 | 
			
		||||
>(
 | 
			
		||||
  config: T
 | 
			
		||||
): T => {
 | 
			
		||||
  config = migrateAutomationConfig(config);
 | 
			
		||||
 | 
			
		||||
  // Normalize data: ensure triggers, actions and conditions are lists
 | 
			
		||||
  // Happens when people copy paste their automations into the config
 | 
			
		||||
  for (const key of ["trigger", "condition", "action"]) {
 | 
			
		||||
  for (const key of ["triggers", "conditions", "actions"]) {
 | 
			
		||||
    const value = config[key];
 | 
			
		||||
    if (value && !Array.isArray(value)) {
 | 
			
		||||
      config[key] = [value];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (config.action) {
 | 
			
		||||
    config.action = migrateAutomationAction(config.action);
 | 
			
		||||
  return config;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const migrateAutomationConfig = <
 | 
			
		||||
  T extends Partial<AutomationConfig> | AutomationConfig,
 | 
			
		||||
>(
 | 
			
		||||
  config: T
 | 
			
		||||
) => {
 | 
			
		||||
  if ("trigger" in config) {
 | 
			
		||||
    if (!("triggers" in config)) {
 | 
			
		||||
      config.triggers = config.trigger;
 | 
			
		||||
    }
 | 
			
		||||
    delete config.trigger;
 | 
			
		||||
  }
 | 
			
		||||
  if ("condition" in config) {
 | 
			
		||||
    if (!("conditions" in config)) {
 | 
			
		||||
      config.conditions = config.condition;
 | 
			
		||||
    }
 | 
			
		||||
    delete config.condition;
 | 
			
		||||
  }
 | 
			
		||||
  if ("action" in config) {
 | 
			
		||||
    if (!("actions" in config)) {
 | 
			
		||||
      config.actions = config.action;
 | 
			
		||||
    }
 | 
			
		||||
    delete config.action;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (config.triggers) {
 | 
			
		||||
    config.triggers = migrateAutomationTrigger(config.triggers);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (config.actions) {
 | 
			
		||||
    config.actions = migrateAutomationAction(config.actions);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return config;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const showAutomationEditor = (data?: Partial<AutomationConfig>) => {
 | 
			
		||||
export const migrateAutomationTrigger = (
 | 
			
		||||
  trigger: Trigger | Trigger[]
 | 
			
		||||
): Trigger | Trigger[] => {
 | 
			
		||||
  if (Array.isArray(trigger)) {
 | 
			
		||||
    return trigger.map(migrateAutomationTrigger) as Trigger[];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ("triggers" in trigger && trigger.triggers) {
 | 
			
		||||
    trigger.triggers = migrateAutomationTrigger(trigger.triggers);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if ("platform" in trigger) {
 | 
			
		||||
    if (!("trigger" in trigger)) {
 | 
			
		||||
      // @ts-ignore
 | 
			
		||||
      trigger.trigger = trigger.platform;
 | 
			
		||||
    }
 | 
			
		||||
    delete trigger.platform;
 | 
			
		||||
  }
 | 
			
		||||
  return trigger;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const flattenTriggers = (
 | 
			
		||||
  triggers: undefined | Trigger | Trigger[]
 | 
			
		||||
): Trigger[] => {
 | 
			
		||||
  if (!triggers) {
 | 
			
		||||
    return [];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const flatTriggers: Trigger[] = [];
 | 
			
		||||
 | 
			
		||||
  ensureArray(triggers).forEach((t) => {
 | 
			
		||||
    if ("triggers" in t) {
 | 
			
		||||
      if (t.triggers) {
 | 
			
		||||
        flatTriggers.push(...flattenTriggers(t.triggers));
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      flatTriggers.push(t);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  return flatTriggers;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const showAutomationEditor = (
 | 
			
		||||
  data?: Partial<AutomationConfig>,
 | 
			
		||||
  expanded?: boolean
 | 
			
		||||
) => {
 | 
			
		||||
  initialAutomationEditorData = data;
 | 
			
		||||
  navigate("/config/automation/edit/new");
 | 
			
		||||
  const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
 | 
			
		||||
  navigate(`/config/automation/edit/new${params}`);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const duplicateAutomation = (config: AutomationConfig) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,6 +22,7 @@ import {
 | 
			
		||||
  formatListWithAnds,
 | 
			
		||||
  formatListWithOrs,
 | 
			
		||||
} from "../common/string/format-list";
 | 
			
		||||
import { isTriggerList } from "./trigger";
 | 
			
		||||
 | 
			
		||||
const triggerTranslationBaseKey =
 | 
			
		||||
  "ui.panel.config.automation.editor.triggers.type";
 | 
			
		||||
@@ -68,9 +69,18 @@ export const describeTrigger = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entityRegistry: EntityRegistryEntry[],
 | 
			
		||||
  ignoreAlias = false
 | 
			
		||||
) => {
 | 
			
		||||
): string => {
 | 
			
		||||
  try {
 | 
			
		||||
    return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias);
 | 
			
		||||
    const description = tryDescribeTrigger(
 | 
			
		||||
      trigger,
 | 
			
		||||
      hass,
 | 
			
		||||
      entityRegistry,
 | 
			
		||||
      ignoreAlias
 | 
			
		||||
    );
 | 
			
		||||
    if (typeof description !== "string") {
 | 
			
		||||
      throw new Error(String(description));
 | 
			
		||||
    }
 | 
			
		||||
    return description;
 | 
			
		||||
  } catch (error: any) {
 | 
			
		||||
    // eslint-disable-next-line no-console
 | 
			
		||||
    console.error(error);
 | 
			
		||||
@@ -89,12 +99,26 @@ const tryDescribeTrigger = (
 | 
			
		||||
  entityRegistry: EntityRegistryEntry[],
 | 
			
		||||
  ignoreAlias = false
 | 
			
		||||
) => {
 | 
			
		||||
  if (isTriggerList(trigger)) {
 | 
			
		||||
    const triggers = ensureArray(trigger.triggers);
 | 
			
		||||
 | 
			
		||||
    if (!triggers || triggers.length === 0) {
 | 
			
		||||
      return hass.localize(
 | 
			
		||||
        `${triggerTranslationBaseKey}.list.description.no_trigger`
 | 
			
		||||
      );
 | 
			
		||||
    }
 | 
			
		||||
    const count = triggers.length;
 | 
			
		||||
    return hass.localize(`${triggerTranslationBaseKey}.list.description.full`, {
 | 
			
		||||
      count: count,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (trigger.alias && !ignoreAlias) {
 | 
			
		||||
    return trigger.alias;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Event Trigger
 | 
			
		||||
  if (trigger.platform === "event" && trigger.event_type) {
 | 
			
		||||
  if (trigger.trigger === "event" && trigger.event_type) {
 | 
			
		||||
    const eventTypes: string[] = [];
 | 
			
		||||
 | 
			
		||||
    if (Array.isArray(trigger.event_type)) {
 | 
			
		||||
@@ -113,7 +137,7 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Home Assistant Trigger
 | 
			
		||||
  if (trigger.platform === "homeassistant" && trigger.event) {
 | 
			
		||||
  if (trigger.trigger === "homeassistant" && trigger.event) {
 | 
			
		||||
    return hass.localize(
 | 
			
		||||
      trigger.event === "start"
 | 
			
		||||
        ? `${triggerTranslationBaseKey}.homeassistant.description.started`
 | 
			
		||||
@@ -122,7 +146,7 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Numeric State Trigger
 | 
			
		||||
  if (trigger.platform === "numeric_state" && trigger.entity_id) {
 | 
			
		||||
  if (trigger.trigger === "numeric_state" && trigger.entity_id) {
 | 
			
		||||
    const entities: string[] = [];
 | 
			
		||||
    const states = hass.states;
 | 
			
		||||
 | 
			
		||||
@@ -197,7 +221,7 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // State Trigger
 | 
			
		||||
  if (trigger.platform === "state") {
 | 
			
		||||
  if (trigger.trigger === "state") {
 | 
			
		||||
    const entities: string[] = [];
 | 
			
		||||
    const states = hass.states;
 | 
			
		||||
 | 
			
		||||
@@ -320,7 +344,7 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Sun Trigger
 | 
			
		||||
  if (trigger.platform === "sun" && trigger.event) {
 | 
			
		||||
  if (trigger.trigger === "sun" && trigger.event) {
 | 
			
		||||
    let duration = "";
 | 
			
		||||
    if (trigger.offset) {
 | 
			
		||||
      if (typeof trigger.offset === "number") {
 | 
			
		||||
@@ -341,12 +365,12 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Tag Trigger
 | 
			
		||||
  if (trigger.platform === "tag") {
 | 
			
		||||
  if (trigger.trigger === "tag") {
 | 
			
		||||
    return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Time Trigger
 | 
			
		||||
  if (trigger.platform === "time" && trigger.at) {
 | 
			
		||||
  if (trigger.trigger === "time" && trigger.at) {
 | 
			
		||||
    const result = ensureArray(trigger.at).map((at) =>
 | 
			
		||||
      typeof at !== "string"
 | 
			
		||||
        ? at
 | 
			
		||||
@@ -361,7 +385,7 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Time Pattern Trigger
 | 
			
		||||
  if (trigger.platform === "time_pattern") {
 | 
			
		||||
  if (trigger.trigger === "time_pattern") {
 | 
			
		||||
    if (!trigger.seconds && !trigger.minutes && !trigger.hours) {
 | 
			
		||||
      return hass.localize(
 | 
			
		||||
        `${triggerTranslationBaseKey}.time_pattern.description.initial`
 | 
			
		||||
@@ -538,7 +562,7 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Zone Trigger
 | 
			
		||||
  if (trigger.platform === "zone" && trigger.entity_id && trigger.zone) {
 | 
			
		||||
  if (trigger.trigger === "zone" && trigger.entity_id && trigger.zone) {
 | 
			
		||||
    const entities: string[] = [];
 | 
			
		||||
    const zones: string[] = [];
 | 
			
		||||
 | 
			
		||||
@@ -581,7 +605,7 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Geo Location Trigger
 | 
			
		||||
  if (trigger.platform === "geo_location" && trigger.source && trigger.zone) {
 | 
			
		||||
  if (trigger.trigger === "geo_location" && trigger.source && trigger.zone) {
 | 
			
		||||
    const sources: string[] = [];
 | 
			
		||||
    const zones: string[] = [];
 | 
			
		||||
    const states = hass.states;
 | 
			
		||||
@@ -620,12 +644,12 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // MQTT Trigger
 | 
			
		||||
  if (trigger.platform === "mqtt") {
 | 
			
		||||
  if (trigger.trigger === "mqtt") {
 | 
			
		||||
    return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Template Trigger
 | 
			
		||||
  if (trigger.platform === "template") {
 | 
			
		||||
  if (trigger.trigger === "template") {
 | 
			
		||||
    let duration = "";
 | 
			
		||||
    if (trigger.for) {
 | 
			
		||||
      duration = describeDuration(hass.locale, trigger.for) ?? "";
 | 
			
		||||
@@ -638,14 +662,14 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Webhook Trigger
 | 
			
		||||
  if (trigger.platform === "webhook") {
 | 
			
		||||
  if (trigger.trigger === "webhook") {
 | 
			
		||||
    return hass.localize(
 | 
			
		||||
      `${triggerTranslationBaseKey}.webhook.description.full`
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Conversation Trigger
 | 
			
		||||
  if (trigger.platform === "conversation") {
 | 
			
		||||
  if (trigger.trigger === "conversation") {
 | 
			
		||||
    if (!trigger.command) {
 | 
			
		||||
      return hass.localize(
 | 
			
		||||
        `${triggerTranslationBaseKey}.conversation.description.empty`
 | 
			
		||||
@@ -664,14 +688,14 @@ const tryDescribeTrigger = (
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Persistent Notification Trigger
 | 
			
		||||
  if (trigger.platform === "persistent_notification") {
 | 
			
		||||
  if (trigger.trigger === "persistent_notification") {
 | 
			
		||||
    return hass.localize(
 | 
			
		||||
      `${triggerTranslationBaseKey}.persistent_notification.description.full`
 | 
			
		||||
    );
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Device Trigger
 | 
			
		||||
  if (trigger.platform === "device" && trigger.device_id) {
 | 
			
		||||
  if (trigger.trigger === "device" && trigger.device_id) {
 | 
			
		||||
    const config = trigger as DeviceTrigger;
 | 
			
		||||
    const localized = localizeDeviceAutomationTrigger(
 | 
			
		||||
      hass,
 | 
			
		||||
@@ -689,7 +713,7 @@ const tryDescribeTrigger = (
 | 
			
		||||
 | 
			
		||||
  return (
 | 
			
		||||
    hass.localize(
 | 
			
		||||
      `ui.panel.config.automation.editor.triggers.type.${trigger.platform}.label`
 | 
			
		||||
      `ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label`
 | 
			
		||||
    ) ||
 | 
			
		||||
    hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`)
 | 
			
		||||
  );
 | 
			
		||||
@@ -700,9 +724,18 @@ export const describeCondition = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entityRegistry: EntityRegistryEntry[],
 | 
			
		||||
  ignoreAlias = false
 | 
			
		||||
) => {
 | 
			
		||||
): string => {
 | 
			
		||||
  try {
 | 
			
		||||
    return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias);
 | 
			
		||||
    const description = tryDescribeCondition(
 | 
			
		||||
      condition,
 | 
			
		||||
      hass,
 | 
			
		||||
      entityRegistry,
 | 
			
		||||
      ignoreAlias
 | 
			
		||||
    );
 | 
			
		||||
    if (typeof description !== "string") {
 | 
			
		||||
      throw new Error(String(description));
 | 
			
		||||
    }
 | 
			
		||||
    return description;
 | 
			
		||||
  } catch (error: any) {
 | 
			
		||||
    // eslint-disable-next-line no-console
 | 
			
		||||
    console.error(error);
 | 
			
		||||
@@ -889,8 +922,14 @@ const tryDescribeCondition = (
 | 
			
		||||
 | 
			
		||||
  // Numeric State Condition
 | 
			
		||||
  if (condition.condition === "numeric_state" && condition.entity_id) {
 | 
			
		||||
    const stateObj = hass.states[condition.entity_id];
 | 
			
		||||
    const entity = stateObj ? computeStateName(stateObj) : condition.entity_id;
 | 
			
		||||
    const entity_ids = ensureArray(condition.entity_id);
 | 
			
		||||
    const stateObj = hass.states[entity_ids[0]];
 | 
			
		||||
    const entity = formatListWithAnds(
 | 
			
		||||
      hass.locale,
 | 
			
		||||
      entity_ids.map((id) =>
 | 
			
		||||
        hass.states[id] ? computeStateName(hass.states[id]) : id || ""
 | 
			
		||||
      )
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
    const attribute = condition.attribute
 | 
			
		||||
      ? computeAttributeNameDisplay(
 | 
			
		||||
@@ -905,8 +944,9 @@ const tryDescribeCondition = (
 | 
			
		||||
      return hass.localize(
 | 
			
		||||
        `${conditionsTranslationBaseKey}.numeric_state.description.above-below`,
 | 
			
		||||
        {
 | 
			
		||||
          attribute: attribute,
 | 
			
		||||
          entity: entity,
 | 
			
		||||
          attribute,
 | 
			
		||||
          entity,
 | 
			
		||||
          numberOfEntities: entity_ids.length,
 | 
			
		||||
          above: condition.above,
 | 
			
		||||
          below: condition.below,
 | 
			
		||||
        }
 | 
			
		||||
@@ -916,8 +956,9 @@ const tryDescribeCondition = (
 | 
			
		||||
      return hass.localize(
 | 
			
		||||
        `${conditionsTranslationBaseKey}.numeric_state.description.above`,
 | 
			
		||||
        {
 | 
			
		||||
          attribute: attribute,
 | 
			
		||||
          entity: entity,
 | 
			
		||||
          attribute,
 | 
			
		||||
          entity,
 | 
			
		||||
          numberOfEntities: entity_ids.length,
 | 
			
		||||
          above: condition.above,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
@@ -926,8 +967,9 @@ const tryDescribeCondition = (
 | 
			
		||||
      return hass.localize(
 | 
			
		||||
        `${conditionsTranslationBaseKey}.numeric_state.description.below`,
 | 
			
		||||
        {
 | 
			
		||||
          attribute: attribute,
 | 
			
		||||
          entity: entity,
 | 
			
		||||
          attribute,
 | 
			
		||||
          entity,
 | 
			
		||||
          numberOfEntities: entity_ids.length,
 | 
			
		||||
          below: condition.below,
 | 
			
		||||
        }
 | 
			
		||||
      );
 | 
			
		||||
 
 | 
			
		||||
@@ -133,3 +133,17 @@ export const isCameraMediaSource = (mediaContentId: string) =>
 | 
			
		||||
 | 
			
		||||
export const getEntityIdFromCameraMediaSource = (mediaContentId: string) =>
 | 
			
		||||
  mediaContentId.substring(CAMERA_MEDIA_SOURCE_PREFIX.length);
 | 
			
		||||
 | 
			
		||||
export interface WebRTCClientConfiguration {
 | 
			
		||||
  configuration: RTCConfiguration;
 | 
			
		||||
  dataChannel?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const fetchWebRtcClientConfiguration = async (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entityId: string
 | 
			
		||||
) =>
 | 
			
		||||
  hass.callWS<WebRTCClientConfiguration>({
 | 
			
		||||
    type: "camera/webrtc/get_client_config",
 | 
			
		||||
    entity_id: entityId,
 | 
			
		||||
  });
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ interface InvalidConfig {
 | 
			
		||||
  error: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type ValidKeys = "trigger" | "action" | "condition";
 | 
			
		||||
type ValidKeys = "triggers" | "actions" | "conditions";
 | 
			
		||||
 | 
			
		||||
export const validateConfig = <
 | 
			
		||||
  T extends Partial<{ [key in ValidKeys]: unknown }>,
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@ import { createContext } from "@lit-labs/context";
 | 
			
		||||
import { HassConfig } from "home-assistant-js-websocket";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import { EntityRegistryEntry } from "./entity_registry";
 | 
			
		||||
import { FloorRegistryEntry } from "./floor_registry";
 | 
			
		||||
import { LabelRegistryEntry } from "./label_registry";
 | 
			
		||||
 | 
			
		||||
export const connectionContext =
 | 
			
		||||
@@ -28,6 +27,4 @@ export const panelsContext = createContext<HomeAssistant["panels"]>("panels");
 | 
			
		||||
export const fullEntitiesContext =
 | 
			
		||||
  createContext<EntityRegistryEntry[]>("extendedEntities");
 | 
			
		||||
 | 
			
		||||
export const floorsContext = createContext<FloorRegistryEntry[]>("floors");
 | 
			
		||||
 | 
			
		||||
export const labelsContext = createContext<LabelRegistryEntry[]>("labels");
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,20 @@
 | 
			
		||||
export interface DataTableFilters {
 | 
			
		||||
  [key: string]: {
 | 
			
		||||
    value: string[] | { key: string[] } | undefined;
 | 
			
		||||
    value: DataTableFiltersValue;
 | 
			
		||||
    items: Set<string> | undefined;
 | 
			
		||||
  };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export type DataTableFiltersValue = string[] | { key: string[] } | undefined;
 | 
			
		||||
 | 
			
		||||
export interface DataTableFiltersValues {
 | 
			
		||||
  [key: string]: DataTableFiltersValue;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface DataTableFiltersItems {
 | 
			
		||||
  [key: string]: Set<string> | undefined;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const serializeFilters = (value: DataTableFilters) => {
 | 
			
		||||
  const serializedValue = {};
 | 
			
		||||
  Object.entries(value).forEach(([key, val]) => {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
import { computeStateName } from "../common/entity/compute_state_name";
 | 
			
		||||
import type { HaFormSchema } from "../components/ha-form/types";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import { BaseTrigger } from "./automation";
 | 
			
		||||
import { BaseTrigger, migrateAutomationTrigger } from "./automation";
 | 
			
		||||
import {
 | 
			
		||||
  computeEntityRegistryName,
 | 
			
		||||
  entityRegistryByEntityId,
 | 
			
		||||
@@ -31,7 +31,7 @@ export interface DeviceCondition extends DeviceAutomation {
 | 
			
		||||
 | 
			
		||||
export type DeviceTrigger = DeviceAutomation &
 | 
			
		||||
  BaseTrigger & {
 | 
			
		||||
    platform: "device";
 | 
			
		||||
    trigger: "device";
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
export interface DeviceCapabilities {
 | 
			
		||||
@@ -51,10 +51,12 @@ export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) =>
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) =>
 | 
			
		||||
  hass.callWS<DeviceTrigger[]>({
 | 
			
		||||
    type: "device_automation/trigger/list",
 | 
			
		||||
    device_id: deviceId,
 | 
			
		||||
  });
 | 
			
		||||
  hass
 | 
			
		||||
    .callWS<DeviceTrigger[]>({
 | 
			
		||||
      type: "device_automation/trigger/list",
 | 
			
		||||
      device_id: deviceId,
 | 
			
		||||
    })
 | 
			
		||||
    .then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]);
 | 
			
		||||
 | 
			
		||||
export const fetchDeviceActionCapabilities = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
@@ -91,7 +93,7 @@ const deviceAutomationIdentifiers = [
 | 
			
		||||
  "subtype",
 | 
			
		||||
  "event",
 | 
			
		||||
  "condition",
 | 
			
		||||
  "platform",
 | 
			
		||||
  "trigger",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export const deviceAutomationsEqual = (
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,4 @@
 | 
			
		||||
import { Connection, createCollection } from "home-assistant-js-websocket";
 | 
			
		||||
import { Store } from "home-assistant-js-websocket/dist/store";
 | 
			
		||||
import { stringCompare } from "../common/string/compare";
 | 
			
		||||
import { debounce } from "../common/util/debounce";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import { AreaRegistryEntry } from "./area_registry";
 | 
			
		||||
import { RegistryEntry } from "./registry";
 | 
			
		||||
@@ -27,48 +24,6 @@ export interface FloorRegistryEntryMutableParams {
 | 
			
		||||
  aliases?: string[];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const fetchFloorRegistry = (conn: Connection) =>
 | 
			
		||||
  conn
 | 
			
		||||
    .sendMessagePromise({
 | 
			
		||||
      type: "config/floor_registry/list",
 | 
			
		||||
    })
 | 
			
		||||
    .then((floors) =>
 | 
			
		||||
      (floors as FloorRegistryEntry[]).sort((ent1, ent2) => {
 | 
			
		||||
        if (ent1.level !== ent2.level) {
 | 
			
		||||
          return (ent1.level ?? 9999) - (ent2.level ?? 9999);
 | 
			
		||||
        }
 | 
			
		||||
        return stringCompare(ent1.name, ent2.name);
 | 
			
		||||
      })
 | 
			
		||||
    );
 | 
			
		||||
 | 
			
		||||
const subscribeFloorRegistryUpdates = (
 | 
			
		||||
  conn: Connection,
 | 
			
		||||
  store: Store<FloorRegistryEntry[]>
 | 
			
		||||
) =>
 | 
			
		||||
  conn.subscribeEvents(
 | 
			
		||||
    debounce(
 | 
			
		||||
      () =>
 | 
			
		||||
        fetchFloorRegistry(conn).then((areas: FloorRegistryEntry[]) =>
 | 
			
		||||
          store.setState(areas, true)
 | 
			
		||||
        ),
 | 
			
		||||
      500,
 | 
			
		||||
      true
 | 
			
		||||
    ),
 | 
			
		||||
    "floor_registry_updated"
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
export const subscribeFloorRegistry = (
 | 
			
		||||
  conn: Connection,
 | 
			
		||||
  onChange: (floors: FloorRegistryEntry[]) => void
 | 
			
		||||
) =>
 | 
			
		||||
  createCollection<FloorRegistryEntry[]>(
 | 
			
		||||
    "_floorRegistry",
 | 
			
		||||
    fetchFloorRegistry,
 | 
			
		||||
    subscribeFloorRegistryUpdates,
 | 
			
		||||
    conn,
 | 
			
		||||
    onChange
 | 
			
		||||
  );
 | 
			
		||||
 | 
			
		||||
export const createFloorRegistryEntry = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  values: FloorRegistryEntryMutableParams
 | 
			
		||||
 
 | 
			
		||||
@@ -28,6 +28,7 @@ export interface UrlActionConfig extends BaseActionConfig {
 | 
			
		||||
 | 
			
		||||
export interface MoreInfoActionConfig extends BaseActionConfig {
 | 
			
		||||
  action: "more-info";
 | 
			
		||||
  entity_id?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface AssistActionConfig extends BaseActionConfig {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,16 @@
 | 
			
		||||
import type { Condition } from "../../../panels/lovelace/common/validate-condition";
 | 
			
		||||
import type { LovelaceLayoutOptions } from "../../../panels/lovelace/types";
 | 
			
		||||
import type {
 | 
			
		||||
  LovelaceGridOptions,
 | 
			
		||||
  LovelaceLayoutOptions,
 | 
			
		||||
} from "../../../panels/lovelace/types";
 | 
			
		||||
 | 
			
		||||
export interface LovelaceCardConfig {
 | 
			
		||||
  index?: number;
 | 
			
		||||
  view_index?: number;
 | 
			
		||||
  view_layout?: any;
 | 
			
		||||
  /** @deprecated Use `grid_options` instead */
 | 
			
		||||
  layout_options?: LovelaceLayoutOptions;
 | 
			
		||||
  grid_options?: LovelaceGridOptions;
 | 
			
		||||
  type: string;
 | 
			
		||||
  [key: string]: any;
 | 
			
		||||
  visibility?: Condition[];
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,13 @@ import type { LovelaceCardConfig } from "./card";
 | 
			
		||||
import type { LovelaceStrategyConfig } from "./strategy";
 | 
			
		||||
 | 
			
		||||
export interface LovelaceBaseSectionConfig {
 | 
			
		||||
  title?: string;
 | 
			
		||||
  visibility?: Condition[];
 | 
			
		||||
  column_span?: number;
 | 
			
		||||
  row_span?: number;
 | 
			
		||||
  /**
 | 
			
		||||
   * @deprecated Use heading card instead.
 | 
			
		||||
   */
 | 
			
		||||
  title?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface LovelaceSectionConfig extends LovelaceBaseSectionConfig {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,8 @@ import { UnsubscribeFunc } from "home-assistant-js-websocket";
 | 
			
		||||
import { navigate } from "../common/navigate";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import { subscribeDeviceRegistry } from "./device_registry";
 | 
			
		||||
import { isComponentLoaded } from "../common/config/is_component_loaded";
 | 
			
		||||
import { getThreadDataSetTLV, listThreadDataSets } from "./thread";
 | 
			
		||||
 | 
			
		||||
export enum NetworkType {
 | 
			
		||||
  THREAD = "thread",
 | 
			
		||||
@@ -51,10 +53,31 @@ export interface MatterCommissioningParameters {
 | 
			
		||||
export const canCommissionMatterExternal = (hass: HomeAssistant) =>
 | 
			
		||||
  hass.auth.external?.config.canCommissionMatter;
 | 
			
		||||
 | 
			
		||||
export const startExternalCommissioning = (hass: HomeAssistant) =>
 | 
			
		||||
  hass.auth.external!.fireMessage({
 | 
			
		||||
export const startExternalCommissioning = async (hass: HomeAssistant) => {
 | 
			
		||||
  if (isComponentLoaded(hass, "thread")) {
 | 
			
		||||
    const datasets = await listThreadDataSets(hass);
 | 
			
		||||
    const preferredDataset = datasets.datasets.find(
 | 
			
		||||
      (dataset) => dataset.preferred
 | 
			
		||||
    );
 | 
			
		||||
    if (preferredDataset) {
 | 
			
		||||
      return hass.auth.external!.fireMessage({
 | 
			
		||||
        type: "matter/commission",
 | 
			
		||||
        payload: {
 | 
			
		||||
          active_operational_dataset: (
 | 
			
		||||
            await getThreadDataSetTLV(hass, preferredDataset.dataset_id)
 | 
			
		||||
          ).tlv,
 | 
			
		||||
          border_agent_id: preferredDataset.preferred_border_agent_id,
 | 
			
		||||
          mac_extended_address: preferredDataset.preferred_extended_address,
 | 
			
		||||
          extended_pan_id: preferredDataset.extended_pan_id,
 | 
			
		||||
        },
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return hass.auth.external!.fireMessage({
 | 
			
		||||
    type: "matter/commission",
 | 
			
		||||
  });
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const redirectOnNewMatterDevice = (
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
 
 | 
			
		||||
@@ -245,7 +245,8 @@ export const computeMediaDescription = (
 | 
			
		||||
      secondaryTitle = stateObj.attributes.media_artist!;
 | 
			
		||||
      break;
 | 
			
		||||
    case "playlist":
 | 
			
		||||
      secondaryTitle = stateObj.attributes.media_playlist!;
 | 
			
		||||
      secondaryTitle =
 | 
			
		||||
        stateObj.attributes.media_playlist || stateObj.attributes.media_artist!;
 | 
			
		||||
      break;
 | 
			
		||||
    case "tvshow":
 | 
			
		||||
      secondaryTitle = stateObj.attributes.media_series_title!;
 | 
			
		||||
 
 | 
			
		||||
@@ -47,11 +47,19 @@ export interface StatisticsMetaData {
 | 
			
		||||
  unit_class: string | null;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const STATISTIC_TYPES: StatisticsValidationResult["type"][] = [
 | 
			
		||||
  "entity_not_recorded",
 | 
			
		||||
  "entity_no_longer_recorded",
 | 
			
		||||
  "state_class_removed",
 | 
			
		||||
  "units_changed",
 | 
			
		||||
  "no_state",
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
export type StatisticsValidationResult =
 | 
			
		||||
  | StatisticsValidationResultNoState
 | 
			
		||||
  | StatisticsValidationResultEntityNotRecorded
 | 
			
		||||
  | StatisticsValidationResultEntityNoLongerRecorded
 | 
			
		||||
  | StatisticsValidationResultUnsupportedStateClass
 | 
			
		||||
  | StatisticsValidationResultStateClassRemoved
 | 
			
		||||
  | StatisticsValidationResultUnitsChanged;
 | 
			
		||||
 | 
			
		||||
export interface StatisticsValidationResultNoState {
 | 
			
		||||
@@ -69,9 +77,9 @@ export interface StatisticsValidationResultEntityNotRecorded {
 | 
			
		||||
  data: { statistic_id: string };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StatisticsValidationResultUnsupportedStateClass {
 | 
			
		||||
  type: "unsupported_state_class";
 | 
			
		||||
  data: { statistic_id: string; state_class: string };
 | 
			
		||||
export interface StatisticsValidationResultStateClassRemoved {
 | 
			
		||||
  type: "state_class_removed";
 | 
			
		||||
  data: { statistic_id: string };
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export interface StatisticsValidationResultUnitsChanged {
 | 
			
		||||
@@ -324,3 +332,6 @@ export const getDisplayUnit = (
 | 
			
		||||
 | 
			
		||||
export const isExternalStatistic = (statisticsId: string): boolean =>
 | 
			
		||||
  statisticsId.includes(":");
 | 
			
		||||
 | 
			
		||||
export const updateStatisticsIssues = (hass: HomeAssistant) =>
 | 
			
		||||
  hass.callWS({ type: "recorder/update_statistics_issues" });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +0,0 @@
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
 | 
			
		||||
export interface WebRtcSettings {
 | 
			
		||||
  stun_server?: string;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
export const fetchWebRtcSettings = async (hass: HomeAssistant) =>
 | 
			
		||||
  hass.callWS<WebRtcSettings>({
 | 
			
		||||
    type: "rtsp_to_webrtc/get_settings",
 | 
			
		||||
  });
 | 
			
		||||
@@ -20,6 +20,7 @@ import { navigate } from "../common/navigate";
 | 
			
		||||
import { HomeAssistant } from "../types";
 | 
			
		||||
import {
 | 
			
		||||
  Condition,
 | 
			
		||||
  migrateAutomationTrigger,
 | 
			
		||||
  ShorthandAndCondition,
 | 
			
		||||
  ShorthandNotCondition,
 | 
			
		||||
  ShorthandOrCondition,
 | 
			
		||||
@@ -27,6 +28,7 @@ import {
 | 
			
		||||
} from "./automation";
 | 
			
		||||
import { BlueprintInput } from "./blueprint";
 | 
			
		||||
import { computeObjectId } from "../common/entity/compute_object_id";
 | 
			
		||||
import { createSearchParam } from "../common/url/search-params";
 | 
			
		||||
 | 
			
		||||
export const MODES = ["single", "restart", "queued", "parallel"] as const;
 | 
			
		||||
export const MODES_MAX = ["queued", "parallel"] as const;
 | 
			
		||||
@@ -346,9 +348,13 @@ export const getScriptStateConfig = (hass: HomeAssistant, entity_id: string) =>
 | 
			
		||||
    entity_id,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
export const showScriptEditor = (data?: Partial<ScriptConfig>) => {
 | 
			
		||||
export const showScriptEditor = (
 | 
			
		||||
  data?: Partial<ScriptConfig>,
 | 
			
		||||
  expanded?: boolean
 | 
			
		||||
) => {
 | 
			
		||||
  inititialScriptEditorData = data;
 | 
			
		||||
  navigate("/config/script/edit/new");
 | 
			
		||||
  const params = expanded ? `?${createSearchParam({ expanded: "1" })}` : "";
 | 
			
		||||
  navigate(`/config/script/edit/new${params}`);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
export const getScriptEditorInitData = () => {
 | 
			
		||||
@@ -404,7 +410,7 @@ export const getActionType = (action: Action): ActionType => {
 | 
			
		||||
  if ("set_conversation_response" in action) {
 | 
			
		||||
    return "set_conversation_response";
 | 
			
		||||
  }
 | 
			
		||||
  if ("action" in action) {
 | 
			
		||||
  if ("action" in action || "service" in action) {
 | 
			
		||||
    if ("metadata" in action) {
 | 
			
		||||
      if (is(action, activateSceneActionStruct)) {
 | 
			
		||||
        return "activate_scene";
 | 
			
		||||
@@ -480,5 +486,10 @@ export const migrateAutomationAction = (
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (actionType === "wait_for_trigger") {
 | 
			
		||||
    const _action = action as WaitForTriggerAction;
 | 
			
		||||
    migrateAutomationTrigger(_action.wait_for_trigger);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return action;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,6 @@ import {
 | 
			
		||||
  computeEntityRegistryName,
 | 
			
		||||
  entityRegistryById,
 | 
			
		||||
} from "./entity_registry";
 | 
			
		||||
import { FloorRegistryEntry } from "./floor_registry";
 | 
			
		||||
import { domainToName } from "./integration";
 | 
			
		||||
import { LabelRegistryEntry } from "./label_registry";
 | 
			
		||||
import {
 | 
			
		||||
@@ -44,21 +43,23 @@ export const describeAction = <T extends ActionType>(
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entityRegistry: EntityRegistryEntry[],
 | 
			
		||||
  labelRegistry: LabelRegistryEntry[],
 | 
			
		||||
  floorRegistry: FloorRegistryEntry[],
 | 
			
		||||
  action: ActionTypes[T],
 | 
			
		||||
  actionType?: T,
 | 
			
		||||
  ignoreAlias = false
 | 
			
		||||
): string => {
 | 
			
		||||
  try {
 | 
			
		||||
    return tryDescribeAction(
 | 
			
		||||
    const description = tryDescribeAction(
 | 
			
		||||
      hass,
 | 
			
		||||
      entityRegistry,
 | 
			
		||||
      labelRegistry,
 | 
			
		||||
      floorRegistry,
 | 
			
		||||
      action,
 | 
			
		||||
      actionType,
 | 
			
		||||
      ignoreAlias
 | 
			
		||||
    );
 | 
			
		||||
    if (typeof description !== "string") {
 | 
			
		||||
      throw new Error(String(description));
 | 
			
		||||
    }
 | 
			
		||||
    return description;
 | 
			
		||||
  } catch (error: any) {
 | 
			
		||||
    // eslint-disable-next-line no-console
 | 
			
		||||
    console.error(error);
 | 
			
		||||
@@ -74,7 +75,6 @@ const tryDescribeAction = <T extends ActionType>(
 | 
			
		||||
  hass: HomeAssistant,
 | 
			
		||||
  entityRegistry: EntityRegistryEntry[],
 | 
			
		||||
  labelRegistry: LabelRegistryEntry[],
 | 
			
		||||
  floorRegistry: FloorRegistryEntry[],
 | 
			
		||||
  action: ActionTypes[T],
 | 
			
		||||
  actionType?: T,
 | 
			
		||||
  ignoreAlias = false
 | 
			
		||||
@@ -164,9 +164,7 @@ const tryDescribeAction = <T extends ActionType>(
 | 
			
		||||
              );
 | 
			
		||||
            }
 | 
			
		||||
          } else if (key === "floor_id") {
 | 
			
		||||
            const floor = floorRegistry.find(
 | 
			
		||||
              (flr) => flr.floor_id === targetThing
 | 
			
		||||
            );
 | 
			
		||||
            const floor = hass.floors[targetThing] ?? undefined;
 | 
			
		||||
            if (floor?.name) {
 | 
			
		||||
              targets.push(floor.name);
 | 
			
		||||
            } else {
 | 
			
		||||
 
 | 
			
		||||