mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 14:39:38 +00:00 
			
		
		
		
	Compare commits
	
		
			1 Commits
		
	
	
		
			20240926.0
			...
			boolean_se
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 932120869b | 
							
								
								
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/cast_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -26,7 +26,7 @@ jobs: | ||||
|           ref: dev | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -62,7 +62,7 @@ jobs: | ||||
|           ref: master | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										12
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										12
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -26,7 +26,7 @@ jobs: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -60,7 +60,7 @@ jobs: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -78,7 +78,7 @@ jobs: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -89,7 +89,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@v4.4.0 | ||||
|         uses: actions/upload-artifact@v4.3.6 | ||||
|         with: | ||||
|           name: frontend-bundle-stats | ||||
|           path: build/stats/*.json | ||||
| @@ -102,7 +102,7 @@ jobs: | ||||
|       - name: Check out files from GitHub | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -113,7 +113,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@v4.4.0 | ||||
|         uses: actions/upload-artifact@v4.3.6 | ||||
|         with: | ||||
|           name: supervisor-bundle-stats | ||||
|           path: build/stats/*.json | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/demo_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -27,7 +27,7 @@ jobs: | ||||
|           ref: dev | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -63,7 +63,7 @@ jobs: | ||||
|           ref: master | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_deployment.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -19,7 +19,7 @@ jobs: | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/design_preview.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -24,7 +24,7 @@ jobs: | ||||
|         uses: actions/checkout@v4.1.7 | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -28,7 +28,7 @@ jobs: | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
| @@ -57,14 +57,14 @@ jobs: | ||||
|         run: tar -czvf translations.tar.gz translations | ||||
|  | ||||
|       - name: Upload build artifacts | ||||
|         uses: actions/upload-artifact@v4.4.0 | ||||
|         uses: actions/upload-artifact@v4.3.6 | ||||
|         with: | ||||
|           name: wheels | ||||
|           path: dist/home_assistant_frontend*.whl | ||||
|           if-no-files-found: error | ||||
|  | ||||
|       - name: Upload translations | ||||
|         uses: actions/upload-artifact@v4.4.0 | ||||
|         uses: actions/upload-artifact@v4.3.6 | ||||
|         with: | ||||
|           name: translations | ||||
|           path: translations.tar.gz | ||||
|   | ||||
							
								
								
									
										2
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/release.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -34,7 +34,7 @@ jobs: | ||||
|           python-version: ${{ env.PYTHON_VERSION }} | ||||
|  | ||||
|       - name: Setup Node | ||||
|         uses: actions/setup-node@v4.0.4 | ||||
|         uses: actions/setup-node@v4.0.3 | ||||
|         with: | ||||
|           node-version-file: ".nvmrc" | ||||
|           cache: yarn | ||||
|   | ||||
| @@ -1,7 +1,16 @@ | ||||
| diff --git a/modular/sortable.core.esm.js b/modular/sortable.core.esm.js
 | ||||
| index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa526090a00 100644
 | ||||
| index 93ba17509e2e8583ab241fea6845fbe714c584a2..de0651ddb5dced30d36f7d764da0dd0b441f523f 100644
 | ||||
| --- a/modular/sortable.core.esm.js
 | ||||
| +++ b/modular/sortable.core.esm.js
 | ||||
| @@ -1461,7 +1461,7 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
|            } | ||||
|            target = parent; // store last element | ||||
|          } | ||||
| -        /* jshint boss:true */ while (parent = parent.parentNode);
 | ||||
| +        /* jshint boss:true */ while (parent = parent.parentNode || parent.getRootNode().host);
 | ||||
|        } | ||||
|        _unhideGhostForTarget(); | ||||
|      } | ||||
| @@ -1781,11 +1781,16 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
|          } | ||||
|          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, !!target) !== false) { | ||||
| @@ -24,7 +33,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5 | ||||
|            } | ||||
|            parentEl = el; // actualization | ||||
|   | ||||
| @@ -1802,7 +1807,12 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
| @@ -1802,7 +1807,13 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
|          targetRect = getRect(target); | ||||
|          if (_onMove(rootEl, el, dragEl, dragRect, target, targetRect, evt, false) !== false) { | ||||
|            capture(); | ||||
| @@ -35,10 +44,11 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5 | ||||
| +          catch(err) {
 | ||||
| +            return completed(false);
 | ||||
| +          }
 | ||||
| +          
 | ||||
|            parentEl = el; // actualization | ||||
|   | ||||
|            changed(); | ||||
| @@ -1849,10 +1859,15 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
| @@ -1849,12 +1860,17 @@ Sortable.prototype = /** @lends Sortable.prototype */{
 | ||||
|            _silent = true; | ||||
|            setTimeout(_unsilent, 30); | ||||
|            capture(); | ||||
| @@ -46,6 +56,8 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5 | ||||
| -            el.appendChild(dragEl);
 | ||||
| -          } else {
 | ||||
| -            target.parentNode.insertBefore(dragEl, after ? nextSibling : target);
 | ||||
| -          }
 | ||||
|   | ||||
| +          try {
 | ||||
| +            if (after && !nextSibling) {
 | ||||
| +              el.appendChild(dragEl);
 | ||||
| @@ -55,6 +67,7 @@ index 8b5e49b011713c8859c669069fbe85ce53974e1d..6a0afc92787157b8a31c38cc5f67dfa5 | ||||
| +          }
 | ||||
| +          catch(err) {
 | ||||
| +            return completed(false);
 | ||||
|            } | ||||
|   | ||||
| +          }
 | ||||
|            // Undo chrome's scroll adjustment (has no effect on other browsers) | ||||
|            if (scrolledPastTop) { | ||||
|              scrollBy(scrolledPastTop, 0, scrollBefore - scrolledPastTop.scrollTop); | ||||
										
											
												File diff suppressed because one or more lines are too long
											
										
									
								
							| @@ -6,4 +6,4 @@ enableGlobalCache: false | ||||
|  | ||||
| nodeLinker: node-modules | ||||
|  | ||||
| yarnPath: .yarn/releases/yarn-4.5.0.cjs | ||||
| yarnPath: .yarn/releases/yarn-4.4.0.cjs | ||||
|   | ||||
| @@ -15,29 +15,23 @@ const brotliOptions = { | ||||
| }; | ||||
| const zopfliOptions = { threshold: 150 }; | ||||
|  | ||||
| const compressDistBrotli = (rootDir, modernDir, compressServiceWorker = true) => | ||||
| const compressDistBrotli = (rootDir, modernDir) => | ||||
|   gulp | ||||
|     .src( | ||||
|       [ | ||||
|         `${modernDir}/**/${filesGlob}`, | ||||
|         compressServiceWorker ? `${rootDir}/sw-modern.js` : undefined, | ||||
|       ].filter(Boolean), | ||||
|       { | ||||
|     .src([`${modernDir}/**/${filesGlob}`, `${rootDir}/sw-modern.js`], { | ||||
|       base: rootDir, | ||||
|       } | ||||
|     ) | ||||
|     }) | ||||
|     .pipe(brotli(brotliOptions)) | ||||
|     .pipe(gulp.dest(rootDir)); | ||||
|  | ||||
| const compressDistZopfli = (rootDir, modernDir, compressModern = false) => | ||||
| const compressDistZopfli = (rootDir, modernDir) => | ||||
|   gulp | ||||
|     .src( | ||||
|       [ | ||||
|         `${rootDir}/**/${filesGlob}`, | ||||
|         compressModern ? undefined : `!${modernDir}/**/${filesGlob}`, | ||||
|         `!${modernDir}/**/${filesGlob}`, | ||||
|         `!${rootDir}/{sw-modern,service_worker}.js`, | ||||
|         `${rootDir}/{authorize,onboarding}.html`, | ||||
|       ].filter(Boolean), | ||||
|       ], | ||||
|       { base: rootDir } | ||||
|     ) | ||||
|     .pipe(zopfli(zopfliOptions)) | ||||
| @@ -46,20 +40,12 @@ const compressDistZopfli = (rootDir, modernDir, compressModern = false) => | ||||
| const compressAppBrotli = () => | ||||
|   compressDistBrotli(paths.app_output_root, paths.app_output_latest); | ||||
| const compressHassioBrotli = () => | ||||
|   compressDistBrotli( | ||||
|     paths.hassio_output_root, | ||||
|     paths.hassio_output_latest, | ||||
|     false | ||||
|   ); | ||||
|   compressDistBrotli(paths.hassio_output_root, paths.hassio_output_latest); | ||||
|  | ||||
| const compressAppZopfli = () => | ||||
|   compressDistZopfli(paths.app_output_root, paths.app_output_latest); | ||||
| const compressHassioZopfli = () => | ||||
|   compressDistZopfli( | ||||
|     paths.hassio_output_root, | ||||
|     paths.hassio_output_latest, | ||||
|     true | ||||
|   ); | ||||
|   compressDistZopfli(paths.hassio_output_root, paths.hassio_output_latest); | ||||
|  | ||||
| gulp.task("compress-app", gulp.parallel(compressAppBrotli, compressAppZopfli)); | ||||
| gulp.task( | ||||
|   | ||||
| @@ -60,12 +60,6 @@ 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">What does Home Assistant Cast do?</div> | ||||
|       <div class="section-header">Wat does Home Assistant Cast do?</div> | ||||
|       <div class="card-content"> | ||||
|         <p> | ||||
|           Home Assistant Cast is a receiver application for the Chromecast. When | ||||
|   | ||||
| @@ -36,7 +36,6 @@ 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: { | ||||
| @@ -366,9 +365,7 @@ export class HcMain extends HassElement { | ||||
|       this._urlPath || "lovelace" | ||||
|     ); | ||||
|     castContext.setApplicationState(title || ""); | ||||
|     this._lovelaceConfig = checkLovelaceConfig( | ||||
|       lovelaceConfig | ||||
|     ) as LovelaceConfig; | ||||
|     this._lovelaceConfig = lovelaceConfig; | ||||
|   } | ||||
|  | ||||
|   private _handleShowDemo(_msg: ShowDemoMessage) { | ||||
|   | ||||
| @@ -111,37 +111,9 @@ export const demoEntitiesSections: DemoConfig["entities"] = (localize) => | ||||
|         friendly_name: "Living room Temperature", | ||||
|       }, | ||||
|     }, | ||||
|     "sensor.outdoor_temperature": { | ||||
|       entity_id: "sensor.outdoor_temperature", | ||||
|       state: "10.5", | ||||
|       attributes: { | ||||
|         state_class: "measurement", | ||||
|         unit_of_measurement: "°C", | ||||
|         device_class: "temperature", | ||||
|         friendly_name: "Outdoor temperature", | ||||
|       }, | ||||
|     }, | ||||
|     "sensor.outdoor_humidity": { | ||||
|       entity_id: "sensor.outdoor_humidity", | ||||
|       state: "70.4", | ||||
|       attributes: { | ||||
|         state_class: "measurement", | ||||
|         unit_of_measurement: "%", | ||||
|         device_class: "humidity", | ||||
|         friendly_name: "Outdoor humidity", | ||||
|       }, | ||||
|     }, | ||||
|     "device_tracker.car": { | ||||
|       entity_id: "sensor.outdoor_humidity", | ||||
|       state: "not_home", | ||||
|       attributes: { | ||||
|         friendly_name: "Car", | ||||
|         icon: "mdi:car", | ||||
|       }, | ||||
|     }, | ||||
|     "media_player.living_room_nest_mini": { | ||||
|       entity_id: "media_player.living_room_nest_mini", | ||||
|       state: "playing", | ||||
|       state: "on", | ||||
|       attributes: { | ||||
|         device_class: "speaker", | ||||
|         volume_level: 0.18, | ||||
|   | ||||
| @@ -9,22 +9,6 @@ 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 | ||||
|           ? [] | ||||
|   | ||||
| @@ -217,22 +217,22 @@ export const basicTrace: DemoTrace = { | ||||
|       id: "1615419646544", | ||||
|       alias: "Ensure Party mode", | ||||
|       description: "", | ||||
|       triggers: [ | ||||
|       trigger: [ | ||||
|         { | ||||
|           trigger: "state", | ||||
|           platform: "state", | ||||
|           entity_id: "input_boolean.toggle_1", | ||||
|         }, | ||||
|       ], | ||||
|       conditions: [ | ||||
|       condition: [ | ||||
|         { | ||||
|           condition: "template", | ||||
|           alias: "Test if Paulus is home", | ||||
|           value_template: "{{ true }}", | ||||
|         }, | ||||
|       ], | ||||
|       actions: [ | ||||
|       action: [ | ||||
|         { | ||||
|           action: "input_boolean.toggle", | ||||
|           service: "input_boolean.toggle", | ||||
|           target: { | ||||
|             entity_id: "input_boolean.toggle_4", | ||||
|           }, | ||||
| @@ -268,7 +268,7 @@ export const basicTrace: DemoTrace = { | ||||
|           ], | ||||
|           default: [ | ||||
|             { | ||||
|               action: "input_boolean.toggle", | ||||
|               service: "input_boolean.toggle", | ||||
|               alias: "Toggle 2", | ||||
|               target: { | ||||
|                 entity_id: "input_boolean.toggle_2", | ||||
| @@ -277,7 +277,7 @@ export const basicTrace: DemoTrace = { | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           action: "input_boolean.toggle", | ||||
|           service: "input_boolean.toggle", | ||||
|           target: { | ||||
|             entity_id: "input_boolean.toggle_4", | ||||
|           }, | ||||
|   | ||||
| @@ -31,8 +31,8 @@ export const mockDemoTrace = ( | ||||
|       ], | ||||
|     }, | ||||
|     config: { | ||||
|       triggers: [], | ||||
|       actions: [], | ||||
|       trigger: [], | ||||
|       action: [], | ||||
|     }, | ||||
|     context: { | ||||
|       id: "abcd", | ||||
|   | ||||
| @@ -133,17 +133,17 @@ export const motionLightTrace: DemoTrace = { | ||||
|     config: { | ||||
|       mode: "restart", | ||||
|       max_exceeded: "silent", | ||||
|       triggers: [ | ||||
|       trigger: [ | ||||
|         { | ||||
|           trigger: "state", | ||||
|           platform: "state", | ||||
|           entity_id: "binary_sensor.pauluss_macbook_pro_camera_in_use", | ||||
|           from: "off", | ||||
|           to: "on", | ||||
|         }, | ||||
|       ], | ||||
|       actions: [ | ||||
|       action: [ | ||||
|         { | ||||
|           action: "light.turn_on", | ||||
|           service: "light.turn_on", | ||||
|           target: { | ||||
|             entity_id: "light.elgato_key_light_air", | ||||
|           }, | ||||
| @@ -162,7 +162,7 @@ export const motionLightTrace: DemoTrace = { | ||||
|           delay: 0, | ||||
|         }, | ||||
|         { | ||||
|           action: "light.turn_off", | ||||
|           service: "light.turn_off", | ||||
|           target: { | ||||
|             entity_id: "light.elgato_key_light_air", | ||||
|           }, | ||||
|   | ||||
| @@ -48,7 +48,7 @@ const ACTIONS = [ | ||||
|   { | ||||
|     wait_for_trigger: [ | ||||
|       { | ||||
|         trigger: "state", | ||||
|         platform: "state", | ||||
|         entity_id: "input_boolean.toggle_1", | ||||
|       }, | ||||
|     ], | ||||
| @@ -121,7 +121,7 @@ const ACTIONS = [ | ||||
| ]; | ||||
|  | ||||
| const initialAction: Action = { | ||||
|   action: "light.turn_on", | ||||
|   service: "light.turn_on", | ||||
|   target: { | ||||
|     entity_id: "light.kitchen", | ||||
|   }, | ||||
|   | ||||
| @@ -22,46 +22,46 @@ const ENTITIES = [ | ||||
| ]; | ||||
|  | ||||
| const triggers = [ | ||||
|   { trigger: "state", entity_id: "light.kitchen", from: "off", to: "on" }, | ||||
|   { trigger: "mqtt" }, | ||||
|   { platform: "state", entity_id: "light.kitchen", from: "off", to: "on" }, | ||||
|   { platform: "mqtt" }, | ||||
|   { | ||||
|     trigger: "geo_location", | ||||
|     platform: "geo_location", | ||||
|     source: "test_source", | ||||
|     zone: "zone.home", | ||||
|     event: "enter", | ||||
|   }, | ||||
|   { trigger: "homeassistant", event: "start" }, | ||||
|   { platform: "homeassistant", event: "start" }, | ||||
|   { | ||||
|     trigger: "numeric_state", | ||||
|     platform: "numeric_state", | ||||
|     entity_id: "light.kitchen", | ||||
|     attribute: "brightness", | ||||
|     below: 80, | ||||
|     above: 20, | ||||
|   }, | ||||
|   { trigger: "sun", event: "sunset" }, | ||||
|   { trigger: "time_pattern" }, | ||||
|   { trigger: "time_pattern", hours: "*", minutes: "/5", seconds: "10" }, | ||||
|   { trigger: "webhook" }, | ||||
|   { trigger: "persistent_notification" }, | ||||
|   { platform: "sun", event: "sunset" }, | ||||
|   { platform: "time_pattern" }, | ||||
|   { platform: "time_pattern", hours: "*", minutes: "/5", seconds: "10" }, | ||||
|   { platform: "webhook" }, | ||||
|   { platform: "persistent_notification" }, | ||||
|   { | ||||
|     trigger: "zone", | ||||
|     platform: "zone", | ||||
|     entity_id: "person.person", | ||||
|     zone: "zone.home", | ||||
|     event: "enter", | ||||
|   }, | ||||
|   { trigger: "tag" }, | ||||
|   { trigger: "time", at: "15:32" }, | ||||
|   { trigger: "template" }, | ||||
|   { trigger: "conversation", command: "Turn on the lights" }, | ||||
|   { platform: "tag" }, | ||||
|   { platform: "time", at: "15:32" }, | ||||
|   { platform: "template" }, | ||||
|   { platform: "conversation", command: "Turn on the lights" }, | ||||
|   { | ||||
|     trigger: "conversation", | ||||
|     platform: "conversation", | ||||
|     command: ["Turn on the lights", "Turn the lights on"], | ||||
|   }, | ||||
|   { trigger: "event", event_type: "homeassistant_started" }, | ||||
|   { platform: "event", event_type: "homeassistant_started" }, | ||||
| ]; | ||||
|  | ||||
| const initialTrigger: Trigger = { | ||||
|   trigger: "state", | ||||
|   platform: "state", | ||||
|   entity_id: "light.kitchen", | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import { mockHassioSupervisor } from "../../../../demo/src/stubs/hassio_supervis | ||||
| import type { ConditionWithShorthand } from "../../../../src/data/automation"; | ||||
| import "../../../../src/panels/config/automation/condition/ha-automation-condition"; | ||||
| import { HaDeviceCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-device"; | ||||
| import { HaLogicalCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-logical"; | ||||
| import HaNumericStateCondition from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-numeric_state"; | ||||
| import { HaStateCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-state"; | ||||
| import { HaSunCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-sun"; | ||||
| @@ -18,67 +19,62 @@ import { HaTemplateCondition } from "../../../../src/panels/config/automation/co | ||||
| import { HaTimeCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-time"; | ||||
| import { HaTriggerCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-trigger"; | ||||
| import { HaZoneCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-zone"; | ||||
| import { HaAndCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-and"; | ||||
| import { HaOrCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-or"; | ||||
| import { HaNotCondition } from "../../../../src/panels/config/automation/condition/types/ha-automation-condition-not"; | ||||
|  | ||||
| const SCHEMAS: { name: string; conditions: ConditionWithShorthand[] }[] = [ | ||||
|   { | ||||
|     name: "State", | ||||
|     conditions: [{ ...HaStateCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "state", ...HaStateCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Numeric State", | ||||
|     conditions: [{ ...HaNumericStateCondition.defaultConfig }], | ||||
|     conditions: [ | ||||
|       { condition: "numeric_state", ...HaNumericStateCondition.defaultConfig }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     name: "Sun", | ||||
|     conditions: [{ ...HaSunCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "sun", ...HaSunCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Zone", | ||||
|     conditions: [{ ...HaZoneCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "zone", ...HaZoneCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Time", | ||||
|     conditions: [{ ...HaTimeCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "time", ...HaTimeCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Template", | ||||
|     conditions: [{ ...HaTemplateCondition.defaultConfig }], | ||||
|     conditions: [ | ||||
|       { condition: "template", ...HaTemplateCondition.defaultConfig }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     name: "Device", | ||||
|     conditions: [{ ...HaDeviceCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "device", ...HaDeviceCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "And", | ||||
|     conditions: [{ ...HaAndCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "and", ...HaLogicalCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Or", | ||||
|     conditions: [{ ...HaOrCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "or", ...HaLogicalCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Not", | ||||
|     conditions: [{ ...HaNotCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "not", ...HaLogicalCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Trigger", | ||||
|     conditions: [{ ...HaTriggerCondition.defaultConfig }], | ||||
|     conditions: [{ condition: "trigger", ...HaTriggerCondition.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Shorthand", | ||||
|     conditions: [ | ||||
|       { | ||||
|         ...HaAndCondition.defaultConfig, | ||||
|       }, | ||||
|       { | ||||
|         ...HaOrCondition.defaultConfig, | ||||
|       }, | ||||
|       { | ||||
|         ...HaNotCondition.defaultConfig, | ||||
|       }, | ||||
|       { and: HaLogicalCondition.defaultConfig.conditions }, | ||||
|       { or: HaLogicalCondition.defaultConfig.conditions }, | ||||
|       { not: HaLogicalCondition.defaultConfig.conditions }, | ||||
|     ], | ||||
|   }, | ||||
| ]; | ||||
|   | ||||
| @@ -30,48 +30,55 @@ import { HaConversationTrigger } from "../../../../src/panels/config/automation/ | ||||
| const SCHEMAS: { name: string; triggers: Trigger[] }[] = [ | ||||
|   { | ||||
|     name: "State", | ||||
|     triggers: [{ ...HaStateTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "state", ...HaStateTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "MQTT", | ||||
|     triggers: [{ ...HaMQTTTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "mqtt", ...HaMQTTTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "GeoLocation", | ||||
|     triggers: [{ ...HaGeolocationTrigger.defaultConfig }], | ||||
|     triggers: [ | ||||
|       { platform: "geo_location", ...HaGeolocationTrigger.defaultConfig }, | ||||
|     ], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Home Assistant", | ||||
|     triggers: [{ ...HaHassTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "homeassistant", ...HaHassTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Numeric State", | ||||
|     triggers: [{ ...HaNumericStateTrigger.defaultConfig }], | ||||
|     triggers: [ | ||||
|       { platform: "numeric_state", ...HaNumericStateTrigger.defaultConfig }, | ||||
|     ], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Sun", | ||||
|     triggers: [{ ...HaSunTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "sun", ...HaSunTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Time Pattern", | ||||
|     triggers: [{ ...HaTimePatternTrigger.defaultConfig }], | ||||
|     triggers: [ | ||||
|       { platform: "time_pattern", ...HaTimePatternTrigger.defaultConfig }, | ||||
|     ], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Webhook", | ||||
|     triggers: [{ ...HaWebhookTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "webhook", ...HaWebhookTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Persistent Notification", | ||||
|     triggers: [ | ||||
|       { | ||||
|         platform: "persistent_notification", | ||||
|         ...HaPersistentNotificationTrigger.defaultConfig, | ||||
|       }, | ||||
|     ], | ||||
| @@ -79,39 +86,39 @@ const SCHEMAS: { name: string; triggers: Trigger[] }[] = [ | ||||
|  | ||||
|   { | ||||
|     name: "Zone", | ||||
|     triggers: [{ ...HaZoneTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "zone", ...HaZoneTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Tag", | ||||
|     triggers: [{ ...HaTagTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "tag", ...HaTagTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Time", | ||||
|     triggers: [{ ...HaTimeTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "time", ...HaTimeTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Template", | ||||
|     triggers: [{ ...HaTemplateTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "template", ...HaTemplateTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Event", | ||||
|     triggers: [{ ...HaEventTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "event", ...HaEventTrigger.defaultConfig }], | ||||
|   }, | ||||
|  | ||||
|   { | ||||
|     name: "Device Trigger", | ||||
|     triggers: [{ ...HaDeviceTrigger.defaultConfig }], | ||||
|     triggers: [{ platform: "device", ...HaDeviceTrigger.defaultConfig }], | ||||
|   }, | ||||
|   { | ||||
|     name: "Sentence", | ||||
|     triggers: [ | ||||
|       { ...HaConversationTrigger.defaultConfig }, | ||||
|       { platform: "conversation", ...HaConversationTrigger.defaultConfig }, | ||||
|       { | ||||
|         trigger: "conversation", | ||||
|         platform: "conversation", | ||||
|         command: ["Turn on the lights", "Turn the lights on"], | ||||
|       }, | ||||
|     ], | ||||
|   | ||||
| @@ -64,7 +64,6 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
|   { | ||||
|     area_id: "backyard", | ||||
| @@ -87,7 +86,6 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
|   { | ||||
|     area_id: null, | ||||
| @@ -110,7 +108,6 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -64,7 +64,6 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
|   { | ||||
|     area_id: "backyard", | ||||
| @@ -87,7 +86,6 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
|   { | ||||
|     area_id: null, | ||||
| @@ -110,7 +108,6 @@ const DEVICES: DeviceRegistryEntry[] = [ | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -1,3 +0,0 @@ | ||||
| --- | ||||
| title: Markdown | ||||
| --- | ||||
| @@ -1,93 +0,0 @@ | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import "../../../../src/components/ha-card"; | ||||
| import "../../../../src/components/ha-markdown"; | ||||
|  | ||||
| import { customElement } from "lit/decorators"; | ||||
|  | ||||
| interface MarkdownContent { | ||||
|   content: string; | ||||
|   breaks: boolean; | ||||
|   allowSvg: boolean; | ||||
|   lazyImages: boolean; | ||||
| } | ||||
|  | ||||
| const mdContentwithDefaults = (md: Partial<MarkdownContent>) => | ||||
|   ({ | ||||
|     breaks: false, | ||||
|     allowSvg: false, | ||||
|     lazyImages: false, | ||||
|     ...md, | ||||
|   }) as MarkdownContent; | ||||
|  | ||||
| const generateContent = (md) => ` | ||||
| \`\`\`json | ||||
| ${JSON.stringify({ ...md, content: undefined })} | ||||
| \`\`\` | ||||
|  | ||||
| --- | ||||
|  | ||||
| ${md.content} | ||||
| `; | ||||
|  | ||||
| const markdownContents: MarkdownContent[] = [ | ||||
|   mdContentwithDefaults({ | ||||
|     content: "_Hello_ **there** 👋, ~~nice~~ of you ||to|| show up.", | ||||
|   }), | ||||
|   ...[true, false].map((breaks) => | ||||
|     mdContentwithDefaults({ | ||||
|       breaks, | ||||
|       content: ` | ||||
|  | ||||
|  | ||||
|  | ||||
| > [!TIP] | ||||
| > Lorem ipsum dolor sit amet, consectetur adipiscing elit. Integer dictum quis ante eu eleifend. Integer sed [consectetur est, nec elementum magna](#). Fusce lobortis lectus ac rutrum tincidunt. Quisque suscipit gravida ante, in convallis risus vulputate non. | ||||
|  | ||||
| key | description | ||||
| --  | -- | ||||
| lorem | ipsum | ||||
|  | ||||
| - list item 1 | ||||
| - list item 2 | ||||
|  | ||||
|  | ||||
|     `, | ||||
|     }) | ||||
|   ), | ||||
| ]; | ||||
|  | ||||
| @customElement("demo-misc-ha-markdown") | ||||
| export class DemoMiscMarkdown extends LitElement { | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <div class="container"> | ||||
|         ${markdownContents.map( | ||||
|           (md) => | ||||
|             html`<ha-card> | ||||
|               <ha-markdown | ||||
|                 .content=${generateContent(md)} | ||||
|                 .breaks=${md.breaks} | ||||
|                 .allowSvg=${md.allowSvg} | ||||
|                 .lazyImages=${md.lazyImages} | ||||
|               ></ha-markdown> | ||||
|             </ha-card>` | ||||
|         )} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles() { | ||||
|     return css` | ||||
|       ha-card { | ||||
|         margin: 12px; | ||||
|         padding: 12px; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "demo-misc-ha-markdown": DemoMiscMarkdown; | ||||
|   } | ||||
| } | ||||
| @@ -232,7 +232,6 @@ const createDeviceRegistryEntries = ( | ||||
|     labels: [], | ||||
|     created_at: 0, | ||||
|     modified_at: 0, | ||||
|     primary_config_entry: null, | ||||
|   }, | ||||
| ]; | ||||
|  | ||||
|   | ||||
| @@ -25,8 +25,8 @@ import type { HomeAssistant } from "../../../../src/types"; | ||||
| import { HassioRepositoryDialogParams } from "./show-dialog-repositories"; | ||||
| import type { HaTextField } from "../../../../src/components/ha-textfield"; | ||||
| import "../../../../src/components/ha-textfield"; | ||||
| import "../../../../src/components/ha-md-list"; | ||||
| import "../../../../src/components/ha-md-list-item"; | ||||
| import "../../../../src/components/ha-list-new"; | ||||
| import "../../../../src/components/ha-list-item-new"; | ||||
|  | ||||
| @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-md-list> | ||||
|           <ha-list-new> | ||||
|             ${repositories.length | ||||
|               ? repositories.map( | ||||
|                   (repo) => html` | ||||
|                     <ha-md-list-item class="option"> | ||||
|                     <ha-list-item-new class="option"> | ||||
|                       ${repo.name} | ||||
|                       <div slot="supporting-text"> | ||||
|                         <div>${repo.maintainer}</div> | ||||
| @@ -142,11 +142,11 @@ class HassioRepositoriesDialog extends LitElement { | ||||
|                           )} | ||||
|                         </simple-tooltip> | ||||
|                       </div> | ||||
|                     </ha-md-list-item> | ||||
|                     </ha-list-item-new> | ||||
|                   ` | ||||
|                 ) | ||||
|               : html`<ha-md-list-item> No repositories </ha-md-list-item>`} | ||||
|           </ha-md-list> | ||||
|               : html`<ha-list-item-new> No repositories </ha-list-item-new>`} | ||||
|           </ha-list-new> | ||||
|           <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-md-list-item { | ||||
|         ha-list-item-new { | ||||
|           position: relative; | ||||
|         } | ||||
|       `, | ||||
|   | ||||
							
								
								
									
										62
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										62
									
								
								package.json
									
									
									
									
									
								
							| @@ -25,15 +25,15 @@ | ||||
|   "license": "Apache-2.0", | ||||
|   "type": "module", | ||||
|   "dependencies": { | ||||
|     "@babel/runtime": "7.25.6", | ||||
|     "@babel/runtime": "7.25.4", | ||||
|     "@braintree/sanitize-url": "7.1.0", | ||||
|     "@codemirror/autocomplete": "6.18.1", | ||||
|     "@codemirror/commands": "6.6.2", | ||||
|     "@codemirror/language": "6.10.3", | ||||
|     "@codemirror/autocomplete": "6.18.0", | ||||
|     "@codemirror/commands": "6.6.0", | ||||
|     "@codemirror/language": "6.10.2", | ||||
|     "@codemirror/legacy-modes": "6.4.1", | ||||
|     "@codemirror/search": "6.5.6", | ||||
|     "@codemirror/state": "6.4.1", | ||||
|     "@codemirror/view": "6.33.0", | ||||
|     "@codemirror/view": "6.32.0", | ||||
|     "@egjs/hammerjs": "2.0.17", | ||||
|     "@formatjs/intl-datetimeformat": "6.12.5", | ||||
|     "@formatjs/intl-displaynames": "6.6.8", | ||||
| @@ -80,17 +80,16 @@ | ||||
|     "@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.2.0", | ||||
|     "@material/web": "2.1.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.9", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.4.9", | ||||
|     "@vaadin/combo-box": "24.4.6", | ||||
|     "@vaadin/vaadin-themable-mixin": "24.4.6", | ||||
|     "@vibrant/color": "3.2.1-alpha.1", | ||||
|     "@vibrant/core": "3.2.1-alpha.1", | ||||
|     "@vibrant/quantizer-mmcq": "3.2.1-alpha.1", | ||||
| @@ -103,11 +102,10 @@ | ||||
|     "comlink": "4.4.1", | ||||
|     "core-js": "3.38.1", | ||||
|     "cropperjs": "1.6.2", | ||||
|     "date-fns": "4.1.0", | ||||
|     "date-fns": "3.6.0", | ||||
|     "date-fns-tz": "3.1.3", | ||||
|     "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", | ||||
| @@ -117,10 +115,10 @@ | ||||
|     "intl-messageformat": "10.5.14", | ||||
|     "js-yaml": "4.1.0", | ||||
|     "leaflet": "1.9.4", | ||||
|     "leaflet-draw": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch", | ||||
|     "leaflet-draw": "1.0.4", | ||||
|     "lit": "2.8.0", | ||||
|     "luxon": "3.5.0", | ||||
|     "marked": "14.1.2", | ||||
|     "marked": "14.0.0", | ||||
|     "memoize-one": "6.0.0", | ||||
|     "node-vibrant": "3.2.1-alpha.1", | ||||
|     "proxy-polyfill": "0.3.2", | ||||
| @@ -129,13 +127,13 @@ | ||||
|     "qrcode": "1.5.4", | ||||
|     "roboto-fontface": "0.10.0", | ||||
|     "rrule": "2.8.1", | ||||
|     "sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch", | ||||
|     "sortablejs": "1.15.2", | ||||
|     "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.39", | ||||
|     "ua-parser-js": "1.0.38", | ||||
|     "unfetch": "5.0.0", | ||||
|     "vis-data": "7.1.9", | ||||
|     "vis-network": "9.1.9", | ||||
| @@ -157,17 +155,17 @@ | ||||
|     "@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.1", | ||||
|     "@bundle-stats/plugin-webpack-filter": "4.14.2", | ||||
|     "@koa/cors": "5.0.0", | ||||
|     "@lokalise/node-api": "12.7.0", | ||||
|     "@octokit/auth-oauth-device": "7.1.1", | ||||
|     "@octokit/plugin-retry": "7.1.2", | ||||
|     "@octokit/plugin-retry": "7.1.1", | ||||
|     "@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.4", | ||||
|     "@rollup/plugin-node-resolve": "15.2.3", | ||||
|     "@rollup/plugin-replace": "5.0.7", | ||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||
|     "@types/chromecast-caf-receiver": "6.0.17", | ||||
| @@ -191,20 +189,20 @@ | ||||
|     "@typescript-eslint/parser": "7.18.0", | ||||
|     "@web/dev-server": "0.1.38", | ||||
|     "@web/dev-server-rollup": "0.4.1", | ||||
|     "babel-loader": "9.2.1", | ||||
|     "babel-loader": "9.1.3", | ||||
|     "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.1", | ||||
|     "eslint": "8.57.0", | ||||
|     "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.15.0", | ||||
|     "eslint-import-resolver-webpack": "0.13.8", | ||||
|     "eslint-plugin-import": "2.29.1", | ||||
|     "eslint-plugin-lit": "1.14.0", | ||||
|     "eslint-plugin-lit-a11y": "4.1.4", | ||||
|     "eslint-plugin-unused-imports": "4.1.4", | ||||
|     "eslint-plugin-unused-imports": "4.1.3", | ||||
|     "eslint-plugin-wc": "2.1.1", | ||||
|     "fancy-log": "2.0.0", | ||||
|     "fs-extra": "11.2.0", | ||||
| @@ -215,10 +213,10 @@ | ||||
|     "gulp-rename": "2.0.0", | ||||
|     "gulp-zopfli-green": "6.0.2", | ||||
|     "html-minifier-terser": "7.2.0", | ||||
|     "husky": "9.1.6", | ||||
|     "husky": "9.1.5", | ||||
|     "instant-mocha": "1.5.2", | ||||
|     "jszip": "3.10.1", | ||||
|     "lint-staged": "15.2.10", | ||||
|     "lint-staged": "15.2.9", | ||||
|     "lit-analyzer": "2.0.3", | ||||
|     "lodash.merge": "4.6.2", | ||||
|     "lodash.template": "4.5.0", | ||||
| @@ -234,16 +232,16 @@ | ||||
|     "rollup-plugin-terser": "7.0.2", | ||||
|     "rollup-plugin-visualizer": "5.12.0", | ||||
|     "serve-handler": "6.1.5", | ||||
|     "sinon": "19.0.2", | ||||
|     "sinon": "18.0.0", | ||||
|     "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.6.2", | ||||
|     "typescript": "5.5.4", | ||||
|     "webpack": "5.94.0", | ||||
|     "webpack-cli": "5.1.4", | ||||
|     "webpack-dev-server": "5.1.0", | ||||
|     "webpack-dev-server": "5.0.4", | ||||
|     "webpack-manifest-plugin": "5.0.0", | ||||
|     "webpack-stats-plugin": "1.1.3", | ||||
|     "webpackbar": "6.0.1", | ||||
| @@ -256,7 +254,9 @@ | ||||
|     "lit": "2.8.0", | ||||
|     "clean-css": "5.3.3", | ||||
|     "@lit/reactive-element": "1.6.3", | ||||
|     "@fullcalendar/daygrid": "6.1.15" | ||||
|     "@fullcalendar/daygrid": "6.1.15", | ||||
|     "sortablejs@1.15.2": "patch:sortablejs@npm%3A1.15.2#~/.yarn/patches/sortablejs-npm-1.15.2-73347ae85a.patch", | ||||
|     "leaflet-draw@1.0.4": "patch:leaflet-draw@npm%3A1.0.4#./.yarn/patches/leaflet-draw-npm-1.0.4-0ca0ebcf65.patch" | ||||
|   }, | ||||
|   "packageManager": "yarn@4.5.0" | ||||
|   "packageManager": "yarn@4.4.0" | ||||
| } | ||||
|   | ||||
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 3.9 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 4.5 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 4.6 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 2.3 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 4.4 KiB | 
										
											Binary file not shown.
										
									
								
							| Before Width: | Height: | Size: 4.4 KiB | 
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name         = "home-assistant-frontend" | ||||
| version      = "20240926.0" | ||||
| version      = "20240809.0" | ||||
| license      = {text = "Apache-2.0"} | ||||
| description  = "The Home Assistant frontend" | ||||
| readme       = "README.md" | ||||
|   | ||||
| @@ -234,12 +234,7 @@ export const SENSOR_ENTITIES = [ | ||||
|   "weather", | ||||
| ]; | ||||
|  | ||||
| export const ASSIST_ENTITIES = [ | ||||
|   "assist_satellite", | ||||
|   "conversation", | ||||
|   "stt", | ||||
|   "tts", | ||||
| ]; | ||||
| export const ASSIST_ENTITIES = ["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 | ||||
|   | ||||
| @@ -71,7 +71,8 @@ export const computeStateDisplayFromEntityAttributes = ( | ||||
|     if ( | ||||
|       attributes.device_class === "duration" && | ||||
|       attributes.unit_of_measurement && | ||||
|       UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] | ||||
|       UNIT_TO_MILLISECOND_CONVERT[attributes.unit_of_measurement] && | ||||
|       entity?.display_precision === undefined | ||||
|     ) { | ||||
|       try { | ||||
|         return formatDuration(state, attributes.unit_of_measurement); | ||||
|   | ||||
| @@ -1,6 +0,0 @@ | ||||
| import type { ChartEvent } from "chart.js"; | ||||
|  | ||||
| export const clickIsTouch = (event: ChartEvent): boolean => | ||||
|   !(event.native instanceof MouseEvent) || | ||||
|   (event.native instanceof PointerEvent && | ||||
|     event.native.pointerType !== "mouse"); | ||||
| @@ -16,7 +16,6 @@ import { | ||||
|   HaChartBase, | ||||
|   MIN_TIME_BETWEEN_UPDATES, | ||||
| } from "./ha-chart-base"; | ||||
| import { clickIsTouch } from "./click_is_touch"; | ||||
|  | ||||
| const safeParseFloat = (value) => { | ||||
|   const parsed = parseFloat(value); | ||||
| @@ -221,7 +220,12 @@ export class StateHistoryChartLine extends LitElement { | ||||
|         // @ts-expect-error | ||||
|         locale: numberFormatToLocale(this.hass.locale), | ||||
|         onClick: (e: any) => { | ||||
|           if (!this.clickForMoreInfo || clickIsTouch(e)) { | ||||
|           if ( | ||||
|             !this.clickForMoreInfo || | ||||
|             !(e.native instanceof MouseEvent) || | ||||
|             (e.native instanceof PointerEvent && | ||||
|               e.native.pointerType !== "mouse") | ||||
|           ) { | ||||
|             return; | ||||
|           } | ||||
|  | ||||
|   | ||||
| @@ -16,7 +16,6 @@ import { | ||||
| } from "./ha-chart-base"; | ||||
| import type { TimeLineData } from "./timeline-chart/const"; | ||||
| import { computeTimelineColor } from "./timeline-chart/timeline-color"; | ||||
| import { clickIsTouch } from "./click_is_touch"; | ||||
|  | ||||
| @customElement("state-history-chart-timeline") | ||||
| export class StateHistoryChartTimeline extends LitElement { | ||||
| @@ -225,7 +224,11 @@ export class StateHistoryChartTimeline extends LitElement { | ||||
|       // @ts-expect-error | ||||
|       locale: numberFormatToLocale(this.hass.locale), | ||||
|       onClick: (e: any) => { | ||||
|         if (!this.clickForMoreInfo || clickIsTouch(e)) { | ||||
|         if ( | ||||
|           !this.clickForMoreInfo || | ||||
|           !(e.native instanceof MouseEvent) || | ||||
|           (e.native instanceof PointerEvent && e.native.pointerType !== "mouse") | ||||
|         ) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -39,7 +39,6 @@ import type { | ||||
|   ChartDatasetExtra, | ||||
|   HaChartBase, | ||||
| } from "./ha-chart-base"; | ||||
| import { clickIsTouch } from "./click_is_touch"; | ||||
|  | ||||
| export const supportedStatTypeMap: Record<StatisticType, StatisticType> = { | ||||
|   mean: "mean", | ||||
| @@ -279,7 +278,11 @@ export class StatisticsChart extends LitElement { | ||||
|       // @ts-expect-error | ||||
|       locale: numberFormatToLocale(this.hass.locale), | ||||
|       onClick: (e: any) => { | ||||
|         if (!this.clickForMoreInfo || clickIsTouch(e)) { | ||||
|         if ( | ||||
|           !this.clickForMoreInfo || | ||||
|           !(e.native instanceof MouseEvent) || | ||||
|           (e.native instanceof PointerEvent && e.native.pointerType !== "mouse") | ||||
|         ) { | ||||
|           return; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -25,6 +25,7 @@ import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { stringCompare } from "../../common/string/compare"; | ||||
| import { debounce } from "../../common/util/debounce"; | ||||
| import { groupBy } from "../../common/util/group-by"; | ||||
| import { nextRender } from "../../common/util/render-status"; | ||||
| import { haStyleScrollbar } from "../../resources/styles"; | ||||
| import { loadVirtualizer } from "../../resources/virtualizer"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| @@ -34,7 +35,6 @@ import "../ha-svg-icon"; | ||||
| import "../search-input"; | ||||
| import { filterData, sortData } from "./sort-filter"; | ||||
| import { LocalizeFunc } from "../../common/translations/localize"; | ||||
| import { nextRender } from "../../common/util/render-status"; | ||||
|  | ||||
| export interface RowClickedEvent { | ||||
|   id: string; | ||||
| @@ -169,6 +169,8 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   @query("slot[name='header']") private _header!: HTMLSlotElement; | ||||
|  | ||||
|   @state() private _items: DataTableRowData[] = []; | ||||
|  | ||||
|   @state() private _collapsedGroups: string[] = []; | ||||
|  | ||||
|   private _checkableRowsCount?: number; | ||||
| @@ -177,9 +179,7 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   private _sortColumns: SortableColumnContainer = {}; | ||||
|  | ||||
|   private _curRequest = 0; | ||||
|  | ||||
|   private _lastUpdate = 0; | ||||
|   private curRequest = 0; | ||||
|  | ||||
|   // @ts-ignore | ||||
|   @restoreScroll(".scroller") private _savedScrollPos?: number; | ||||
| @@ -206,9 +206,9 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   public connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     if (this._filteredData.length) { | ||||
|     if (this._items.length) { | ||||
|       // Force update of location of rows | ||||
|       this._filteredData = [...this._filteredData]; | ||||
|       this._items = [...this._items]; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -291,13 +291,16 @@ export class HaDataTable extends LitElement { | ||||
|       properties.has("columns") || | ||||
|       properties.has("_filter") || | ||||
|       properties.has("sortColumn") || | ||||
|       properties.has("sortDirection") | ||||
|       properties.has("sortDirection") || | ||||
|       properties.has("groupColumn") || | ||||
|       properties.has("groupOrder") || | ||||
|       properties.has("_collapsedGroups") | ||||
|     ) { | ||||
|       this._sortFilterData(); | ||||
|     } | ||||
|  | ||||
|     if (properties.has("selectable") || properties.has("hiddenColumns")) { | ||||
|       this._filteredData = [...this._filteredData]; | ||||
|       this._items = [...this._items]; | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -464,15 +467,7 @@ export class HaDataTable extends LitElement { | ||||
|                   scroller | ||||
|                   class="mdc-data-table__content scroller ha-scrollbar" | ||||
|                   @scroll=${this._saveScrollPos} | ||||
|                   .items=${this._groupData( | ||||
|                     this._filteredData, | ||||
|                     localize, | ||||
|                     this.appendRow, | ||||
|                     this.hasFab, | ||||
|                     this.groupColumn, | ||||
|                     this.groupOrder, | ||||
|                     this._collapsedGroups | ||||
|                   )} | ||||
|                   .items=${this._items} | ||||
|                   .keyFunction=${this._keyFunction} | ||||
|                   .renderItem=${renderRow} | ||||
|                 ></lit-virtualizer> | ||||
| @@ -607,13 +602,8 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   private async _sortFilterData() { | ||||
|     const startTime = new Date().getTime(); | ||||
|     const timeBetweenUpdate = startTime - this._lastUpdate; | ||||
|     const timeBetweenRequest = startTime - this._curRequest; | ||||
|     this._curRequest = startTime; | ||||
|  | ||||
|     const forceUpdate = | ||||
|       !this._lastUpdate || | ||||
|       (timeBetweenUpdate > 500 && timeBetweenRequest < 500); | ||||
|     this.curRequest++; | ||||
|     const curRequest = this.curRequest; | ||||
|  | ||||
|     let filteredData = this.data; | ||||
|     if (this._filter) { | ||||
| @@ -624,10 +614,6 @@ export class HaDataTable extends LitElement { | ||||
|       ); | ||||
|     } | ||||
|  | ||||
|     if (!forceUpdate && this._curRequest !== startTime) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const prom = this.sortColumn | ||||
|       ? sortData( | ||||
|           filteredData, | ||||
| @@ -648,30 +634,17 @@ export class HaDataTable extends LitElement { | ||||
|         setTimeout(resolve, 100 - elapsed); | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (!forceUpdate && this._curRequest !== startTime) { | ||||
|     if (this.curRequest !== curRequest) { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     this._lastUpdate = startTime; | ||||
|     this._filteredData = data; | ||||
|   } | ||||
|     const localize = this.localizeFunc || this.hass.localize; | ||||
|  | ||||
|   private _groupData = memoizeOne( | ||||
|     ( | ||||
|       data: DataTableRowData[], | ||||
|       localize: LocalizeFunc, | ||||
|       appendRow, | ||||
|       hasFab: boolean, | ||||
|       groupColumn: string | undefined, | ||||
|       groupOrder: string[] | undefined, | ||||
|       collapsedGroups: string[] | ||||
|     ) => { | ||||
|       if (appendRow || hasFab || groupColumn) { | ||||
|     if (this.appendRow || this.hasFab || this.groupColumn) { | ||||
|       let items = [...data]; | ||||
|  | ||||
|         if (groupColumn) { | ||||
|           const grouped = groupBy(items, (item) => item[groupColumn]); | ||||
|       if (this.groupColumn) { | ||||
|         const grouped = groupBy(items, (item) => item[this.groupColumn!]); | ||||
|         if (grouped.undefined) { | ||||
|           // make sure ungrouped items are at the bottom | ||||
|           grouped[UNDEFINED_GROUP_KEY] = grouped.undefined; | ||||
| @@ -681,8 +654,8 @@ export class HaDataTable extends LitElement { | ||||
|           [key: string]: DataTableRowData[]; | ||||
|         } = Object.keys(grouped) | ||||
|           .sort((a, b) => { | ||||
|               const orderA = groupOrder?.indexOf(a) ?? -1; | ||||
|               const orderB = groupOrder?.indexOf(b) ?? -1; | ||||
|             const orderA = this.groupOrder?.indexOf(a) ?? -1; | ||||
|             const orderB = this.groupOrder?.indexOf(b) ?? -1; | ||||
|             if (orderA !== orderB) { | ||||
|               if (orderA === -1) { | ||||
|                 return 1; | ||||
| @@ -714,7 +687,7 @@ export class HaDataTable extends LitElement { | ||||
|             > | ||||
|               <ha-icon-button | ||||
|                 .path=${mdiChevronUp} | ||||
|                   class=${collapsedGroups.includes(groupName) | ||||
|                 class=${this._collapsedGroups.includes(groupName) | ||||
|                   ? "collapsed" | ||||
|                   : ""} | ||||
|               > | ||||
| @@ -724,26 +697,27 @@ export class HaDataTable extends LitElement { | ||||
|                 : groupName || ""} | ||||
|             </div>`, | ||||
|           }); | ||||
|             if (!collapsedGroups.includes(groupName)) { | ||||
|           if (!this._collapsedGroups.includes(groupName)) { | ||||
|             groupedItems.push(...rows); | ||||
|           } | ||||
|         }); | ||||
|         items = groupedItems; | ||||
|       } | ||||
|  | ||||
|         if (appendRow) { | ||||
|           items.push({ append: true, content: appendRow }); | ||||
|       if (this.appendRow) { | ||||
|         items.push({ append: true, content: this.appendRow }); | ||||
|       } | ||||
|  | ||||
|         if (hasFab) { | ||||
|       if (this.hasFab) { | ||||
|         items.push({ empty: true }); | ||||
|       } | ||||
|  | ||||
|         return items; | ||||
|       this._items = items; | ||||
|     } else { | ||||
|       this._items = data; | ||||
|     } | ||||
|       return data; | ||||
|     this._filteredData = data; | ||||
|   } | ||||
|   ); | ||||
|  | ||||
|   private _memFilterData = memoizeOne( | ||||
|     ( | ||||
| @@ -828,8 +802,8 @@ export class HaDataTable extends LitElement { | ||||
|  | ||||
|   private _checkedRowsChanged() { | ||||
|     // force scroller to update, change it's items | ||||
|     if (this._filteredData.length) { | ||||
|       this._filteredData = [...this._filteredData]; | ||||
|     if (this._items.length) { | ||||
|       this._items = [...this._items]; | ||||
|     } | ||||
|     fireEvent(this, "selection-changed", { | ||||
|       value: this._checkedRows, | ||||
|   | ||||
| @@ -26,7 +26,7 @@ class HaDeviceTriggerPicker extends HaDeviceAutomationPicker<DeviceTrigger> { | ||||
|       fetchDeviceTriggers, | ||||
|       (deviceId?: string) => ({ | ||||
|         device_id: deviceId || "", | ||||
|         trigger: "device", | ||||
|         platform: "device", | ||||
|         domain: "", | ||||
|         entity_id: "", | ||||
|       }) | ||||
|   | ||||
| @@ -1,9 +1,10 @@ | ||||
| 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 { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||
| import type { ValueChangedEvent, HomeAssistant } from "../../types"; | ||||
| import "./ha-entity-picker"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker"; | ||||
|  | ||||
| @@ -97,7 +98,10 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|               .excludeEntities=${this.excludeEntities} | ||||
|               .includeDeviceClasses=${this.includeDeviceClasses} | ||||
|               .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} | ||||
|               .entityFilter=${this.entityFilter} | ||||
|               .entityFilter=${this._getEntityFilter( | ||||
|                 this.value, | ||||
|                 this.entityFilter | ||||
|               )} | ||||
|               .value=${entityId} | ||||
|               .label=${this.pickedEntityLabel} | ||||
|               .disabled=${this.disabled} | ||||
| @@ -114,13 +118,10 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|           .includeDomains=${this.includeDomains} | ||||
|           .excludeDomains=${this.excludeDomains} | ||||
|           .includeEntities=${this.includeEntities} | ||||
|           .excludeEntities=${this._excludeEntities( | ||||
|             this.value, | ||||
|             this.excludeEntities | ||||
|           )} | ||||
|           .excludeEntities=${this.excludeEntities} | ||||
|           .includeDeviceClasses=${this.includeDeviceClasses} | ||||
|           .includeUnitOfMeasurement=${this.includeUnitOfMeasurement} | ||||
|           .entityFilter=${this.entityFilter} | ||||
|           .entityFilter=${this._getEntityFilter(this.value, this.entityFilter)} | ||||
|           .label=${this.pickEntityLabel} | ||||
|           .helper=${this.helper} | ||||
|           .disabled=${this.disabled} | ||||
| @@ -132,16 +133,14 @@ class HaEntitiesPickerLight extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _excludeEntities = memoizeOne( | ||||
|   private _getEntityFilter = memoizeOne( | ||||
|     ( | ||||
|       value: string[] | undefined, | ||||
|       excludeEntities: string[] | undefined | ||||
|     ): string[] | undefined => { | ||||
|       if (value === undefined) { | ||||
|         return excludeEntities; | ||||
|       } | ||||
|       return [...(excludeEntities || []), ...value]; | ||||
|     } | ||||
|       entityFilter: HaEntityPickerEntityFilterFunc | undefined | ||||
|     ): HaEntityPickerEntityFilterFunc => | ||||
|       (stateObj: HassEntity) => | ||||
|         (!value || !value.includes(stateObj.entity_id)) && | ||||
|         (!entityFilter || entityFilter(stateObj)) | ||||
|   ); | ||||
|  | ||||
|   private get _currentEntities() { | ||||
|   | ||||
| @@ -87,7 +87,7 @@ export class HaEntityPicker extends LitElement { | ||||
|   public includeUnitOfMeasurement?: string[]; | ||||
|  | ||||
|   /** | ||||
|    * List of allowed entities to show. | ||||
|    * List of allowed entities to show. Will ignore all other filters. | ||||
|    * @type {Array} | ||||
|    * @attr include-entities | ||||
|    */ | ||||
| @@ -220,13 +220,30 @@ export class HaEntityPicker extends LitElement { | ||||
|  | ||||
|       if (includeEntities) { | ||||
|         entityIds = entityIds.filter((entityId) => | ||||
|           includeEntities.includes(entityId) | ||||
|           this.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,7 +173,6 @@ 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 } from "home-assistant-js-websocket"; | ||||
| import { HassEntity, UnsubscribeFunc } 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,7 +20,12 @@ import { | ||||
|   getDeviceEntityDisplayLookup, | ||||
| } from "../data/device_registry"; | ||||
| import { EntityRegistryDisplayEntry } from "../data/entity_registry"; | ||||
| import { FloorRegistryEntry, getFloorAreaLookup } from "../data/floor_registry"; | ||||
| import { | ||||
|   FloorRegistryEntry, | ||||
|   getFloorAreaLookup, | ||||
|   subscribeFloorRegistry, | ||||
| } from "../data/floor_registry"; | ||||
| import { SubscribeMixin } from "../mixins/subscribe-mixin"; | ||||
| import { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||
| import "./ha-combo-box"; | ||||
| @@ -45,7 +50,7 @@ interface FloorAreaEntry { | ||||
| } | ||||
|  | ||||
| @customElement("ha-area-floor-picker") | ||||
| export class HaAreaFloorPicker extends LitElement { | ||||
| export class HaAreaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public label?: string; | ||||
| @@ -106,12 +111,22 @@ export class HaAreaFloorPicker extends 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(); | ||||
| @@ -416,12 +431,12 @@ export class HaAreaFloorPicker extends LitElement { | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if ( | ||||
|       (!this._init && this.hass) || | ||||
|       (!this._init && this.hass && this._floors) || | ||||
|       (this._init && changedProps.has("_opened") && this._opened) | ||||
|     ) { | ||||
|       this._init = true; | ||||
|       const areas = this._getAreas( | ||||
|         Object.values(this.hass.floors), | ||||
|         this._floors!, | ||||
|         Object.values(this.hass.areas), | ||||
|         Object.values(this.hass.devices), | ||||
|         Object.values(this.hass.entities), | ||||
|   | ||||
| @@ -1,155 +0,0 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import "./ha-ripple"; | ||||
|  | ||||
| type BadgeType = "badge" | "button"; | ||||
|  | ||||
| @customElement("ha-badge") | ||||
| export class HaBadge extends LitElement { | ||||
|   @property() public type: BadgeType = "badge"; | ||||
|  | ||||
|   @property() public label?: string; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "icon-only" }) iconOnly = false; | ||||
|  | ||||
|   protected render() { | ||||
|     const label = this.label; | ||||
|  | ||||
|     return html` | ||||
|       <div | ||||
|         class="badge ${classMap({ | ||||
|           "icon-only": this.iconOnly, | ||||
|         })}" | ||||
|         role=${ifDefined(this.type === "button" ? "button" : undefined)} | ||||
|         tabindex=${ifDefined(this.type === "button" ? "0" : undefined)} | ||||
|       > | ||||
|         <ha-ripple .disabled=${this.type !== "button"}></ha-ripple> | ||||
|         <slot name="icon"></slot> | ||||
|         ${this.iconOnly | ||||
|           ? nothing | ||||
|           : html`<span class="info"> | ||||
|               ${label ? html`<span class="label">${label}</span>` : nothing} | ||||
|               <span class="content"><slot></slot></span> | ||||
|             </span>`} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static get styles(): CSSResultGroup { | ||||
|     return css` | ||||
|       :host { | ||||
|         --badge-color: var(--secondary-text-color); | ||||
|         -webkit-tap-highlight-color: transparent; | ||||
|       } | ||||
|       .badge { | ||||
|         position: relative; | ||||
|         --ha-ripple-color: var(--badge-color); | ||||
|         --ha-ripple-hover-opacity: 0.04; | ||||
|         --ha-ripple-pressed-opacity: 0.12; | ||||
|         transition: | ||||
|           box-shadow 180ms ease-in-out, | ||||
|           border-color 180ms ease-in-out; | ||||
|         display: flex; | ||||
|         flex-direction: row; | ||||
|         align-items: center; | ||||
|         justify-content: center; | ||||
|         gap: 8px; | ||||
|         height: var(--ha-badge-size, 36px); | ||||
|         min-width: var(--ha-badge-size, 36px); | ||||
|         padding: 0px 12px; | ||||
|         box-sizing: border-box; | ||||
|         width: auto; | ||||
|         border-radius: var( | ||||
|           --ha-badge-border-radius, | ||||
|           calc(var(--ha-badge-size, 36px) / 2) | ||||
|         ); | ||||
|         background: var( | ||||
|           --ha-card-background, | ||||
|           var(--card-background-color, white) | ||||
|         ); | ||||
|         -webkit-backdrop-filter: var(--ha-card-backdrop-filter, none); | ||||
|         backdrop-filter: var(--ha-card-backdrop-filter, none); | ||||
|         border-width: var(--ha-card-border-width, 1px); | ||||
|         box-shadow: var(--ha-card-box-shadow, none); | ||||
|         border-style: solid; | ||||
|         border-color: var( | ||||
|           --ha-card-border-color, | ||||
|           var(--divider-color, #e0e0e0) | ||||
|         ); | ||||
|       } | ||||
|       .badge:focus-visible { | ||||
|         --shadow-default: var(--ha-card-box-shadow, 0 0 0 0 transparent); | ||||
|         --shadow-focus: 0 0 0 1px var(--badge-color); | ||||
|         border-color: var(--badge-color); | ||||
|         box-shadow: var(--shadow-default), var(--shadow-focus); | ||||
|       } | ||||
|       [role="button"] { | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       [role="button"]:focus { | ||||
|         outline: none; | ||||
|       } | ||||
|       .info { | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         align-items: flex-start; | ||||
|         padding-inline-start: initial; | ||||
|         text-align: center; | ||||
|         font-family: Roboto; | ||||
|       } | ||||
|       .label { | ||||
|         font-size: 10px; | ||||
|         font-style: normal; | ||||
|         font-weight: 500; | ||||
|         line-height: 10px; | ||||
|         letter-spacing: 0.1px; | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|       .content { | ||||
|         font-size: 12px; | ||||
|         font-style: normal; | ||||
|         font-weight: 500; | ||||
|         line-height: 16px; | ||||
|         letter-spacing: 0.1px; | ||||
|         color: var(--primary-text-color); | ||||
|       } | ||||
|       ::slotted([slot="icon"]) { | ||||
|         --mdc-icon-size: 18px; | ||||
|         color: var(--badge-color); | ||||
|         line-height: 0; | ||||
|         margin-left: -4px; | ||||
|         margin-right: 0; | ||||
|         margin-inline-start: -4px; | ||||
|         margin-inline-end: 0; | ||||
|       } | ||||
|       ::slotted(img[slot="icon"]) { | ||||
|         width: 30px; | ||||
|         height: 30px; | ||||
|         border-radius: 50%; | ||||
|         object-fit: cover; | ||||
|         overflow: hidden; | ||||
|         margin-left: -10px; | ||||
|         margin-right: 0; | ||||
|         margin-inline-start: -10px; | ||||
|         margin-inline-end: 0; | ||||
|       } | ||||
|       .badge.icon-only { | ||||
|         padding: 0; | ||||
|       } | ||||
|       .badge.icon-only ::slotted([slot="icon"]) { | ||||
|         margin-left: 0; | ||||
|         margin-right: 0; | ||||
|         margin-inline-start: 0; | ||||
|         margin-inline-end: 0; | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-badge": HaBadge; | ||||
|   } | ||||
| } | ||||
| @@ -6,8 +6,8 @@ import type { HaIconButton } from "./ha-icon-button"; | ||||
| import "./ha-menu"; | ||||
| import type { HaMenu } from "./ha-menu"; | ||||
| 
 | ||||
| @customElement("ha-md-button-menu") | ||||
| export class HaMdButtonMenu extends LitElement { | ||||
| @customElement("ha-button-menu-new") | ||||
| export class HaButtonMenuNew extends LitElement { | ||||
|   protected readonly [FOCUS_TARGET]; | ||||
| 
 | ||||
|   @property({ type: Boolean }) public disabled = false; | ||||
| @@ -84,6 +84,6 @@ export class HaMdButtonMenu extends LitElement { | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-md-button-menu": HaMdButtonMenu; | ||||
|     "ha-button-menu-new": HaButtonMenuNew; | ||||
|   } | ||||
| } | ||||
| @@ -124,12 +124,9 @@ export class HaCodeEditor extends ReactiveElement { | ||||
|     const transactions: TransactionSpec[] = []; | ||||
|     if (changedProps.has("mode")) { | ||||
|       transactions.push({ | ||||
|         effects: [ | ||||
|           this._loadedCodeMirror!.langCompartment!.reconfigure(this._mode), | ||||
|           this._loadedCodeMirror!.foldingCompartment.reconfigure( | ||||
|             this._getFoldingExtensions() | ||||
|         effects: this._loadedCodeMirror!.langCompartment!.reconfigure( | ||||
|           this._mode | ||||
|         ), | ||||
|         ], | ||||
|       }); | ||||
|     } | ||||
|     if (changedProps.has("readOnly")) { | ||||
| @@ -180,14 +177,6 @@ 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, | ||||
| @@ -205,9 +194,6 @@ 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) { | ||||
| @@ -325,17 +311,6 @@ 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,15 +1,14 @@ | ||||
| import { mdiInvertColorsOff, mdiPalette } from "@mdi/js"; | ||||
| import "@material/mwc-list/mwc-list-item"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } 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 { LocalizeKeys } from "../common/translations/localize"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import "./ha-md-divider"; | ||||
| import "./ha-list-item"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { LocalizeKeys } from "../common/translations/localize"; | ||||
|  | ||||
| @customElement("ha-color-picker") | ||||
| export class HaColorPicker extends LitElement { | ||||
| @@ -21,81 +20,43 @@ export class HaColorPicker extends LitElement { | ||||
|  | ||||
|   @property() public value?: string; | ||||
|  | ||||
|   @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 defaultColor = false; | ||||
|  | ||||
|   @property({ type: Boolean }) public disabled = false; | ||||
|  | ||||
|   _valueSelected(ev) { | ||||
|     const value = ev.target.value; | ||||
|     this.value = value === this.defaultColor ? undefined : value; | ||||
|     if (value) { | ||||
|       fireEvent(this, "value-changed", { | ||||
|       value: this.value, | ||||
|         value: value !== "default" ? value : undefined, | ||||
|       }); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   render() { | ||||
|     const value = this.value || this.defaultColor; | ||||
|  | ||||
|     return html` | ||||
|       <ha-select | ||||
|         .icon=${Boolean(value)} | ||||
|         .icon=${Boolean(this.value)} | ||||
|         .label=${this.label} | ||||
|         .value=${value} | ||||
|         .value=${this.value || "default"} | ||||
|         .helper=${this.helper} | ||||
|         .disabled=${this.disabled} | ||||
|         @closed=${stopPropagation} | ||||
|         @selected=${this._valueSelected} | ||||
|         fixedMenuPosition | ||||
|         naturalMenuWidth | ||||
|         .clearable=${!this.defaultColor} | ||||
|       > | ||||
|         ${value | ||||
|         ${this.value | ||||
|           ? html` | ||||
|               <span slot="icon"> | ||||
|                 ${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")} | ||||
|                 ${this.renderColorCircle(this.value || "grey")} | ||||
|               </span> | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${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>` | ||||
|         ${this.defaultColor | ||||
|           ? html` <ha-list-item value="default"> | ||||
|               ${this.hass.localize(`ui.components.color-picker.default_color`)} | ||||
|             </ha-list-item>` | ||||
|           : nothing} | ||||
|         ${Array.from(THEME_COLORS).map( | ||||
|           (color) => html` | ||||
| @@ -103,9 +64,6 @@ 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> | ||||
|           ` | ||||
| @@ -129,11 +87,10 @@ export class HaColorPicker extends LitElement { | ||||
|     return css` | ||||
|       .circle-color { | ||||
|         display: block; | ||||
|         background-color: var(--circle-color, var(--divider-color)); | ||||
|         background-color: var(--circle-color); | ||||
|         border-radius: 10px; | ||||
|         width: 20px; | ||||
|         height: 20px; | ||||
|         box-sizing: border-box; | ||||
|       } | ||||
|       ha-select { | ||||
|         width: 100%; | ||||
|   | ||||
| @@ -45,35 +45,15 @@ export class HaConversationAgentPicker extends LitElement { | ||||
|     if (!this._agents) { | ||||
|       return nothing; | ||||
|     } | ||||
|     let value = this.value; | ||||
|     if (!value && this.required) { | ||||
|       // Select Home Assistant conversation agent if it supports the language | ||||
|       for (const agent of this._agents) { | ||||
|         if ( | ||||
|           agent.id === "conversation.home_assistant" && | ||||
|           agent.supported_languages.includes(this.language!) | ||||
|         ) { | ||||
|           value = agent.id; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|       if (!value) { | ||||
|         // Select the first agent that supports the language | ||||
|         for (const agent of this._agents) { | ||||
|           if ( | ||||
|             agent.supported_languages === "*" && | ||||
|             agent.supported_languages.includes(this.language!) | ||||
|           ) { | ||||
|             value = agent.id; | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (!value) { | ||||
|       value = NONE; | ||||
|     } | ||||
|  | ||||
|     const value = | ||||
|       this.value ?? | ||||
|       (this.required && | ||||
|       (!this.language || | ||||
|         this._agents | ||||
|           .find((agent) => agent.id === "homeassistant") | ||||
|           ?.supported_languages.includes(this.language)) | ||||
|         ? "homeassistant" | ||||
|         : NONE); | ||||
|     return html` | ||||
|       <ha-select | ||||
|         .label=${this.label || | ||||
|   | ||||
| @@ -10,13 +10,8 @@ export class HaDialogHeader extends LitElement { | ||||
|           <section class="header-navigation-icon"> | ||||
|             <slot name="navigationIcon"></slot> | ||||
|           </section> | ||||
|           <section class="header-content"> | ||||
|             <div class="header-title"> | ||||
|           <section 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> | ||||
| @@ -44,24 +39,17 @@ export class HaDialogHeader extends LitElement { | ||||
|           padding: 4px; | ||||
|           box-sizing: border-box; | ||||
|         } | ||||
|         .header-content { | ||||
|         .header-title { | ||||
|           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; | ||||
|   | ||||
| @@ -68,8 +68,8 @@ export class HaExpansionPanel extends LitElement { | ||||
|                 ></ha-svg-icon> | ||||
|               ` | ||||
|             : ""} | ||||
|           <slot name="icons"></slot> | ||||
|         </div> | ||||
|         <slot name="icons"></slot> | ||||
|       </div> | ||||
|       <div | ||||
|         class="container ${classMap({ expanded: this.expanded })}" | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| import "@material/mwc-menu/mwc-menu-surface"; | ||||
| import { mdiFilterVariantRemove, mdiTextureBox } from "@mdi/js"; | ||||
| import { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   CSSResultGroup, | ||||
|   LitElement, | ||||
| @@ -14,8 +15,13 @@ 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 { getFloorAreaLookup } from "../data/floor_registry"; | ||||
| import { | ||||
|   FloorRegistryEntry, | ||||
|   getFloorAreaLookup, | ||||
|   subscribeFloorRegistry, | ||||
| } 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"; | ||||
| @@ -25,7 +31,7 @@ import "./ha-svg-icon"; | ||||
| import "./ha-tree-indicator"; | ||||
|  | ||||
| @customElement("ha-filter-floor-areas") | ||||
| export class HaFilterFloorAreas extends LitElement { | ||||
| export class HaFilterFloorAreas extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public value?: { | ||||
| @@ -41,6 +47,8 @@ export class HaFilterFloorAreas extends LitElement { | ||||
|  | ||||
|   @state() private _shouldRender = false; | ||||
|  | ||||
|   @state() private _floors?: FloorRegistryEntry[]; | ||||
|  | ||||
|   public willUpdate(properties: PropertyValues) { | ||||
|     super.willUpdate(properties); | ||||
|  | ||||
| @@ -52,7 +60,7 @@ export class HaFilterFloorAreas extends LitElement { | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     const areas = this._areas(this.hass.areas, this.hass.floors); | ||||
|     const areas = this._areas(this.hass.areas, this._floors); | ||||
|  | ||||
|     return html` | ||||
|       <ha-expansion-panel | ||||
| @@ -181,6 +189,14 @@ export class HaFilterFloorAreas extends 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(() => { | ||||
| @@ -204,9 +220,9 @@ export class HaFilterFloorAreas extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _areas = memoizeOne( | ||||
|     (areaReg: HomeAssistant["areas"], floorReg: HomeAssistant["floors"]) => { | ||||
|     (areaReg: HomeAssistant["areas"], floors?: FloorRegistryEntry[]) => { | ||||
|       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 } from "home-assistant-js-websocket"; | ||||
| import { HassEntity, UnsubscribeFunc } 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,8 +24,10 @@ 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"; | ||||
| @@ -51,7 +53,7 @@ const rowRenderer: ComboBoxLitRenderer<FloorRegistryEntry> = (item) => | ||||
|   </ha-list-item>`; | ||||
|  | ||||
| @customElement("ha-floor-picker") | ||||
| export class HaFloorPicker extends LitElement { | ||||
| export class HaFloorPicker extends SubscribeMixin(LitElement) { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public label?: string; | ||||
| @@ -109,6 +111,8 @@ export class HaFloorPicker extends LitElement { | ||||
|  | ||||
|   @state() private _opened?: boolean; | ||||
|  | ||||
|   @state() private _floors?: FloorRegistryEntry[]; | ||||
|  | ||||
|   @query("ha-combo-box", true) public comboBox!: HaComboBox; | ||||
|  | ||||
|   private _suggestion?: string; | ||||
| @@ -125,6 +129,14 @@ export class HaFloorPicker extends LitElement { | ||||
|     await this.comboBox?.focus(); | ||||
|   } | ||||
|  | ||||
|   protected hassSubscribe(): (UnsubscribeFunc | Promise<UnsubscribeFunc>)[] { | ||||
|     return [ | ||||
|       subscribeFloorRegistry(this.hass.connection, (floors) => { | ||||
|         this._floors = floors; | ||||
|       }), | ||||
|     ]; | ||||
|   } | ||||
|  | ||||
|   private _getFloors = memoizeOne( | ||||
|     ( | ||||
|       floors: FloorRegistryEntry[], | ||||
| @@ -308,12 +320,12 @@ export class HaFloorPicker extends LitElement { | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if ( | ||||
|       (!this._init && this.hass) || | ||||
|       (!this._init && this.hass && this._floors) || | ||||
|       (this._init && changedProps.has("_opened") && this._opened) | ||||
|     ) { | ||||
|       this._init = true; | ||||
|       const floors = this._getFloors( | ||||
|         Object.values(this.hass.floors), | ||||
|         this._floors!, | ||||
|         Object.values(this.hass.areas), | ||||
|         Object.values(this.hass.devices), | ||||
|         Object.values(this.hass.entities), | ||||
| @@ -348,7 +360,8 @@ export class HaFloorPicker extends LitElement { | ||||
|           ? this.hass.localize("ui.components.floor-picker.floor") | ||||
|           : this.label} | ||||
|         .placeholder=${this.placeholder | ||||
|           ? this.hass.floors[this.placeholder]?.name | ||||
|           ? this._floors?.find((floor) => floor.floor_id === this.placeholder) | ||||
|               ?.name | ||||
|           : undefined} | ||||
|         .renderer=${rowRenderer} | ||||
|         @filter-changed=${this._filterChanged} | ||||
| @@ -447,7 +460,7 @@ export class HaFloorPicker extends LitElement { | ||||
|               floor_id: floor.floor_id, | ||||
|             }); | ||||
|           }); | ||||
|           const floors = [...Object.values(this.hass.floors), floor]; | ||||
|           const floors = [...this._floors!, floor]; | ||||
|           this.comboBox.filteredItems = this._getFloors( | ||||
|             floors, | ||||
|             Object.values(this.hass.areas)!, | ||||
|   | ||||
| @@ -95,10 +95,10 @@ export const computeInitialHaFormData = ( | ||||
|       } else if ( | ||||
|         "action" in selector || | ||||
|         "trigger" in selector || | ||||
|         "condition" in selector | ||||
|         "condition" in selector || | ||||
|         "media" in selector || | ||||
|         "target" in selector | ||||
|       ) { | ||||
|         data[field.name] = []; | ||||
|       } else if ("media" in selector || "target" in selector) { | ||||
|         data[field.name] = {}; | ||||
|       } else { | ||||
|         throw new Error( | ||||
|   | ||||
| @@ -21,45 +21,13 @@ export class HaFormExpendable extends LitElement implements HaFormElement { | ||||
|  | ||||
|   @property({ attribute: false }) public computeLabel?: ( | ||||
|     schema: HaFormSchema, | ||||
|     data?: HaFormDataContainer, | ||||
|     options?: { path?: string[] } | ||||
|     data?: HaFormDataContainer | ||||
|   ) => string; | ||||
|  | ||||
|   @property({ attribute: false }) public computeHelper?: ( | ||||
|     schema: HaFormSchema, | ||||
|     options?: { path?: string[] } | ||||
|     schema: HaFormSchema | ||||
|   ) => string; | ||||
|  | ||||
|   private _renderDescription() { | ||||
|     const description = this.computeHelper?.(this.schema); | ||||
|     return description ? html`<p>${description}</p>` : nothing; | ||||
|   } | ||||
|  | ||||
|   private _computeLabel = ( | ||||
|     schema: HaFormSchema, | ||||
|     data?: HaFormDataContainer, | ||||
|     options?: { path?: string[] } | ||||
|   ) => { | ||||
|     if (!this.computeLabel) return this.computeLabel; | ||||
|  | ||||
|     return this.computeLabel(schema, data, { | ||||
|       ...options, | ||||
|       path: [...(options?.path || []), this.schema.name], | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   private _computeHelper = ( | ||||
|     schema: HaFormSchema, | ||||
|     options?: { path?: string[] } | ||||
|   ) => { | ||||
|     if (!this.computeHelper) return this.computeHelper; | ||||
|  | ||||
|     return this.computeHelper(schema, { | ||||
|       ...options, | ||||
|       path: [...(options?.path || []), this.schema.name], | ||||
|     }); | ||||
|   }; | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <ha-expansion-panel outlined .expanded=${Boolean(this.schema.expanded)}> | ||||
| @@ -75,17 +43,16 @@ export class HaFormExpendable extends LitElement implements HaFormElement { | ||||
|                   <ha-svg-icon .path=${this.schema.iconPath}></ha-svg-icon> | ||||
|                 ` | ||||
|               : nothing} | ||||
|           ${this.schema.title || this.computeLabel?.(this.schema)} | ||||
|           ${this.schema.title} | ||||
|         </div> | ||||
|         <div class="content"> | ||||
|           ${this._renderDescription()} | ||||
|           <ha-form | ||||
|             .hass=${this.hass} | ||||
|             .data=${this.data} | ||||
|             .schema=${this.schema.schema} | ||||
|             .disabled=${this.disabled} | ||||
|             .computeLabel=${this._computeLabel} | ||||
|             .computeHelper=${this._computeHelper} | ||||
|             .computeLabel=${this.computeLabel} | ||||
|             .computeHelper=${this.computeHelper} | ||||
|           ></ha-form> | ||||
|         </div> | ||||
|       </ha-expansion-panel> | ||||
| @@ -104,9 +71,6 @@ export class HaFormExpendable extends LitElement implements HaFormElement { | ||||
|       .content { | ||||
|         padding: 12px; | ||||
|       } | ||||
|       .content p { | ||||
|         margin: 0 0 24px; | ||||
|       } | ||||
|       ha-expansion-panel { | ||||
|         display: block; | ||||
|         --expansion-panel-content-padding: 0; | ||||
|   | ||||
| @@ -31,7 +31,7 @@ const LOAD_ELEMENTS = { | ||||
| }; | ||||
|  | ||||
| const getValue = (obj, item) => | ||||
|   obj ? (!item.name || item.flatten ? obj : obj[item.name]) : null; | ||||
|   obj ? (!item.name ? obj : obj[item.name]) : null; | ||||
|  | ||||
| const getError = (obj, item) => (obj && item.name ? obj[item.name] : null); | ||||
|  | ||||
| @@ -204,8 +204,7 @@ export class HaForm extends LitElement implements HaFormElement { | ||||
|  | ||||
|       if (ev.target === this) return; | ||||
|  | ||||
|       const newValue = | ||||
|         !schema.name || ("flatten" in schema && schema.flatten) | ||||
|       const newValue = !schema.name | ||||
|         ? ev.detail.value | ||||
|         : { [schema.name]: ev.detail.value }; | ||||
|  | ||||
|   | ||||
| @@ -31,15 +31,15 @@ export interface HaFormBaseSchema { | ||||
|  | ||||
| export interface HaFormGridSchema extends HaFormBaseSchema { | ||||
|   type: "grid"; | ||||
|   flatten?: boolean; | ||||
|   name: string; | ||||
|   column_min_width?: string; | ||||
|   schema: readonly HaFormSchema[]; | ||||
| } | ||||
|  | ||||
| export interface HaFormExpandableSchema extends HaFormBaseSchema { | ||||
|   type: "expandable"; | ||||
|   flatten?: boolean; | ||||
|   title?: string; | ||||
|   name: ""; | ||||
|   title: string; | ||||
|   icon?: string; | ||||
|   iconPath?: string; | ||||
|   expanded?: boolean; | ||||
| @@ -100,7 +100,7 @@ export type SchemaUnion< | ||||
|   SchemaArray extends readonly HaFormSchema[], | ||||
|   Schema = SchemaArray[number], | ||||
| > = Schema extends HaFormGridSchema | HaFormExpandableSchema | ||||
|   ? SchemaUnion<Schema["schema"]> | Schema | ||||
|   ? SchemaUnion<Schema["schema"]> | ||||
|   : Schema; | ||||
|  | ||||
| export interface HaFormDataContainer { | ||||
|   | ||||
| @@ -18,9 +18,9 @@ export class HaFormfield extends FormfieldBase { | ||||
|  | ||||
|     return html` <div class="mdc-form-field ${classMap(classes)}"> | ||||
|       <slot></slot> | ||||
|       <label class="mdc-label" @click=${this._labelClick}> | ||||
|         <slot name="label">${this.label}</slot> | ||||
|       </label> | ||||
|       <label class="mdc-label" @click=${this._labelClick} | ||||
|         ><slot name="label">${this.label}</slot></label | ||||
|       > | ||||
|     </div>`; | ||||
|   } | ||||
|  | ||||
| @@ -57,13 +57,13 @@ export class HaFormfield extends FormfieldBase { | ||||
|       } | ||||
|       .mdc-form-field { | ||||
|         align-items: var(--ha-formfield-align-items, center); | ||||
|         gap: 4px; | ||||
|       } | ||||
|       .mdc-form-field > label { | ||||
|         direction: var(--direction); | ||||
|         margin-inline-start: 0; | ||||
|         margin-inline-end: auto; | ||||
|         padding: 0; | ||||
|         padding-inline-start: 4px; | ||||
|         padding-inline-end: 0; | ||||
|       } | ||||
|       :host([disabled]) label { | ||||
|         color: var(--disabled-text-color); | ||||
|   | ||||
| @@ -1,24 +1,24 @@ | ||||
| import { LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider"; | ||||
| import "./ha-icon-button"; | ||||
| import "../panels/lovelace/editor/card-editor/ha-grid-layout-slider"; | ||||
|  | ||||
| import { mdiRestore } from "@mdi/js"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { conditionalClamp } from "../common/number/clamp"; | ||||
| import { | ||||
|   CardGridSize, | ||||
|   DEFAULT_GRID_SIZE, | ||||
| } from "../panels/lovelace/common/compute-card-grid-size"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import { conditionalClamp } from "../common/number/clamp"; | ||||
|  | ||||
| type GridSizeValue = { | ||||
|   rows?: number | "auto"; | ||||
|   columns?: number; | ||||
| }; | ||||
|  | ||||
| @customElement("ha-grid-size-picker") | ||||
| export class HaGridSizeEditor extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public value?: CardGridSize; | ||||
|   @property({ attribute: false }) public value?: GridSizeValue; | ||||
|  | ||||
|   @property({ attribute: false }) public rows = 8; | ||||
|  | ||||
| @@ -34,7 +34,7 @@ export class HaGridSizeEditor extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) public isDefault?: boolean; | ||||
|  | ||||
|   @state() public _localValue?: CardGridSize = { rows: 1, columns: 1 }; | ||||
|   @state() public _localValue?: GridSizeValue = undefined; | ||||
|  | ||||
|   protected willUpdate(changedProperties) { | ||||
|     if (changedProperties.has("value")) { | ||||
| @@ -49,7 +49,6 @@ export class HaGridSizeEditor extends LitElement { | ||||
|       this.rowMin !== undefined && this.rowMin === this.rowMax; | ||||
|  | ||||
|     const autoHeight = this._localValue?.rows === "auto"; | ||||
|     const fullWidth = this._localValue?.columns === "full"; | ||||
|  | ||||
|     const rowMin = this.rowMin ?? 1; | ||||
|     const rowMax = this.rowMax ?? this.rows; | ||||
| @@ -68,7 +67,7 @@ export class HaGridSizeEditor extends LitElement { | ||||
|           .min=${columnMin} | ||||
|           .max=${columnMax} | ||||
|           .range=${this.columns} | ||||
|           .value=${fullWidth ? this.columns : columnValue} | ||||
|           .value=${columnValue} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           @slider-moved=${this._sliderMoved} | ||||
|           .disabled=${disabledColumns} | ||||
| @@ -105,12 +104,12 @@ export class HaGridSizeEditor extends LitElement { | ||||
|             ` | ||||
|           : nothing} | ||||
|         <div | ||||
|           class="preview ${classMap({ "full-width": fullWidth })}" | ||||
|           class="preview" | ||||
|           style=${styleMap({ | ||||
|             "--total-rows": this.rows, | ||||
|             "--total-columns": this.columns, | ||||
|             "--rows": rowValue, | ||||
|             "--columns": fullWidth ? this.columns : columnValue, | ||||
|             "--columns": columnValue, | ||||
|           })} | ||||
|         > | ||||
|           <div> | ||||
| @@ -141,21 +140,12 @@ export class HaGridSizeEditor extends LitElement { | ||||
|     const cell = ev.currentTarget as HTMLElement; | ||||
|     const rows = Number(cell.getAttribute("data-row")); | ||||
|     const columns = Number(cell.getAttribute("data-column")); | ||||
|     const clampedRow: CardGridSize["rows"] = conditionalClamp( | ||||
|       rows, | ||||
|       this.rowMin, | ||||
|       this.rowMax | ||||
|     ); | ||||
|     let clampedColumn: CardGridSize["columns"] = conditionalClamp( | ||||
|     const clampedRow = conditionalClamp(rows, this.rowMin, this.rowMax); | ||||
|     const clampedColumn = conditionalClamp( | ||||
|       columns, | ||||
|       this.columnMin, | ||||
|       this.columnMax | ||||
|     ); | ||||
|  | ||||
|     const currentSize = this.value ?? DEFAULT_GRID_SIZE; | ||||
|     if (currentSize.columns === "full" && clampedColumn === this.columns) { | ||||
|       clampedColumn = "full"; | ||||
|     } | ||||
|     fireEvent(this, "value-changed", { | ||||
|       value: { rows: clampedRow, columns: clampedColumn }, | ||||
|     }); | ||||
| @@ -163,23 +153,12 @@ export class HaGridSizeEditor extends LitElement { | ||||
|  | ||||
|   private _valueChanged(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     const key = ev.currentTarget.id as "rows" | "columns"; | ||||
|     const currentSize = this.value ?? DEFAULT_GRID_SIZE; | ||||
|     let value = ev.detail.value as CardGridSize[typeof key]; | ||||
|  | ||||
|     if ( | ||||
|       key === "columns" && | ||||
|       currentSize.columns === "full" && | ||||
|       value === this.columns | ||||
|     ) { | ||||
|       value = "full"; | ||||
|     } | ||||
|  | ||||
|     const newSize = { | ||||
|       ...currentSize, | ||||
|       [key]: value, | ||||
|     const key = ev.currentTarget.id; | ||||
|     const newValue = { | ||||
|       ...this.value, | ||||
|       [key]: ev.detail.value, | ||||
|     }; | ||||
|     fireEvent(this, "value-changed", { value: newSize }); | ||||
|     fireEvent(this, "value-changed", { value: newValue }); | ||||
|   } | ||||
|  | ||||
|   private _reset(ev) { | ||||
| @@ -194,14 +173,11 @@ export class HaGridSizeEditor extends LitElement { | ||||
|  | ||||
|   private _sliderMoved(ev) { | ||||
|     ev.stopPropagation(); | ||||
|     const key = ev.currentTarget.id as "rows" | "columns"; | ||||
|     const currentSize = this.value ?? DEFAULT_GRID_SIZE; | ||||
|     const value = ev.detail.value as CardGridSize[typeof key] | undefined; | ||||
|  | ||||
|     const key = ev.currentTarget.id; | ||||
|     const value = ev.detail.value; | ||||
|     if (value === undefined) return; | ||||
|  | ||||
|     this._localValue = { | ||||
|       ...currentSize, | ||||
|       ...this.value, | ||||
|       [key]: ev.detail.value, | ||||
|     }; | ||||
|   } | ||||
| @@ -213,7 +189,7 @@ export class HaGridSizeEditor extends LitElement { | ||||
|         grid-template-areas: | ||||
|           "reset column-slider" | ||||
|           "row-slider preview"; | ||||
|         grid-template-rows: auto auto; | ||||
|         grid-template-rows: auto 1fr; | ||||
|         grid-template-columns: auto 1fr; | ||||
|         gap: 8px; | ||||
|       } | ||||
| @@ -229,12 +205,17 @@ export class HaGridSizeEditor extends LitElement { | ||||
|       .preview { | ||||
|         position: relative; | ||||
|         grid-area: preview; | ||||
|         aspect-ratio: 1 / 1.2; | ||||
|       } | ||||
|       .preview > div { | ||||
|         position: relative; | ||||
|         position: absolute; | ||||
|         width: 100%; | ||||
|         height: 100%; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         display: grid; | ||||
|         grid-template-columns: repeat(var(--total-columns), 1fr); | ||||
|         grid-template-rows: repeat(var(--total-rows), 25px); | ||||
|         grid-template-rows: repeat(var(--total-rows), 1fr); | ||||
|         gap: 4px; | ||||
|       } | ||||
|       .preview .cell { | ||||
| @@ -245,23 +226,15 @@ export class HaGridSizeEditor extends LitElement { | ||||
|         opacity: 0.2; | ||||
|         cursor: pointer; | ||||
|       } | ||||
|       .preview .selected { | ||||
|         position: absolute; | ||||
|       .selected { | ||||
|         pointer-events: none; | ||||
|         top: 0; | ||||
|         left: 0; | ||||
|         height: 100%; | ||||
|         width: 100%; | ||||
|       } | ||||
|       .selected .cell { | ||||
|         background-color: var(--primary-color); | ||||
|         grid-column: 1 / span min(var(--columns, 0), var(--total-columns)); | ||||
|         grid-row: 1 / span min(var(--rows, 0), var(--total-rows)); | ||||
|         grid-column: 1 / span var(--columns, 0); | ||||
|         grid-row: 1 / span var(--rows, 0); | ||||
|         opacity: 0.5; | ||||
|       } | ||||
|       .preview.full-width .selected .cell { | ||||
|         grid-column: 1 / -1; | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
| } | ||||
|   | ||||
| @@ -2,8 +2,8 @@ import { MdListItem } from "@material/web/list/list-item"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| 
 | ||||
| @customElement("ha-md-list-item") | ||||
| export class HaMdListItem extends MdListItem { | ||||
| @customElement("ha-list-item-new") | ||||
| export class HaListItemNew extends MdListItem { | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
| @@ -21,6 +21,6 @@ export class HaMdListItem extends MdListItem { | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-md-list-item": HaMdListItem; | ||||
|     "ha-list-item-new": HaListItemNew; | ||||
|   } | ||||
| } | ||||
| @@ -2,8 +2,8 @@ import { MdList } from "@material/web/list/list"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| 
 | ||||
| @customElement("ha-md-list") | ||||
| export class HaMdList extends MdList { | ||||
| @customElement("ha-list-new") | ||||
| export class HaListNew extends MdList { | ||||
|   static override styles = [ | ||||
|     ...super.styles, | ||||
|     css` | ||||
| @@ -16,6 +16,6 @@ export class HaMdList extends MdList { | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-md-list": HaMdList; | ||||
|     "ha-list-new": HaListNew; | ||||
|   } | ||||
| } | ||||
| @@ -96,25 +96,7 @@ class HaMarkdownElement extends ReactiveElement { | ||||
|  | ||||
|           haAlertNode.append( | ||||
|             ...Array.from(node.childNodes) | ||||
|               .map((child) => { | ||||
|                 const arr = Array.from(child.childNodes); | ||||
|                 if (!this.breaks && arr.length) { | ||||
|                   // When we are not breaking, the first line of the blockquote is not considered, | ||||
|                   // so we need to adjust the first child text content | ||||
|                   const firstChild = arr[0]; | ||||
|                   if ( | ||||
|                     firstChild.nodeType === Node.TEXT_NODE && | ||||
|                     firstChild.textContent === gitHubAlertMatch.input && | ||||
|                     firstChild.textContent?.includes("\n") | ||||
|                   ) { | ||||
|                     firstChild.textContent = firstChild.textContent | ||||
|                       .split("\n") | ||||
|                       .slice(1) | ||||
|                       .join("\n"); | ||||
|                   } | ||||
|                 } | ||||
|                 return arr; | ||||
|               }) | ||||
|               .map((child) => Array.from(child.childNodes)) | ||||
|               .reduce((acc, val) => acc.concat(val), []) | ||||
|               .filter( | ||||
|                 (childNode) => | ||||
|   | ||||
| @@ -1,250 +0,0 @@ | ||||
| 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; | ||||
|   } | ||||
| } | ||||
| @@ -1,21 +0,0 @@ | ||||
| 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 { MdMenuItem } from "@material/web/menu/menu-item"; | ||||
| import { css } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| 
 | ||||
| @customElement("ha-md-menu-item") | ||||
| export class HaMdMenuItem extends MdMenuItem { | ||||
| @customElement("ha-menu-item") | ||||
| export class HaMenuItem extends MdMenuItem { | ||||
|   @property({ attribute: false }) clickAction?: (item?: HTMLElement) => void; | ||||
| 
 | ||||
|   static override styles = [ | ||||
| @@ -41,6 +41,6 @@ export class HaMdMenuItem extends MdMenuItem { | ||||
| 
 | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-md-menu-item": HaMdMenuItem; | ||||
|     "ha-menu-item": HaMenuItem; | ||||
|   } | ||||
| } | ||||
| @@ -6,7 +6,7 @@ import { | ||||
| } from "@material/web/menu/internal/controllers/shared"; | ||||
| import { css } from "lit"; | ||||
| import { customElement } from "lit/decorators"; | ||||
| import type { HaMdMenuItem } from "./ha-md-menu-item"; | ||||
| import type { HaMenuItem } from "./ha-menu-item"; | ||||
|  | ||||
| @customElement("ha-menu") | ||||
| export class HaMenu extends MdMenu { | ||||
| @@ -22,7 +22,7 @@ export class HaMenu extends MdMenu { | ||||
|     ) { | ||||
|       return; | ||||
|     } | ||||
|     (ev.detail.initiator as HaMdMenuItem).clickAction?.(ev.detail.initiator); | ||||
|     (ev.detail.initiator as HaMenuItem).clickAction?.(ev.detail.initiator); | ||||
|   } | ||||
|  | ||||
|   static override styles = [ | ||||
|   | ||||
| @@ -24,11 +24,9 @@ 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,7 +1,6 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { Action, migrateAutomationAction } from "../../data/script"; | ||||
| import { Action } from "../../data/script"; | ||||
| import { ActionSelector } from "../../data/selector"; | ||||
| import "../../panels/config/automation/action/ha-automation-action"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| @@ -18,19 +17,12 @@ 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._actions(this.value)} | ||||
|         .actions=${this.value || []} | ||||
|         .hass=${this.hass} | ||||
|         .path=${this.selector.action?.path} | ||||
|       ></ha-automation-action> | ||||
|   | ||||
| @@ -1,15 +1,19 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { css, CSSResultGroup, html, LitElement } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { BooleanSelector } from "../../data/selector"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| import "../ha-checkbox"; | ||||
| import "../ha-formfield"; | ||||
| import "../ha-switch"; | ||||
| import "../ha-input-helper-text"; | ||||
| import "../ha-switch"; | ||||
|  | ||||
| @customElement("ha-selector-boolean") | ||||
| export class HaBooleanSelector extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public selector!: BooleanSelector; | ||||
|  | ||||
|   @property({ type: Boolean }) public value = false; | ||||
|  | ||||
|   @property() public placeholder?: any; | ||||
| @@ -21,20 +25,28 @@ export class HaBooleanSelector extends LitElement { | ||||
|   @property({ type: Boolean }) public disabled = false; | ||||
|  | ||||
|   protected render() { | ||||
|     const checkbox = this.selector.boolean?.mode === "checkbox"; | ||||
|     return html` | ||||
|       <ha-formfield alignEnd spaceBetween .label=${this.label}> | ||||
|       <ha-formfield .alignEnd=${!checkbox} spaceBetween .label=${this.label}> | ||||
|         ${checkbox | ||||
|           ? html` | ||||
|               <ha-checkbox | ||||
|                 .checked=${this.value ?? this.placeholder === true} | ||||
|                 @change=${this._handleChange} | ||||
|                 .disabled=${this.disabled} | ||||
|               ></ha-checkbox> | ||||
|             ` | ||||
|           : html` | ||||
|               <ha-switch | ||||
|                 .checked=${this.value ?? this.placeholder === true} | ||||
|                 @change=${this._handleChange} | ||||
|                 .disabled=${this.disabled} | ||||
|               ></ha-switch> | ||||
|         <span slot="label"> | ||||
|           <p class="primary">${this.label}</p> | ||||
|           ${this.helper | ||||
|             ? html`<p class="secondary">${this.helper}</p>` | ||||
|             : nothing} | ||||
|         </span> | ||||
|             `} | ||||
|       </ha-formfield> | ||||
|       ${this.helper | ||||
|         ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` | ||||
|         : ""} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -50,21 +62,10 @@ export class HaBooleanSelector extends LitElement { | ||||
|     return css` | ||||
|       ha-formfield { | ||||
|         display: flex; | ||||
|         min-height: 56px; | ||||
|         height: 56px; | ||||
|         align-items: center; | ||||
|         --mdc-typography-body2-font-size: 1em; | ||||
|       } | ||||
|       p { | ||||
|         margin: 0; | ||||
|       } | ||||
|       .secondary { | ||||
|         direction: var(--direction); | ||||
|         padding-top: 4px; | ||||
|         box-sizing: border-box; | ||||
|         color: var(--secondary-text-color); | ||||
|         font-size: 0.875rem; | ||||
|         font-weight: var(--mdc-typography-body2-font-weight, 400); | ||||
|       } | ||||
|     `; | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -31,7 +31,7 @@ export class HaColorRGBSelector extends LitElement { | ||||
|         .label=${this.label || ""} | ||||
|         .required=${this.required} | ||||
|         .helper=${this.helper} | ||||
|         .disabled=${this.disabled} | ||||
|         .disalbled=${this.disabled} | ||||
|         @change=${this._valueChanged} | ||||
|       ></ha-textfield> | ||||
|     `; | ||||
|   | ||||
| @@ -162,14 +162,8 @@ export class HaLocationSelector extends LitElement { | ||||
|  | ||||
|   private _computeLabel = ( | ||||
|     entry: SchemaUnion<ReturnType<typeof this._schema>> | ||||
|   ): string => { | ||||
|     if (entry.name) { | ||||
|       return this.hass.localize( | ||||
|         `ui.components.selectors.location.${entry.name}` | ||||
|       ); | ||||
|     } | ||||
|     return ""; | ||||
|   }; | ||||
|   ): string => | ||||
|     this.hass.localize(`ui.components.selectors.location.${entry.name}`); | ||||
|  | ||||
|   static styles = css` | ||||
|     ha-locations-editor { | ||||
|   | ||||
| @@ -1,11 +1,4 @@ | ||||
| import { | ||||
|   css, | ||||
|   CSSResultGroup, | ||||
|   html, | ||||
|   LitElement, | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
| } from "lit"; | ||||
| import { css, CSSResultGroup, html, LitElement, PropertyValues } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| @@ -67,12 +60,12 @@ export class HaNumberSelector extends LitElement { | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       ${this.label && !isBox | ||||
|         ? html`${this.label}${this.required ? "*" : ""}` | ||||
|         : nothing} | ||||
|       <div class="input"> | ||||
|         ${!isBox | ||||
|           ? html` | ||||
|               ${this.label | ||||
|                 ? html`${this.label}${this.required ? "*" : ""}` | ||||
|                 : ""} | ||||
|               <ha-slider | ||||
|                 labeled | ||||
|                 .min=${this.selector.number!.min} | ||||
| @@ -82,11 +75,10 @@ export class HaNumberSelector extends LitElement { | ||||
|                 .disabled=${this.disabled} | ||||
|                 .required=${this.required} | ||||
|                 @change=${this._handleSliderChange} | ||||
|                 .ticks=${this.selector.number?.slider_ticks} | ||||
|               > | ||||
|               </ha-slider> | ||||
|             ` | ||||
|           : nothing} | ||||
|           : ""} | ||||
|         <ha-textfield | ||||
|           .inputMode=${this.selector.number?.step === "any" || | ||||
|           (this.selector.number?.step ?? 1) % 1 !== 0 | ||||
| @@ -113,7 +105,7 @@ export class HaNumberSelector extends LitElement { | ||||
|       </div> | ||||
|       ${!isBox && this.helper | ||||
|         ? html`<ha-input-helper-text>${this.helper}</ha-input-helper-text>` | ||||
|         : nothing} | ||||
|         : ""} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -149,9 +141,6 @@ export class HaNumberSelector extends LitElement { | ||||
|       } | ||||
|       ha-slider { | ||||
|         flex: 1; | ||||
|         margin-right: 16px; | ||||
|         margin-inline-end: 16px; | ||||
|         margin-inline-start: 0; | ||||
|       } | ||||
|       ha-textfield { | ||||
|         --ha-textfield-input-width: 40px; | ||||
|   | ||||
| @@ -7,7 +7,12 @@ import "../ha-code-editor"; | ||||
| import "../ha-input-helper-text"; | ||||
| import "../ha-alert"; | ||||
|  | ||||
| const WARNING_STRINGS = ["template:", "sensor:", "state:", "trigger: template"]; | ||||
| const WARNING_STRINGS = [ | ||||
|   "template:", | ||||
|   "sensor:", | ||||
|   "state:", | ||||
|   "platform: template", | ||||
| ]; | ||||
|  | ||||
| @customElement("ha-selector-template") | ||||
| export class HaTemplateSelector extends LitElement { | ||||
|   | ||||
| @@ -82,7 +82,6 @@ export class HaTextSelector extends LitElement { | ||||
|         .disabled=${this.disabled} | ||||
|         .type=${this._unmaskedPassword ? "text" : this.selector.text?.type} | ||||
|         @input=${this._handleChange} | ||||
|         @change=${this._handleChange} | ||||
|         .label=${this.label || ""} | ||||
|         .prefix=${this.selector.text?.prefix} | ||||
|         .suffix=${this.selector.text?.type === "password" | ||||
|   | ||||
| @@ -30,7 +30,7 @@ export class HaTimeSelector extends LitElement { | ||||
|         clearable | ||||
|         .helper=${this.helper} | ||||
|         .label=${this.label} | ||||
|         .enableSecond=${!this.selector.time?.no_second} | ||||
|         enable-second | ||||
|       ></ha-time-input> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import { css, CSSResultGroup, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { migrateAutomationTrigger, Trigger } from "../../data/automation"; | ||||
| import { Trigger } from "../../data/automation"; | ||||
| import { TriggerSelector } from "../../data/selector"; | ||||
| import "../../panels/config/automation/trigger/ha-automation-trigger"; | ||||
| import { HomeAssistant } from "../../types"; | ||||
| @@ -18,19 +17,12 @@ 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._triggers(this.value)} | ||||
|         .triggers=${this.value || []} | ||||
|         .hass=${this.hass} | ||||
|         .path=${this.selector.trigger?.path} | ||||
|       ></ha-automation-trigger> | ||||
|   | ||||
| @@ -24,8 +24,6 @@ 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> | ||||
|   | ||||
| @@ -44,7 +44,6 @@ import "./ha-service-picker"; | ||||
| import "./ha-settings-row"; | ||||
| import "./ha-yaml-editor"; | ||||
| import type { HaYamlEditor } from "./ha-yaml-editor"; | ||||
| import "./ha-service-section-icon"; | ||||
|  | ||||
| const attributeFilter = (values: any[], attribute: any) => { | ||||
|   if (typeof attribute === "object") { | ||||
| @@ -240,24 +239,12 @@ 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, | ||||
|         hasSelector: fields.length | ||||
|           ? fields.filter((field) => field.selector).map((field) => field.key) | ||||
|           : [], | ||||
|       }; | ||||
|     } | ||||
|   ); | ||||
| @@ -509,18 +496,12 @@ export class HaServiceControl extends LitElement { | ||||
|                 ) || | ||||
|                 dataField.name || | ||||
|                 dataField.key} | ||||
|                 .secondary=${this._getSectionDescription( | ||||
|               > | ||||
|                 ${this._renderSectionDescription( | ||||
|                   dataField, | ||||
|                   domain, | ||||
|                   serviceName | ||||
|                 )} | ||||
|               > | ||||
|                 <ha-service-section-icon | ||||
|                   slot="icons" | ||||
|                   .hass=${this.hass} | ||||
|                   .service=${this._value!.action} | ||||
|                   .section=${dataField.key} | ||||
|                 ></ha-service-section-icon> | ||||
|                 ${Object.entries(dataField.fields).map(([key, field]) => | ||||
|                   this._renderField( | ||||
|                     { key, ...field }, | ||||
| @@ -541,14 +522,20 @@ export class HaServiceControl extends LitElement { | ||||
|         )} `; | ||||
|   } | ||||
|  | ||||
|   private _getSectionDescription( | ||||
|   private _renderSectionDescription( | ||||
|     dataField: ExtHassService["fields"][number], | ||||
|     domain: string | undefined, | ||||
|     serviceName: string | undefined | ||||
|   ) { | ||||
|     return this.hass!.localize( | ||||
|     const description = this.hass!.localize( | ||||
|       `component.${domain}.services.${serviceName}.sections.${dataField.key}.description` | ||||
|     ); | ||||
|  | ||||
|     if (!description) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     return html`<p>${description}</p>`; | ||||
|   } | ||||
|  | ||||
|   private _renderField = ( | ||||
|   | ||||
| @@ -1,53 +0,0 @@ | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import { until } from "lit/directives/until"; | ||||
| import { HomeAssistant } from "../types"; | ||||
| import "./ha-icon"; | ||||
| import "./ha-svg-icon"; | ||||
| import { serviceSectionIcon } from "../data/icons"; | ||||
|  | ||||
| @customElement("ha-service-section-icon") | ||||
| export class HaServiceSectionIcon extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public service?: string; | ||||
|  | ||||
|   @property() public section?: string; | ||||
|  | ||||
|   @property() public icon?: string; | ||||
|  | ||||
|   protected render() { | ||||
|     if (this.icon) { | ||||
|       return html`<ha-icon .icon=${this.icon}></ha-icon>`; | ||||
|     } | ||||
|  | ||||
|     if (!this.service || !this.section) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     if (!this.hass) { | ||||
|       return this._renderFallback(); | ||||
|     } | ||||
|  | ||||
|     const icon = serviceSectionIcon(this.hass, this.service, this.section).then( | ||||
|       (icn) => { | ||||
|         if (icn) { | ||||
|           return html`<ha-icon .icon=${icn}></ha-icon>`; | ||||
|         } | ||||
|         return this._renderFallback(); | ||||
|       } | ||||
|     ); | ||||
|  | ||||
|     return html`${until(icon)}`; | ||||
|   } | ||||
|  | ||||
|   private _renderFallback() { | ||||
|     return nothing; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-service-section-icon": HaServiceSectionIcon; | ||||
|   } | ||||
| } | ||||
| @@ -20,7 +20,6 @@ export class HaSlider extends MdSlider { | ||||
|         --md-sys-color-on-surface: var(--primary-text-color); | ||||
|         --md-slider-handle-width: 14px; | ||||
|         --md-slider-handle-height: 14px; | ||||
|         --md-slider-state-layer-size: 24px; | ||||
|         min-width: 100px; | ||||
|         min-inline-size: 100px; | ||||
|         width: 200px; | ||||
|   | ||||
| @@ -43,13 +43,6 @@ 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; | ||||
|  | ||||
| @@ -152,9 +145,6 @@ 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); | ||||
|   } | ||||
|   | ||||
| @@ -16,10 +16,11 @@ import { HomeAssistant } from "../types"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
| import { computeDomain } from "../common/entity/compute_domain"; | ||||
|  | ||||
| const NONE = "__NONE_OPTION__"; | ||||
|  | ||||
| const NAME_MAP = { cloud: "Home Assistant Cloud" }; | ||||
|  | ||||
| @customElement("ha-stt-picker") | ||||
| export class HaSTTPicker extends LitElement { | ||||
|   @property() public value?: string; | ||||
| @@ -40,32 +41,13 @@ export class HaSTTPicker extends LitElement { | ||||
|     if (!this._engines) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     let value = this.value; | ||||
|     if (!value && this.required) { | ||||
|       for (const entity of Object.values(this.hass.entities)) { | ||||
|         if ( | ||||
|           entity.platform === "cloud" && | ||||
|           computeDomain(entity.entity_id) === "stt" | ||||
|         ) { | ||||
|           value = entity.entity_id; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (!value) { | ||||
|         for (const sttEngine of this._engines) { | ||||
|           if (sttEngine?.supported_languages?.length !== 0) { | ||||
|             value = sttEngine.engine_id; | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (!value) { | ||||
|       value = NONE; | ||||
|     } | ||||
|  | ||||
|     const value = | ||||
|       this.value ?? | ||||
|       (this.required | ||||
|         ? this._engines.find( | ||||
|             (engine) => engine.supported_languages?.length !== 0 | ||||
|           ) | ||||
|         : NONE); | ||||
|     return html` | ||||
|       <ha-select | ||||
|         .label=${this.label || | ||||
| @@ -84,15 +66,12 @@ export class HaSTTPicker extends LitElement { | ||||
|             </ha-list-item>` | ||||
|           : nothing} | ||||
|         ${this._engines.map((engine) => { | ||||
|           if (engine.deprecated && engine.engine_id !== value) { | ||||
|             return nothing; | ||||
|           } | ||||
|           let label: string; | ||||
|           let label = engine.engine_id; | ||||
|           if (engine.engine_id.includes(".")) { | ||||
|             const stateObj = this.hass!.states[engine.engine_id]; | ||||
|             label = stateObj ? computeStateName(stateObj) : engine.engine_id; | ||||
|           } else { | ||||
|             label = engine.name || engine.engine_id; | ||||
|           } else if (engine.engine_id in NAME_MAP) { | ||||
|             label = NAME_MAP[engine.engine_id]; | ||||
|           } | ||||
|           return html`<ha-list-item | ||||
|             .value=${engine.engine_id} | ||||
|   | ||||
| @@ -35,6 +35,10 @@ import { | ||||
|   computeDeviceName, | ||||
| } from "../data/device_registry"; | ||||
| import { EntityRegistryDisplayEntry } from "../data/entity_registry"; | ||||
| import { | ||||
|   FloorRegistryEntry, | ||||
|   subscribeFloorRegistry, | ||||
| } from "../data/floor_registry"; | ||||
| import { | ||||
|   LabelRegistryEntry, | ||||
|   subscribeLabelRegistry, | ||||
| @@ -99,12 +103,17 @@ 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; | ||||
|       }), | ||||
| @@ -123,7 +132,9 @@ 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.hass.floors[floor_id]; | ||||
|               const floor = this._floors?.find( | ||||
|                 (flr) => flr.floor_id === floor_id | ||||
|               ); | ||||
|               return this._renderChip( | ||||
|                 "floor_id", | ||||
|                 floor_id, | ||||
|   | ||||
| @@ -109,7 +109,7 @@ export class HaTextField extends TextFieldBase { | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|       .mdc-text-field:not(.mdc-text-field--disabled) .mdc-text-field__icon { | ||||
|       .mdc-text-field__icon { | ||||
|         color: var(--secondary-text-color); | ||||
|       } | ||||
|  | ||||
|   | ||||
| @@ -16,10 +16,14 @@ import { HomeAssistant } from "../types"; | ||||
| import "./ha-list-item"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
| import { computeDomain } from "../common/entity/compute_domain"; | ||||
|  | ||||
| const NONE = "__NONE_OPTION__"; | ||||
|  | ||||
| const NAME_MAP = { | ||||
|   cloud: "Home Assistant Cloud", | ||||
|   google_translate: "Google Translate", | ||||
| }; | ||||
|  | ||||
| @customElement("ha-tts-picker") | ||||
| export class HaTTSPicker extends LitElement { | ||||
|   @property() public value?: string; | ||||
| @@ -40,32 +44,13 @@ export class HaTTSPicker extends LitElement { | ||||
|     if (!this._engines) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     let value = this.value; | ||||
|     if (!value && this.required) { | ||||
|       for (const entity of Object.values(this.hass.entities)) { | ||||
|         if ( | ||||
|           entity.platform === "cloud" && | ||||
|           computeDomain(entity.entity_id) === "tts" | ||||
|         ) { | ||||
|           value = entity.entity_id; | ||||
|           break; | ||||
|         } | ||||
|       } | ||||
|  | ||||
|       if (!value) { | ||||
|         for (const ttsEngine of this._engines) { | ||||
|           if (ttsEngine?.supported_languages?.length !== 0) { | ||||
|             value = ttsEngine.engine_id; | ||||
|             break; | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|     } | ||||
|     if (!value) { | ||||
|       value = NONE; | ||||
|     } | ||||
|  | ||||
|     const value = | ||||
|       this.value ?? | ||||
|       (this.required | ||||
|         ? this._engines.find( | ||||
|             (engine) => engine.supported_languages?.length !== 0 | ||||
|           ) | ||||
|         : NONE); | ||||
|     return html` | ||||
|       <ha-select | ||||
|         .label=${this.label || | ||||
| @@ -84,15 +69,12 @@ export class HaTTSPicker extends LitElement { | ||||
|             </ha-list-item>` | ||||
|           : nothing} | ||||
|         ${this._engines.map((engine) => { | ||||
|           if (engine.deprecated && engine.engine_id !== value) { | ||||
|             return nothing; | ||||
|           } | ||||
|           let label: string; | ||||
|           let label = engine.engine_id; | ||||
|           if (engine.engine_id.includes(".")) { | ||||
|             const stateObj = this.hass!.states[engine.engine_id]; | ||||
|             label = stateObj ? computeStateName(stateObj) : engine.engine_id; | ||||
|           } else { | ||||
|             label = engine.name || engine.engine_id; | ||||
|           } else if (engine.engine_id in NAME_MAP) { | ||||
|             label = NAME_MAP[engine.engine_id]; | ||||
|           } | ||||
|           return html`<ha-list-item | ||||
|             .value=${engine.engine_id} | ||||
|   | ||||
| @@ -109,7 +109,7 @@ class HaWebRtcPlayer extends LitElement { | ||||
|     let candidates = ""; // Build an Offer SDP string with ice candidates | ||||
|     const iceResolver = new Promise<void>((resolve) => { | ||||
|       peerConnection.addEventListener("icecandidate", async (event) => { | ||||
|         if (!event.candidate?.candidate) { | ||||
|         if (!event.candidate) { | ||||
|           resolve(); // Gathering complete | ||||
|           return; | ||||
|         } | ||||
|   | ||||
| @@ -7,15 +7,13 @@ import { | ||||
|   nothing, | ||||
|   PropertyValues, | ||||
| } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { customElement, property, 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") { | ||||
| @@ -55,8 +53,6 @@ export class HaYamlEditor extends LitElement { | ||||
|  | ||||
|   @state() private _yaml = ""; | ||||
|  | ||||
|   @query("ha-code-editor") _codeEditor?: HaCodeEditor; | ||||
|  | ||||
|   public setValue(value): void { | ||||
|     try { | ||||
|       this._yaml = | ||||
| @@ -87,12 +83,6 @@ export class HaYamlEditor extends LitElement { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public focus(): void { | ||||
|     if (this._codeEditor?.codemirror) { | ||||
|       this._codeEditor?.codemirror.focus(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (this._yaml === undefined) { | ||||
|       return nothing; | ||||
| @@ -100,7 +90,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} | ||||
| @@ -113,20 +103,16 @@ export class HaYamlEditor extends LitElement { | ||||
|         dir="ltr" | ||||
|       ></ha-code-editor> | ||||
|       ${this.copyClipboard || this.hasExtraActions | ||||
|         ? html` | ||||
|             <div class="card-actions"> | ||||
|         ? html`<div class="card-actions"> | ||||
|             ${this.copyClipboard | ||||
|                 ? html` | ||||
|                     <ha-button @click=${this._copyYaml}> | ||||
|               ? html` <mwc-button @click=${this._copyYaml}> | ||||
|                   ${this.hass.localize( | ||||
|                     "ui.components.yaml-editor.copy_to_clipboard" | ||||
|                   )} | ||||
|                     </ha-button> | ||||
|                   ` | ||||
|                 </mwc-button>` | ||||
|               : nothing} | ||||
|             <slot name="extra-actions"></slot> | ||||
|             </div> | ||||
|           ` | ||||
|           </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, flattenTriggers } from "../../data/automation"; | ||||
| import { Condition, Trigger } from "../../data/automation"; | ||||
| import { | ||||
|   Action, | ||||
|   ChooseAction, | ||||
| @@ -569,16 +569,11 @@ 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 = | ||||
|       triggerKey in this.trace.config | ||||
|         ? flattenTriggers(ensureArray(this.trace.config[triggerKey])).map( | ||||
|             (trigger, i) => this.render_trigger(trigger, i) | ||||
|       "trigger" in this.trace.config | ||||
|         ? ensureArray(this.trace.config.trigger).map((trigger, i) => | ||||
|             this.render_trigger(trigger, i) | ||||
|           ) | ||||
|         : undefined; | ||||
|     try { | ||||
| @@ -589,14 +584,14 @@ export class HatScriptGraph extends LitElement { | ||||
|                 ${trigger_nodes} | ||||
|               </hat-graph-branch>` | ||||
|             : ""} | ||||
|           ${conditionKey in this.trace.config | ||||
|             ? html`${ensureArray(this.trace.config[conditionKey])?.map( | ||||
|           ${"condition" in this.trace.config | ||||
|             ? html`${ensureArray(this.trace.config.condition)?.map( | ||||
|                 (condition, i) => this.render_condition(condition, i) | ||||
|               )}` | ||||
|             : ""} | ||||
|           ${actionKey in this.trace.config | ||||
|             ? html`${ensureArray(this.trace.config[actionKey]).map( | ||||
|                 (action, i) => this.render_action_node(action, `action/${i}`) | ||||
|           ${"action" in this.trace.config | ||||
|             ? html`${ensureArray(this.trace.config.action).map((action, i) => | ||||
|                 this.render_action_node(action, `action/${i}`) | ||||
|               )}` | ||||
|             : ""} | ||||
|           ${"sequence" in this.trace.config | ||||
|   | ||||
| @@ -1,81 +0,0 @@ | ||||
| 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,7 +3,6 @@ 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"; | ||||
| @@ -27,14 +26,8 @@ export interface ManualAutomationConfig { | ||||
|   id?: string; | ||||
|   alias?: string; | ||||
|   description?: string; | ||||
|   triggers: Trigger | Trigger[]; | ||||
|   /** @deprecated Use `triggers` instead */ | ||||
|   trigger?: Trigger | Trigger[]; | ||||
|   conditions?: Condition | Condition[]; | ||||
|   /** @deprecated Use `conditions` instead */ | ||||
|   trigger: Trigger | Trigger[]; | ||||
|   condition?: Condition | Condition[]; | ||||
|   actions: Action | Action[]; | ||||
|   /** @deprecated Use `actions` instead */ | ||||
|   action?: Action | Action[]; | ||||
|   mode?: (typeof MODES)[number]; | ||||
|   max?: number; | ||||
| @@ -69,22 +62,16 @@ export interface ContextConstraint { | ||||
|   user_id?: string | string[]; | ||||
| } | ||||
|  | ||||
| export interface TriggerList { | ||||
|   triggers: Trigger | Trigger[] | undefined; | ||||
| } | ||||
|  | ||||
| export interface BaseTrigger { | ||||
|   alias?: string; | ||||
|   /** @deprecated Use `trigger` instead */ | ||||
|   platform?: string; | ||||
|   trigger: string; | ||||
|   platform: string; | ||||
|   id?: string; | ||||
|   variables?: Record<string, unknown>; | ||||
|   enabled?: boolean; | ||||
| } | ||||
|  | ||||
| export interface StateTrigger extends BaseTrigger { | ||||
|   trigger: "state"; | ||||
|   platform: "state"; | ||||
|   entity_id: string | string[]; | ||||
|   attribute?: string; | ||||
|   from?: string | string[]; | ||||
| @@ -93,25 +80,25 @@ export interface StateTrigger extends BaseTrigger { | ||||
| } | ||||
|  | ||||
| export interface MqttTrigger extends BaseTrigger { | ||||
|   trigger: "mqtt"; | ||||
|   platform: "mqtt"; | ||||
|   topic: string; | ||||
|   payload?: string; | ||||
| } | ||||
|  | ||||
| export interface GeoLocationTrigger extends BaseTrigger { | ||||
|   trigger: "geo_location"; | ||||
|   platform: "geo_location"; | ||||
|   source: string; | ||||
|   zone: string; | ||||
|   event: "enter" | "leave"; | ||||
| } | ||||
|  | ||||
| export interface HassTrigger extends BaseTrigger { | ||||
|   trigger: "homeassistant"; | ||||
|   platform: "homeassistant"; | ||||
|   event: "start" | "shutdown"; | ||||
| } | ||||
|  | ||||
| export interface NumericStateTrigger extends BaseTrigger { | ||||
|   trigger: "numeric_state"; | ||||
|   platform: "numeric_state"; | ||||
|   entity_id: string | string[]; | ||||
|   attribute?: string; | ||||
|   above?: number; | ||||
| @@ -121,69 +108,69 @@ export interface NumericStateTrigger extends BaseTrigger { | ||||
| } | ||||
|  | ||||
| export interface ConversationTrigger extends BaseTrigger { | ||||
|   trigger: "conversation"; | ||||
|   platform: "conversation"; | ||||
|   command: string | string[]; | ||||
| } | ||||
|  | ||||
| export interface SunTrigger extends BaseTrigger { | ||||
|   trigger: "sun"; | ||||
|   platform: "sun"; | ||||
|   offset: number; | ||||
|   event: "sunrise" | "sunset"; | ||||
| } | ||||
|  | ||||
| export interface TimePatternTrigger extends BaseTrigger { | ||||
|   trigger: "time_pattern"; | ||||
|   platform: "time_pattern"; | ||||
|   hours?: number | string; | ||||
|   minutes?: number | string; | ||||
|   seconds?: number | string; | ||||
| } | ||||
|  | ||||
| export interface WebhookTrigger extends BaseTrigger { | ||||
|   trigger: "webhook"; | ||||
|   platform: "webhook"; | ||||
|   webhook_id: string; | ||||
|   allowed_methods?: string[]; | ||||
|   local_only?: boolean; | ||||
| } | ||||
|  | ||||
| export interface PersistentNotificationTrigger extends BaseTrigger { | ||||
|   trigger: "persistent_notification"; | ||||
|   platform: "persistent_notification"; | ||||
|   notification_id?: string; | ||||
|   update_type?: string[]; | ||||
| } | ||||
|  | ||||
| export interface ZoneTrigger extends BaseTrigger { | ||||
|   trigger: "zone"; | ||||
|   platform: "zone"; | ||||
|   entity_id: string; | ||||
|   zone: string; | ||||
|   event: "enter" | "leave"; | ||||
| } | ||||
|  | ||||
| export interface TagTrigger extends BaseTrigger { | ||||
|   trigger: "tag"; | ||||
|   platform: "tag"; | ||||
|   tag_id: string; | ||||
|   device_id?: string; | ||||
| } | ||||
|  | ||||
| export interface TimeTrigger extends BaseTrigger { | ||||
|   trigger: "time"; | ||||
|   platform: "time"; | ||||
|   at: string; | ||||
| } | ||||
|  | ||||
| export interface TemplateTrigger extends BaseTrigger { | ||||
|   trigger: "template"; | ||||
|   platform: "template"; | ||||
|   value_template: string; | ||||
|   for?: string | number | ForDict; | ||||
| } | ||||
|  | ||||
| export interface EventTrigger extends BaseTrigger { | ||||
|   trigger: "event"; | ||||
|   platform: "event"; | ||||
|   event_type: string; | ||||
|   event_data?: any; | ||||
|   context?: ContextConstraint; | ||||
| } | ||||
|  | ||||
| export interface CalendarTrigger extends BaseTrigger { | ||||
|   trigger: "calendar"; | ||||
|   platform: "calendar"; | ||||
|   event: "start" | "end"; | ||||
|   entity_id: string; | ||||
|   offset: string; | ||||
| @@ -370,93 +357,22 @@ 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 ["triggers", "conditions", "actions"]) { | ||||
|   for (const key of ["trigger", "condition", "action"]) { | ||||
|     const value = config[key]; | ||||
|     if (value && !Array.isArray(value)) { | ||||
|       config[key] = [value]; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   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); | ||||
|   if (config.action) { | ||||
|     config.action = migrateAutomationAction(config.action); | ||||
|   } | ||||
|  | ||||
|   return config; | ||||
| }; | ||||
|  | ||||
| export const migrateAutomationTrigger = ( | ||||
|   trigger: Trigger | Trigger[] | ||||
| ): Trigger | Trigger[] => { | ||||
|   if (Array.isArray(trigger)) { | ||||
|     return trigger.map(migrateAutomationTrigger) as Trigger[]; | ||||
|   } | ||||
|  | ||||
|   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 | TriggerList)[] | ||||
| ): Trigger[] => { | ||||
|   if (!triggers) { | ||||
|     return []; | ||||
|   } | ||||
|  | ||||
|   const flatTriggers: Trigger[] = []; | ||||
|  | ||||
|   ensureArray(triggers).forEach((t) => { | ||||
|     if ("triggers" in t) { | ||||
|       if (t.triggers) { | ||||
|         flatTriggers.push(...ensureArray(t.triggers)); | ||||
|       } | ||||
|     } else { | ||||
|       flatTriggers.push(t); | ||||
|     } | ||||
|   }); | ||||
|   return flatTriggers; | ||||
| }; | ||||
|  | ||||
| export const showAutomationEditor = (data?: Partial<AutomationConfig>) => { | ||||
|   initialAutomationEditorData = data; | ||||
|   navigate("/config/automation/edit/new"); | ||||
|   | ||||
| @@ -68,18 +68,9 @@ export const describeTrigger = ( | ||||
|   hass: HomeAssistant, | ||||
|   entityRegistry: EntityRegistryEntry[], | ||||
|   ignoreAlias = false | ||||
| ): string => { | ||||
| ) => { | ||||
|   try { | ||||
|     const description = tryDescribeTrigger( | ||||
|       trigger, | ||||
|       hass, | ||||
|       entityRegistry, | ||||
|       ignoreAlias | ||||
|     ); | ||||
|     if (typeof description !== "string") { | ||||
|       throw new Error(String(description)); | ||||
|     } | ||||
|     return description; | ||||
|     return tryDescribeTrigger(trigger, hass, entityRegistry, ignoreAlias); | ||||
|   } catch (error: any) { | ||||
|     // eslint-disable-next-line no-console | ||||
|     console.error(error); | ||||
| @@ -103,7 +94,7 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Event Trigger | ||||
|   if (trigger.trigger === "event" && trigger.event_type) { | ||||
|   if (trigger.platform === "event" && trigger.event_type) { | ||||
|     const eventTypes: string[] = []; | ||||
|  | ||||
|     if (Array.isArray(trigger.event_type)) { | ||||
| @@ -122,7 +113,7 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Home Assistant Trigger | ||||
|   if (trigger.trigger === "homeassistant" && trigger.event) { | ||||
|   if (trigger.platform === "homeassistant" && trigger.event) { | ||||
|     return hass.localize( | ||||
|       trigger.event === "start" | ||||
|         ? `${triggerTranslationBaseKey}.homeassistant.description.started` | ||||
| @@ -131,7 +122,7 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Numeric State Trigger | ||||
|   if (trigger.trigger === "numeric_state" && trigger.entity_id) { | ||||
|   if (trigger.platform === "numeric_state" && trigger.entity_id) { | ||||
|     const entities: string[] = []; | ||||
|     const states = hass.states; | ||||
|  | ||||
| @@ -206,7 +197,7 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // State Trigger | ||||
|   if (trigger.trigger === "state") { | ||||
|   if (trigger.platform === "state") { | ||||
|     const entities: string[] = []; | ||||
|     const states = hass.states; | ||||
|  | ||||
| @@ -329,7 +320,7 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Sun Trigger | ||||
|   if (trigger.trigger === "sun" && trigger.event) { | ||||
|   if (trigger.platform === "sun" && trigger.event) { | ||||
|     let duration = ""; | ||||
|     if (trigger.offset) { | ||||
|       if (typeof trigger.offset === "number") { | ||||
| @@ -350,12 +341,12 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Tag Trigger | ||||
|   if (trigger.trigger === "tag") { | ||||
|   if (trigger.platform === "tag") { | ||||
|     return hass.localize(`${triggerTranslationBaseKey}.tag.description.full`); | ||||
|   } | ||||
|  | ||||
|   // Time Trigger | ||||
|   if (trigger.trigger === "time" && trigger.at) { | ||||
|   if (trigger.platform === "time" && trigger.at) { | ||||
|     const result = ensureArray(trigger.at).map((at) => | ||||
|       typeof at !== "string" | ||||
|         ? at | ||||
| @@ -370,7 +361,7 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Time Pattern Trigger | ||||
|   if (trigger.trigger === "time_pattern") { | ||||
|   if (trigger.platform === "time_pattern") { | ||||
|     if (!trigger.seconds && !trigger.minutes && !trigger.hours) { | ||||
|       return hass.localize( | ||||
|         `${triggerTranslationBaseKey}.time_pattern.description.initial` | ||||
| @@ -547,7 +538,7 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Zone Trigger | ||||
|   if (trigger.trigger === "zone" && trigger.entity_id && trigger.zone) { | ||||
|   if (trigger.platform === "zone" && trigger.entity_id && trigger.zone) { | ||||
|     const entities: string[] = []; | ||||
|     const zones: string[] = []; | ||||
|  | ||||
| @@ -590,7 +581,7 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Geo Location Trigger | ||||
|   if (trigger.trigger === "geo_location" && trigger.source && trigger.zone) { | ||||
|   if (trigger.platform === "geo_location" && trigger.source && trigger.zone) { | ||||
|     const sources: string[] = []; | ||||
|     const zones: string[] = []; | ||||
|     const states = hass.states; | ||||
| @@ -629,12 +620,12 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // MQTT Trigger | ||||
|   if (trigger.trigger === "mqtt") { | ||||
|   if (trigger.platform === "mqtt") { | ||||
|     return hass.localize(`${triggerTranslationBaseKey}.mqtt.description.full`); | ||||
|   } | ||||
|  | ||||
|   // Template Trigger | ||||
|   if (trigger.trigger === "template") { | ||||
|   if (trigger.platform === "template") { | ||||
|     let duration = ""; | ||||
|     if (trigger.for) { | ||||
|       duration = describeDuration(hass.locale, trigger.for) ?? ""; | ||||
| @@ -647,14 +638,14 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Webhook Trigger | ||||
|   if (trigger.trigger === "webhook") { | ||||
|   if (trigger.platform === "webhook") { | ||||
|     return hass.localize( | ||||
|       `${triggerTranslationBaseKey}.webhook.description.full` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Conversation Trigger | ||||
|   if (trigger.trigger === "conversation") { | ||||
|   if (trigger.platform === "conversation") { | ||||
|     if (!trigger.command) { | ||||
|       return hass.localize( | ||||
|         `${triggerTranslationBaseKey}.conversation.description.empty` | ||||
| @@ -673,14 +664,14 @@ const tryDescribeTrigger = ( | ||||
|   } | ||||
|  | ||||
|   // Persistent Notification Trigger | ||||
|   if (trigger.trigger === "persistent_notification") { | ||||
|   if (trigger.platform === "persistent_notification") { | ||||
|     return hass.localize( | ||||
|       `${triggerTranslationBaseKey}.persistent_notification.description.full` | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   // Device Trigger | ||||
|   if (trigger.trigger === "device" && trigger.device_id) { | ||||
|   if (trigger.platform === "device" && trigger.device_id) { | ||||
|     const config = trigger as DeviceTrigger; | ||||
|     const localized = localizeDeviceAutomationTrigger( | ||||
|       hass, | ||||
| @@ -698,7 +689,7 @@ const tryDescribeTrigger = ( | ||||
|  | ||||
|   return ( | ||||
|     hass.localize( | ||||
|       `ui.panel.config.automation.editor.triggers.type.${trigger.trigger}.label` | ||||
|       `ui.panel.config.automation.editor.triggers.type.${trigger.platform}.label` | ||||
|     ) || | ||||
|     hass.localize(`ui.panel.config.automation.editor.triggers.unknown_trigger`) | ||||
|   ); | ||||
| @@ -709,18 +700,9 @@ export const describeCondition = ( | ||||
|   hass: HomeAssistant, | ||||
|   entityRegistry: EntityRegistryEntry[], | ||||
|   ignoreAlias = false | ||||
| ): string => { | ||||
| ) => { | ||||
|   try { | ||||
|     const description = tryDescribeCondition( | ||||
|       condition, | ||||
|       hass, | ||||
|       entityRegistry, | ||||
|       ignoreAlias | ||||
|     ); | ||||
|     if (typeof description !== "string") { | ||||
|       throw new Error(String(description)); | ||||
|     } | ||||
|     return description; | ||||
|     return tryDescribeCondition(condition, hass, entityRegistry, ignoreAlias); | ||||
|   } catch (error: any) { | ||||
|     // eslint-disable-next-line no-console | ||||
|     console.error(error); | ||||
| @@ -907,14 +889,8 @@ const tryDescribeCondition = ( | ||||
|  | ||||
|   // Numeric State Condition | ||||
|   if (condition.condition === "numeric_state" && 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 stateObj = hass.states[condition.entity_id]; | ||||
|     const entity = stateObj ? computeStateName(stateObj) : condition.entity_id; | ||||
|  | ||||
|     const attribute = condition.attribute | ||||
|       ? computeAttributeNameDisplay( | ||||
| @@ -929,9 +905,8 @@ const tryDescribeCondition = ( | ||||
|       return hass.localize( | ||||
|         `${conditionsTranslationBaseKey}.numeric_state.description.above-below`, | ||||
|         { | ||||
|           attribute, | ||||
|           entity, | ||||
|           numberOfEntities: entity_ids.length, | ||||
|           attribute: attribute, | ||||
|           entity: entity, | ||||
|           above: condition.above, | ||||
|           below: condition.below, | ||||
|         } | ||||
| @@ -941,9 +916,8 @@ const tryDescribeCondition = ( | ||||
|       return hass.localize( | ||||
|         `${conditionsTranslationBaseKey}.numeric_state.description.above`, | ||||
|         { | ||||
|           attribute, | ||||
|           entity, | ||||
|           numberOfEntities: entity_ids.length, | ||||
|           attribute: attribute, | ||||
|           entity: entity, | ||||
|           above: condition.above, | ||||
|         } | ||||
|       ); | ||||
| @@ -952,9 +926,8 @@ const tryDescribeCondition = ( | ||||
|       return hass.localize( | ||||
|         `${conditionsTranslationBaseKey}.numeric_state.description.below`, | ||||
|         { | ||||
|           attribute, | ||||
|           entity, | ||||
|           numberOfEntities: entity_ids.length, | ||||
|           attribute: attribute, | ||||
|           entity: entity, | ||||
|           below: condition.below, | ||||
|         } | ||||
|       ); | ||||
|   | ||||
| @@ -10,7 +10,7 @@ interface InvalidConfig { | ||||
|   error: string; | ||||
| } | ||||
|  | ||||
| type ValidKeys = "triggers" | "actions" | "conditions"; | ||||
| type ValidKeys = "trigger" | "action" | "condition"; | ||||
|  | ||||
| export const validateConfig = < | ||||
|   T extends Partial<{ [key in ValidKeys]: unknown }>, | ||||
|   | ||||
| @@ -1,6 +1,6 @@ | ||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import type { IntegrationType } from "./integration"; | ||||
| import type { IntegrationManifest, IntegrationType } from "./integration"; | ||||
|  | ||||
| export interface ConfigEntry { | ||||
|   entry_id: string; | ||||
| @@ -149,19 +149,20 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) => | ||||
|  | ||||
| export const sortConfigEntries = ( | ||||
|   configEntries: ConfigEntry[], | ||||
|   primaryConfigEntry: string | null | ||||
|   manifestLookup: { [domain: string]: IntegrationManifest } | ||||
| ): ConfigEntry[] => { | ||||
|   if (!primaryConfigEntry) { | ||||
|     return configEntries; | ||||
|   } | ||||
|   const primaryEntry = configEntries.find( | ||||
|     (e) => e.entry_id === primaryConfigEntry | ||||
|   ); | ||||
|   if (!primaryEntry) { | ||||
|     return configEntries; | ||||
|   } | ||||
|   const otherEntries = configEntries.filter( | ||||
|     (e) => e.entry_id !== primaryConfigEntry | ||||
|   ); | ||||
|   return [primaryEntry, ...otherEntries]; | ||||
|   const sortedConfigEntries = [...configEntries]; | ||||
|  | ||||
|   const getScore = (entry: ConfigEntry) => { | ||||
|     const manifest = manifestLookup[entry.domain] as | ||||
|       | IntegrationManifest | ||||
|       | undefined; | ||||
|     const isHelper = manifest?.integration_type === "helper"; | ||||
|     return isHelper ? -1 : 1; | ||||
|   }; | ||||
|  | ||||
|   const configEntriesCompare = (a: ConfigEntry, b: ConfigEntry) => | ||||
|     getScore(b) - getScore(a); | ||||
|  | ||||
|   return sortedConfigEntries.sort(configEntriesCompare); | ||||
| }; | ||||
|   | ||||
| @@ -1,20 +1,10 @@ | ||||
| export interface DataTableFilters { | ||||
|   [key: string]: { | ||||
|     value: DataTableFiltersValue; | ||||
|     value: string[] | { key: string[] } | undefined; | ||||
|     items: Set<string> | undefined; | ||||
|   }; | ||||
| } | ||||
|  | ||||
| export type DataTableFiltersValue = string[] | { key: string[] } | undefined; | ||||
|  | ||||
| export interface DataTableFiltersValues { | ||||
|   [key: string]: DataTableFiltersValue; | ||||
| } | ||||
|  | ||||
| export interface DataTableFiltersItems { | ||||
|   [key: string]: Set<string> | undefined; | ||||
| } | ||||
|  | ||||
| export const serializeFilters = (value: DataTableFilters) => { | ||||
|   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, migrateAutomationTrigger } from "./automation"; | ||||
| import { BaseTrigger } from "./automation"; | ||||
| import { | ||||
|   computeEntityRegistryName, | ||||
|   entityRegistryByEntityId, | ||||
| @@ -31,7 +31,7 @@ export interface DeviceCondition extends DeviceAutomation { | ||||
|  | ||||
| export type DeviceTrigger = DeviceAutomation & | ||||
|   BaseTrigger & { | ||||
|     trigger: "device"; | ||||
|     platform: "device"; | ||||
|   }; | ||||
|  | ||||
| export interface DeviceCapabilities { | ||||
| @@ -51,12 +51,10 @@ export const fetchDeviceConditions = (hass: HomeAssistant, deviceId: string) => | ||||
|   }); | ||||
|  | ||||
| export const fetchDeviceTriggers = (hass: HomeAssistant, deviceId: string) => | ||||
|   hass | ||||
|     .callWS<DeviceTrigger[]>({ | ||||
|   hass.callWS<DeviceTrigger[]>({ | ||||
|     type: "device_automation/trigger/list", | ||||
|     device_id: deviceId, | ||||
|     }) | ||||
|     .then((triggers) => migrateAutomationTrigger(triggers) as DeviceTrigger[]); | ||||
|   }); | ||||
|  | ||||
| export const fetchDeviceActionCapabilities = ( | ||||
|   hass: HomeAssistant, | ||||
| @@ -93,7 +91,7 @@ const deviceAutomationIdentifiers = [ | ||||
|   "subtype", | ||||
|   "event", | ||||
|   "condition", | ||||
|   "trigger", | ||||
|   "platform", | ||||
| ]; | ||||
|  | ||||
| export const deviceAutomationsEqual = ( | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user