mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-30 14:09:58 +00:00 
			
		
		
		
	Compare commits
	
		
			73 Commits
		
	
	
		
			20250506.0
			...
			remove-pad
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 957bf875ae | ||
|   | 3804a4c7cb | ||
|   | b823a3b139 | ||
|   | 47c9a407e6 | ||
|   | c0ba48beb6 | ||
|   | 075e1df204 | ||
|   | 22c57853b4 | ||
|   | fe824062a5 | ||
|   | 92bf9b4979 | ||
|   | ac616a4d3d | ||
|   | 1aa1bfda2c | ||
|   | 38a5035d68 | ||
|   | 042cd0d3a3 | ||
|   | 00d708fbd4 | ||
|   | 852278e8aa | ||
|   | 15dcdffe55 | ||
|   | 0729aaacb8 | ||
|   | 92b8cd8f45 | ||
|   | ad8d3dd598 | ||
|   | d618c25095 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 83289bdd41 | ||
|   | c7882f3926 | ||
|   | 7434b12d9f | ||
|   | 9081441d95 | ||
|   | d63f610023 | ||
|   | e069875432 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 91026b0986 | ||
|   | 4ec5fbc9a4 | ||
|   | fb3a59272d | ||
|   | 9155c85509 | ||
|   | 9d74cd7561 | ||
|   | 22ddcca954 | ||
|   | 8f422357f1 | ||
|   | 44f5f7bdb5 | ||
|   | 83819a9be0 | ||
|   | 3c9dce20e2 | ||
|   | a19e7002ea | ||
|   | 75608db9b8 | ||
|   | 08f30b714b | ||
|   | 9983129e26 | ||
|   | 40fe62c2ec | ||
|   | d7660370ab | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | bdad76937e | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | d2822308ec | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 99b94e799d | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1fb28df1a6 | ||
|   | b4e8c56f58 | ||
|   | 17cc63deba | ||
|   | b4f1c8755d | ||
|   | b0d4c699db | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c07bf68161 | ||
|   | d1a0eaece5 | ||
|   | f608783551 | ||
|   | dddba58d38 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | ebc16d6520 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 4ed8ecad01 | ||
|   | c26fb1713d | ||
|   | 2b7b17625e | ||
|   | cd3e4f55e2 | ||
|   | 1c12aea8f6 | ||
|   | 3722f971ca | ||
|   | 409f665641 | ||
|   | 5b3b17ef6d | ||
|   | 05b49e8c80 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 2dbdbb4b64 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | a825b632bf | ||
|   | f8d706277d | ||
|   | 221bc732fb | ||
|   | 055c18463c | ||
|   | ddd51ff097 | ||
|   | 55c75096d0 | ||
|   | fd3502f3bc | ||
|   | 7d6bec01ae | 
							
								
								
									
										32
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										32
									
								
								package.json
									
									
									
									
									
								
							| @@ -26,7 +26,7 @@ | |||||||
|   "license": "Apache-2.0", |   "license": "Apache-2.0", | ||||||
|   "type": "module", |   "type": "module", | ||||||
|   "dependencies": { |   "dependencies": { | ||||||
|     "@babel/runtime": "7.27.0", |     "@babel/runtime": "7.27.1", | ||||||
|     "@braintree/sanitize-url": "7.1.1", |     "@braintree/sanitize-url": "7.1.1", | ||||||
|     "@codemirror/autocomplete": "6.18.6", |     "@codemirror/autocomplete": "6.18.6", | ||||||
|     "@codemirror/commands": "6.8.1", |     "@codemirror/commands": "6.8.1", | ||||||
| @@ -34,7 +34,7 @@ | |||||||
|     "@codemirror/legacy-modes": "6.5.1", |     "@codemirror/legacy-modes": "6.5.1", | ||||||
|     "@codemirror/search": "6.5.10", |     "@codemirror/search": "6.5.10", | ||||||
|     "@codemirror/state": "6.5.2", |     "@codemirror/state": "6.5.2", | ||||||
|     "@codemirror/view": "6.36.6", |     "@codemirror/view": "6.36.7", | ||||||
|     "@egjs/hammerjs": "2.0.17", |     "@egjs/hammerjs": "2.0.17", | ||||||
|     "@formatjs/intl-datetimeformat": "6.18.0", |     "@formatjs/intl-datetimeformat": "6.18.0", | ||||||
|     "@formatjs/intl-displaynames": "6.8.11", |     "@formatjs/intl-displaynames": "6.8.11", | ||||||
| @@ -89,8 +89,8 @@ | |||||||
|     "@thomasloven/round-slider": "0.6.0", |     "@thomasloven/round-slider": "0.6.0", | ||||||
|     "@tsparticles/engine": "3.8.1", |     "@tsparticles/engine": "3.8.1", | ||||||
|     "@tsparticles/preset-links": "3.2.0", |     "@tsparticles/preset-links": "3.2.0", | ||||||
|     "@vaadin/combo-box": "24.7.4", |     "@vaadin/combo-box": "24.7.5", | ||||||
|     "@vaadin/vaadin-themable-mixin": "24.7.4", |     "@vaadin/vaadin-themable-mixin": "24.7.5", | ||||||
|     "@vibrant/color": "4.0.0", |     "@vibrant/color": "4.0.0", | ||||||
|     "@vue/web-component-wrapper": "1.3.0", |     "@vue/web-component-wrapper": "1.3.0", | ||||||
|     "@webcomponents/scoped-custom-element-registry": "0.0.10", |     "@webcomponents/scoped-custom-element-registry": "0.0.10", | ||||||
| @@ -99,7 +99,7 @@ | |||||||
|     "barcode-detector": "3.0.1", |     "barcode-detector": "3.0.1", | ||||||
|     "color-name": "2.0.0", |     "color-name": "2.0.0", | ||||||
|     "comlink": "4.4.2", |     "comlink": "4.4.2", | ||||||
|     "core-js": "3.41.0", |     "core-js": "3.42.0", | ||||||
|     "cropperjs": "1.6.2", |     "cropperjs": "1.6.2", | ||||||
|     "date-fns": "4.1.0", |     "date-fns": "4.1.0", | ||||||
|     "date-fns-tz": "3.2.0", |     "date-fns-tz": "3.2.0", | ||||||
| @@ -131,7 +131,7 @@ | |||||||
|     "qrcode": "1.5.4", |     "qrcode": "1.5.4", | ||||||
|     "roboto-fontface": "0.10.0", |     "roboto-fontface": "0.10.0", | ||||||
|     "rrule": "2.8.1", |     "rrule": "2.8.1", | ||||||
|     "sortablejs": "patch:sortablejs@npm%3A1.15.3#~/.yarn/patches/sortablejs-npm-1.15.3-3235a8f83b.patch", |     "sortablejs": "patch:sortablejs@npm%3A1.15.6#~/.yarn/patches/sortablejs-npm-1.15.6-3235a8f83b.patch", | ||||||
|     "stacktrace-js": "2.0.2", |     "stacktrace-js": "2.0.2", | ||||||
|     "superstruct": "2.0.2", |     "superstruct": "2.0.2", | ||||||
|     "tinykeys": "3.0.0", |     "tinykeys": "3.0.0", | ||||||
| @@ -150,18 +150,18 @@ | |||||||
|     "xss": "1.0.15" |     "xss": "1.0.15" | ||||||
|   }, |   }, | ||||||
|   "devDependencies": { |   "devDependencies": { | ||||||
|     "@babel/core": "7.26.10", |     "@babel/core": "7.27.1", | ||||||
|     "@babel/helper-define-polyfill-provider": "0.6.4", |     "@babel/helper-define-polyfill-provider": "0.6.4", | ||||||
|     "@babel/plugin-transform-runtime": "7.26.10", |     "@babel/plugin-transform-runtime": "7.27.1", | ||||||
|     "@babel/preset-env": "7.26.9", |     "@babel/preset-env": "7.27.1", | ||||||
|     "@bundle-stats/plugin-webpack-filter": "4.19.1", |     "@bundle-stats/plugin-webpack-filter": "4.20.0", | ||||||
|     "@lokalise/node-api": "14.4.0", |     "@lokalise/node-api": "14.5.0", | ||||||
|     "@octokit/auth-oauth-device": "7.1.5", |     "@octokit/auth-oauth-device": "7.1.5", | ||||||
|     "@octokit/plugin-retry": "7.2.1", |     "@octokit/plugin-retry": "7.2.1", | ||||||
|     "@octokit/rest": "21.1.1", |     "@octokit/rest": "21.1.1", | ||||||
|     "@rsdoctor/rspack-plugin": "1.0.2", |     "@rsdoctor/rspack-plugin": "1.0.2", | ||||||
|     "@rspack/cli": "1.3.7", |     "@rspack/cli": "1.3.8", | ||||||
|     "@rspack/core": "1.3.7", |     "@rspack/core": "1.3.8", | ||||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", |     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||||
|     "@types/chromecast-caf-receiver": "6.0.21", |     "@types/chromecast-caf-receiver": "6.0.21", | ||||||
|     "@types/chromecast-caf-sender": "1.0.11", |     "@types/chromecast-caf-sender": "1.0.11", | ||||||
| @@ -185,7 +185,7 @@ | |||||||
|     "babel-plugin-template-html-minifier": "4.1.0", |     "babel-plugin-template-html-minifier": "4.1.0", | ||||||
|     "browserslist-useragent-regexp": "4.1.3", |     "browserslist-useragent-regexp": "4.1.3", | ||||||
|     "del": "8.0.0", |     "del": "8.0.0", | ||||||
|     "eslint": "9.25.1", |     "eslint": "9.26.0", | ||||||
|     "eslint-config-airbnb-base": "15.0.0", |     "eslint-config-airbnb-base": "15.0.0", | ||||||
|     "eslint-config-prettier": "10.1.2", |     "eslint-config-prettier": "10.1.2", | ||||||
|     "eslint-import-resolver-webpack": "0.13.10", |     "eslint-import-resolver-webpack": "0.13.10", | ||||||
| @@ -193,7 +193,7 @@ | |||||||
|     "eslint-plugin-lit": "2.1.1", |     "eslint-plugin-lit": "2.1.1", | ||||||
|     "eslint-plugin-lit-a11y": "4.1.4", |     "eslint-plugin-lit-a11y": "4.1.4", | ||||||
|     "eslint-plugin-unused-imports": "4.1.4", |     "eslint-plugin-unused-imports": "4.1.4", | ||||||
|     "eslint-plugin-wc": "3.0.0", |     "eslint-plugin-wc": "3.0.1", | ||||||
|     "fancy-log": "2.0.0", |     "fancy-log": "2.0.0", | ||||||
|     "fs-extra": "11.3.0", |     "fs-extra": "11.3.0", | ||||||
|     "glob": "11.0.2", |     "glob": "11.0.2", | ||||||
| @@ -219,7 +219,7 @@ | |||||||
|     "terser-webpack-plugin": "5.3.14", |     "terser-webpack-plugin": "5.3.14", | ||||||
|     "ts-lit-plugin": "2.0.2", |     "ts-lit-plugin": "2.0.2", | ||||||
|     "typescript": "5.8.3", |     "typescript": "5.8.3", | ||||||
|     "typescript-eslint": "8.31.0", |     "typescript-eslint": "8.31.1", | ||||||
|     "vite-tsconfig-paths": "5.1.4", |     "vite-tsconfig-paths": "5.1.4", | ||||||
|     "vitest": "3.1.2", |     "vitest": "3.1.2", | ||||||
|     "webpack-stats-plugin": "1.1.3", |     "webpack-stats-plugin": "1.1.3", | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| <svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg"> | <svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||
| <path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8283 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 39.4999L76.9105 39.4999V36.4999L37.5 36.4999L37.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/> | <path d="M76.9105 39.4999C77.739 39.4999 78.4105 38.8284 78.4105 37.9999C78.4105 37.1715 77.739 36.4999 76.9105 36.4999V39.4999ZM37.5 37.9999L37.5 39.4999L76.9105 39.4999V37.9999V36.4999L37.5 36.4999L37.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/> | ||||||
| <path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/> | <path d="M30.8239 22.3365L38.8239 38.8365L30.3239 50.3365" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/> | ||||||
| <mask id="mask0_1110_23734" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18"> | <mask id="mask0_2_779" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18"> | ||||||
| <path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/> | <path d="M45.75 42.075C45.75 42.4462 45.4462 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/> | ||||||
| </mask> | </mask> | ||||||
| <g mask="url(#mask0_1110_23734)"> | <g mask="url(#mask0_2_779)"> | ||||||
| <rect x="30" y="27" width="18" height="18" fill="#212121"/> | <rect x="30" y="27" width="18" height="18" fill="#00AFFF"/> | ||||||
| </g> | </g> | ||||||
| <path d="M82 37.9999C82 36.343 83.3431 34.9999 85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999Z" stroke="#00AFFF" stroke-width="2"/> | <path d="M85 34.9999C86.6569 34.9999 88 36.343 88 37.9999C88 39.6567 86.6569 40.9999 85 40.9999C83.3431 40.9999 82 39.6567 82 37.9999C82 36.343 83.3431 34.9999 85 34.9999Z" stroke="#00AFFF" stroke-width="2"/> | ||||||
| <rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/> | <rect x="23" y="11" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/> | ||||||
| <rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/> | <rect x="22" y="52" width="8" height="8" rx="4" fill="black" fill-opacity="0.32"/> | ||||||
| <path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/> | <path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="black" stroke-opacity="0.12" stroke-width="3" stroke-linecap="round"/> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 1.6 KiB After Width: | Height: | Size: 1.6 KiB | 
| @@ -1,19 +1,19 @@ | |||||||
| <svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg"> | <svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||
| <path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/> | <path d="M55.1358 38.5084C55.9608 38.4334 56.5688 37.7038 56.4938 36.8788C56.4188 36.0538 55.6892 35.4457 54.8642 35.5207L55.1358 38.5084ZM38.5 38.5146L38.6358 40.0084L55.1358 38.5084L55 37.0146L54.8642 35.5207L38.3642 37.0207L38.5 38.5146Z" fill="#00AFFF" fill-opacity="0.3"/> | ||||||
| <path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | <path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | ||||||
| <circle cx="47" cy="36" r="34" fill="white"/> | <circle cx="39" cy="36" r="34" fill="white"/> | ||||||
| <circle cx="47" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/> | <circle cx="39" cy="36" r="33.5" stroke="black" stroke-opacity="0.12"/> | ||||||
| <path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | <path d="M33.8777 12.5216C35.4905 12.1798 37.1631 12 38.8777 12C50.2401 12 59.7582 19.8959 62.2445 30.5M32 59C34.1788 59.6506 36.4874 60 38.8777 60C48.9498 60 57.5728 53.7955 61.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | ||||||
| <path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | <path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | ||||||
| <mask id="mask0_1110_23775" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18"> | <mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18"> | ||||||
| <path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/> | <path d="M45.75 42.075C45.75 42.4462 45.4463 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/> | ||||||
| </mask> | </mask> | ||||||
| <g mask="url(#mask0_1110_23775)"> | <g mask="url(#mask0_2_810)"> | ||||||
| <rect x="38" y="27" width="18" height="18" fill="#212121"/> | <rect x="30" y="27" width="18" height="18" fill="#00AFFF"/> | ||||||
| </g> | </g> | ||||||
| <path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/> | <path d="M55.5 39.4999C56.3284 39.4999 57 38.8283 57 37.9999C57 37.1715 56.3284 36.4999 55.5 36.4999L55.5 39.4999ZM41.5 37.9999L41.5 39.4999L55.5 39.4999L55.5 37.9999L55.5 36.4999L41.5 36.4999L41.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/> | ||||||
| <rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/> | <rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/> | ||||||
| <rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/> | <rect x="22" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/> | ||||||
| <path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | <path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | ||||||
| <path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/> | <path d="M63 34.9999C64.6569 34.9999 66 36.343 66 37.9999C66 39.6567 64.6569 40.9999 63 40.9999C61.3431 40.9999 60 39.6567 60 37.9999C60 36.343 61.3431 34.9999 63 34.9999Z" stroke="#00AFFF" stroke-width="2"/> | ||||||
| </svg> | </svg> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.5 KiB | 
| @@ -1,19 +1,18 @@ | |||||||
| <svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg"> | <svg width="94" height="72" viewBox="0 0 94 72" fill="none" xmlns="http://www.w3.org/2000/svg"> | ||||||
| <path d="M63.1358 38.5084C63.9608 38.4334 64.5688 37.7037 64.4938 36.8787C64.4188 36.0537 63.6892 35.4457 62.8642 35.5207L63.1358 38.5084ZM46.6358 40.0084L63.1358 38.5084L62.8642 35.5207L46.3642 37.0207L46.6358 40.0084Z" fill="#00AFFF" fill-opacity="0.3"/> | <path d="M55.1358 38.5084C55.9608 38.4334 56.5688 37.7038 56.4938 36.8788C56.4188 36.0538 55.6892 35.4457 54.8642 35.5207L55.1358 38.5084ZM38.5 38.5146L38.6358 40.0084L55.1358 38.5084L55 37.0146L54.8642 35.5207L38.3642 37.0207L38.5 38.5146Z" fill="#00AFFF" fill-opacity="0.3"/> | ||||||
| <path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | <path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | ||||||
| <circle cx="47" cy="36" r="34" fill="#1C1C1C"/> | <circle cx="39" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/> | ||||||
| <circle cx="47" cy="36" r="33.5" stroke="white" stroke-opacity="0.24"/> | <path d="M33.8777 12.5216C35.4905 12.1798 37.1631 12 38.8777 12C50.2401 12 59.7582 19.8959 62.2445 30.5M32 59C34.1788 59.6506 36.4874 60 38.8777 60C48.9498 60 57.5728 53.7955 61.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | ||||||
| <path d="M41.8777 12.5216C43.4905 12.1798 45.1631 12 46.8777 12C58.2401 12 67.7582 19.8959 70.2445 30.5M40 59C42.1788 59.6506 44.4874 60 46.8777 60C56.9498 60 65.5728 53.7955 69.1332 45" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | <path d="M30.5 22L37.9722 37.4115C38.2967 38.0807 38.223 38.8747 37.781 39.4728L30 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | ||||||
| <path d="M38.5 22L45.9722 37.4115C46.2967 38.0807 46.223 38.8747 45.781 39.4728L38 50" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | <mask id="mask0_2_810" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="30" y="27" width="18" height="18"> | ||||||
| <mask id="mask0_1180_4965" style="mask-type:alpha" maskUnits="userSpaceOnUse" x="38" y="27" width="18" height="18"> | <path d="M45.75 42.075C45.75 42.4462 45.4463 42.75 45.075 42.75H32.925C32.5538 42.75 32.25 42.4462 32.25 42.075V36.675C32.25 36.3037 32.4649 35.7851 32.7276 35.5224L38.5224 29.7275C38.7851 29.4649 39.2143 29.4649 39.477 29.7275L45.2724 35.523C45.5351 35.7857 45.75 36.3043 45.75 36.6755V42.075Z" fill="black"/> | ||||||
| <path d="M53.75 42.075C53.75 42.4462 53.4462 42.75 53.075 42.75H40.925C40.5538 42.75 40.25 42.4462 40.25 42.075V36.675C40.25 36.3037 40.4649 35.7851 40.7276 35.5224L46.5224 29.7275C46.7851 29.4649 47.2143 29.4649 47.477 29.7275L53.2724 35.523C53.5351 35.7857 53.75 36.3043 53.75 36.6755V42.075Z" fill="black"/> |  | ||||||
| </mask> | </mask> | ||||||
| <g mask="url(#mask0_1180_4965)"> | <g mask="url(#mask0_2_810)"> | ||||||
| <rect x="38" y="27" width="18" height="18" fill="#00AFFF"/> | <rect x="30" y="27" width="18" height="18" fill="#00AFFF"/> | ||||||
| </g> | </g> | ||||||
| <path d="M63.5 39.4999C64.3284 39.4999 65 38.8283 65 37.9999C65 37.1715 64.3284 36.4999 63.5 36.4999L63.5 39.4999ZM49.5 39.4999L63.5 39.4999L63.5 36.4999L49.5 36.4999L49.5 39.4999Z" fill="#00AFFF" fill-opacity="0.3"/> | <path d="M55.5 39.4999C56.3284 39.4999 57 38.8283 57 37.9999C57 37.1715 56.3284 36.4999 55.5 36.4999L55.5 39.4999ZM41.5 37.9999L41.5 39.4999L55.5 39.4999L55.5 37.9999L55.5 36.4999L41.5 36.4999L41.5 37.9999Z" fill="#00AFFF" fill-opacity="0.3"/> | ||||||
| <rect x="31" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/> | <rect x="23" y="11" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/> | ||||||
| <rect x="30" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/> | <rect x="22" y="52" width="8" height="8" rx="4" fill="#00AFFF" fill-opacity="0.6"/> | ||||||
| <path d="M29.5715 19.5C25.4983 23.801 23 29.6087 23 36C23 41.9085 25.1351 47.3183 28.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | <path d="M21.5715 19.5C17.4983 23.801 15 29.6087 15 36C15 41.9085 17.1351 47.3183 20.6759 51.5" stroke="#00AFFF" stroke-opacity="0.3" stroke-width="3" stroke-linecap="round" stroke-linejoin="bevel"/> | ||||||
| <path d="M68 37.9999C68 36.343 69.3431 34.9999 71 34.9999C72.6569 34.9999 74 36.343 74 37.9999C74 39.6567 72.6569 40.9999 71 40.9999C69.3431 40.9999 68 39.6567 68 37.9999Z" stroke="#00AFFF" stroke-width="2"/> | <path d="M63 34.9999C64.6569 34.9999 66 36.343 66 37.9999C66 39.6567 64.6569 40.9999 63 40.9999C61.3431 40.9999 60 39.6567 60 37.9999C60 36.343 61.3431 34.9999 63 34.9999Z" stroke="#00AFFF" stroke-width="2"/> | ||||||
| </svg> | </svg> | ||||||
|   | |||||||
| Before Width: | Height: | Size: 2.4 KiB After Width: | Height: | Size: 2.4 KiB | 
| @@ -9,6 +9,8 @@ import type { LitElement } from "lit"; | |||||||
|  */ |  */ | ||||||
| export interface DragScrollControllerConfig { | export interface DragScrollControllerConfig { | ||||||
|   selector: string; |   selector: string; | ||||||
|  |   enabled?: boolean; | ||||||
|  |   trackScroll?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export class DragScrollController implements ReactiveController { | export class DragScrollController implements ReactiveController { | ||||||
| @@ -22,37 +24,108 @@ export class DragScrollController implements ReactiveController { | |||||||
|  |  | ||||||
|   public scrollLeft = 0; |   public scrollLeft = 0; | ||||||
|  |  | ||||||
|  |   public scrolledStart = false; | ||||||
|  |  | ||||||
|  |   public scrolledEnd = false; | ||||||
|  |  | ||||||
|   private _host: ReactiveControllerHost & LitElement; |   private _host: ReactiveControllerHost & LitElement; | ||||||
|  |  | ||||||
|   private _selector: string; |   private _selector: string; | ||||||
|  |  | ||||||
|   private _scrollContainer?: HTMLElement | null; |   private _scrollContainer?: HTMLElement | null; | ||||||
|  |  | ||||||
|  |   private _enabled = true; | ||||||
|  |  | ||||||
|  |   private _trackScroll = false; | ||||||
|  |  | ||||||
|  |   public get enabled(): boolean { | ||||||
|  |     return this._enabled; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public set enabled(value: boolean) { | ||||||
|  |     if (value === this._enabled) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     this._enabled = value; | ||||||
|  |     if (this._enabled) { | ||||||
|  |       this._attach(); | ||||||
|  |     } else { | ||||||
|  |       this._detach(); | ||||||
|  |     } | ||||||
|  |     this._host.requestUpdate(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   constructor( |   constructor( | ||||||
|     host: ReactiveControllerHost & LitElement, |     host: ReactiveControllerHost & LitElement, | ||||||
|     { selector }: DragScrollControllerConfig |     { selector, enabled, trackScroll }: DragScrollControllerConfig | ||||||
|   ) { |   ) { | ||||||
|     this._selector = selector; |     this._selector = selector; | ||||||
|     this._host = host; |     this._host = host; | ||||||
|  |     this._trackScroll = trackScroll ?? false; | ||||||
|  |     this.enabled = enabled ?? true; | ||||||
|     host.addController(this); |     host.addController(this); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   hostUpdated() { |   hostUpdated() { | ||||||
|     if (this._scrollContainer) { |     if (!this.enabled || this._scrollContainer) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     this._attach(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   hostDisconnected() { | ||||||
|  |     this._detach(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _attach() { | ||||||
|     this._scrollContainer = this._host.renderRoot?.querySelector( |     this._scrollContainer = this._host.renderRoot?.querySelector( | ||||||
|       this._selector |       this._selector | ||||||
|     ); |     ); | ||||||
|     if (this._scrollContainer) { |     if (this._scrollContainer) { | ||||||
|       this._scrollContainer.addEventListener("mousedown", this._mouseDown); |       this._scrollContainer.addEventListener("mousedown", this._mouseDown); | ||||||
|  |       if (this._trackScroll) { | ||||||
|  |         this._scrollContainer.addEventListener("scroll", this._onScroll); | ||||||
|  |         this.scrolledStart = this._scrollContainer.scrollLeft > 0; | ||||||
|  |         this.scrolledEnd = | ||||||
|  |           this._scrollContainer.scrollLeft + this._scrollContainer.offsetWidth < | ||||||
|  |           this._scrollContainer.scrollWidth; | ||||||
|  |         this._host.requestUpdate(); | ||||||
|  |       } | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   hostDisconnected() { |   private _detach() { | ||||||
|     window.removeEventListener("mousemove", this._mouseMove); |     window.removeEventListener("mousemove", this._mouseMove); | ||||||
|     window.removeEventListener("mouseup", this._mouseUp); |     window.removeEventListener("mouseup", this._mouseUp); | ||||||
|  |     if (this._scrollContainer) { | ||||||
|  |       this._scrollContainer.removeEventListener("mousedown", this._mouseDown); | ||||||
|  |       this._scrollContainer.removeEventListener("scroll", this._onScroll); | ||||||
|  |       this._scrollContainer = undefined; | ||||||
|     } |     } | ||||||
|  |     this.scrolled = false; | ||||||
|  |     this.scrolling = false; | ||||||
|  |     this.scrolledStart = false; | ||||||
|  |     this.scrolledEnd = false; | ||||||
|  |     this.mouseIsDown = false; | ||||||
|  |     this.scrollStartX = 0; | ||||||
|  |     this.scrollLeft = 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _onScroll = (event: Event) => { | ||||||
|  |     const oldScrolledStart = this.scrolledStart; | ||||||
|  |     const oldScrolledEnd = this.scrolledEnd; | ||||||
|  |  | ||||||
|  |     const container = event.currentTarget as HTMLElement; | ||||||
|  |     this.scrolledStart = container.scrollLeft > 0; | ||||||
|  |     this.scrolledEnd = | ||||||
|  |       container.scrollLeft + container.offsetWidth < container.scrollWidth; | ||||||
|  |     if ( | ||||||
|  |       this.scrolledStart !== oldScrolledStart || | ||||||
|  |       this.scrolledEnd !== oldScrolledEnd | ||||||
|  |     ) { | ||||||
|  |       this._host.requestUpdate(); | ||||||
|  |     } | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   private _mouseDown = (event: MouseEvent) => { |   private _mouseDown = (event: MouseEvent) => { | ||||||
|     const scrollContainer = this._scrollContainer; |     const scrollContainer = this._scrollContainer; | ||||||
|   | |||||||
| @@ -1,6 +1,5 @@ | |||||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import { ReactiveElement } from "lit"; | import type { ReactiveElement } from "lit"; | ||||||
| import type { InternalPropertyDeclaration } from "lit/decorators"; |  | ||||||
|  |  | ||||||
| type Callback = (oldValue: any, newValue: any) => void; | type Callback = (oldValue: any, newValue: any) => void; | ||||||
|  |  | ||||||
| @@ -108,7 +107,6 @@ export function storage(options: { | |||||||
|   storage?: "localStorage" | "sessionStorage"; |   storage?: "localStorage" | "sessionStorage"; | ||||||
|   subscribe?: boolean; |   subscribe?: boolean; | ||||||
|   state?: boolean; |   state?: boolean; | ||||||
|   stateOptions?: InternalPropertyDeclaration; |  | ||||||
|   serializer?: (value: any) => any; |   serializer?: (value: any) => any; | ||||||
|   deserializer?: (value: any) => any; |   deserializer?: (value: any) => any; | ||||||
| }) { | }) { | ||||||
| @@ -174,7 +172,7 @@ export function storage(options: { | |||||||
|       performUpdate.call(this); |       performUpdate.call(this); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|     if (options.state && options.subscribe) { |     if (options.subscribe) { | ||||||
|       const connectedCallback = proto.connectedCallback; |       const connectedCallback = proto.connectedCallback; | ||||||
|       const disconnectedCallback = proto.disconnectedCallback; |       const disconnectedCallback = proto.disconnectedCallback; | ||||||
|  |  | ||||||
| @@ -192,12 +190,6 @@ export function storage(options: { | |||||||
|         el.__unbsubLocalStorage = undefined; |         el.__unbsubLocalStorage = undefined; | ||||||
|       }; |       }; | ||||||
|     } |     } | ||||||
|     if (options.state) { |  | ||||||
|       ReactiveElement.createProperty(propertyKey, { |  | ||||||
|         noAccessor: true, |  | ||||||
|         ...options.stateOptions, |  | ||||||
|       }); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); |     const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); | ||||||
|     let newDescriptor: PropertyDescriptor; |     let newDescriptor: PropertyDescriptor; | ||||||
|   | |||||||
| @@ -1,10 +1,4 @@ | |||||||
| import { | import type { ReactiveElement, PropertyValues } from "lit"; | ||||||
|   ReactiveElement, |  | ||||||
|   type PropertyDeclaration, |  | ||||||
|   type PropertyValues, |  | ||||||
| } from "lit"; |  | ||||||
| import { shallowEqual } from "../util/shallow-equal"; |  | ||||||
|  |  | ||||||
| /** | /** | ||||||
|  * Transform function type. |  * Transform function type. | ||||||
|  */ |  */ | ||||||
| @@ -23,7 +17,6 @@ type ReactiveTransformElement = ReactiveElement & { | |||||||
| export function transform<T, V>(config: { | export function transform<T, V>(config: { | ||||||
|   transformer: Transformer<T, V>; |   transformer: Transformer<T, V>; | ||||||
|   watch?: PropertyKey[]; |   watch?: PropertyKey[]; | ||||||
|   propertyOptions?: PropertyDeclaration; |  | ||||||
| }) { | }) { | ||||||
|   return <ElemClass extends ReactiveElement>( |   return <ElemClass extends ReactiveElement>( | ||||||
|     proto: ElemClass, |     proto: ElemClass, | ||||||
| @@ -84,11 +77,6 @@ export function transform<T, V>(config: { | |||||||
|         curWatch.add(propertyKey); |         curWatch.add(propertyKey); | ||||||
|       }); |       }); | ||||||
|     } |     } | ||||||
|     ReactiveElement.createProperty(propertyKey, { |  | ||||||
|       noAccessor: true, |  | ||||||
|       hasChanged: (v: any, o: any) => !shallowEqual(v, o), |  | ||||||
|       ...config.propertyOptions, |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); |     const descriptor = Object.getOwnPropertyDescriptor(proto, propertyKey); | ||||||
|     let newDescriptor: PropertyDescriptor; |     let newDescriptor: PropertyDescriptor; | ||||||
|   | |||||||
| @@ -1,5 +1,11 @@ | |||||||
| export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => { | export const canOverrideAlphanumericInput = (composedPath: EventTarget[]) => { | ||||||
|   if (composedPath.some((el) => "tagName" in el && el.tagName === "HA-MENU")) { |   if ( | ||||||
|  |     composedPath.some( | ||||||
|  |       (el) => | ||||||
|  |         "tagName" in el && | ||||||
|  |         (el.tagName === "HA-MENU" || el.tagName === "HA-CODE-EDITOR") | ||||||
|  |     ) | ||||||
|  |   ) { | ||||||
|     return false; |     return false; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -600,12 +600,32 @@ export class HaChartBase extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _getSeries() { |   private _getSeries() { | ||||||
|     if (!Array.isArray(this.data)) { |     const series = ensureArray(this.data).filter( | ||||||
|       return this.data; |  | ||||||
|     } |  | ||||||
|     return this.data.filter( |  | ||||||
|       (d) => !this._hiddenDatasets.has(String(d.name ?? d.id)) |       (d) => !this._hiddenDatasets.has(String(d.name ?? d.id)) | ||||||
|     ); |     ); | ||||||
|  |     const yAxis = (this.options?.yAxis?.[0] ?? this.options?.yAxis) as | ||||||
|  |       | YAXisOption | ||||||
|  |       | undefined; | ||||||
|  |     if (yAxis?.type === "log") { | ||||||
|  |       // set <=0 values to null so they render as gaps on a log graph | ||||||
|  |       return series.map((d) => | ||||||
|  |         d.type === "line" | ||||||
|  |           ? { | ||||||
|  |               ...d, | ||||||
|  |               data: d.data?.map((v) => | ||||||
|  |                 Array.isArray(v) | ||||||
|  |                   ? [ | ||||||
|  |                       v[0], | ||||||
|  |                       typeof v[1] !== "number" || v[1] > 0 ? v[1] : null, | ||||||
|  |                       ...v.slice(2), | ||||||
|  |                     ] | ||||||
|  |                   : v | ||||||
|  |               ), | ||||||
|  |             } | ||||||
|  |           : d | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     return series; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _getDefaultHeight() { |   private _getDefaultHeight() { | ||||||
|   | |||||||
| @@ -603,7 +603,7 @@ export class HaDataTable extends LitElement { | |||||||
|                           .map( |                           .map( | ||||||
|                             ([key2, column2], i) => |                             ([key2, column2], i) => | ||||||
|                               html`${i !== 0 |                               html`${i !== 0 | ||||||
|                                 ? " ⸱ " |                                 ? " · " | ||||||
|                                 : nothing}${column2.template |                                 : nothing}${column2.template | ||||||
|                                 ? column2.template(row) |                                 ? column2.template(row) | ||||||
|                                 : row[key2]}` |                                 : row[key2]}` | ||||||
|   | |||||||
| @@ -73,16 +73,20 @@ class HaEntityAttributePicker extends LitElement { | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const stateObj = this.hass.states[this.entityId!] as HassEntity | undefined; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-combo-box |       <ha-combo-box | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .value=${this.value |         .value=${this.value | ||||||
|  |           ? stateObj | ||||||
|             ? computeAttributeNameDisplay( |             ? computeAttributeNameDisplay( | ||||||
|                 this.hass.localize, |                 this.hass.localize, | ||||||
|               this.hass.states[this.entityId!], |                 stateObj, | ||||||
|                 this.hass.entities, |                 this.hass.entities, | ||||||
|                 this.value |                 this.value | ||||||
|               ) |               ) | ||||||
|  |             : this.value | ||||||
|           : ""} |           : ""} | ||||||
|         .autofocus=${this.autofocus} |         .autofocus=${this.autofocus} | ||||||
|         .label=${this.label ?? |         .label=${this.label ?? | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import type { HassEntity } from "home-assistant-js-websocket"; | |||||||
| import type { PropertyValues, TemplateResult } from "lit"; | import type { PropertyValues, TemplateResult } from "lit"; | ||||||
| import { html, LitElement, nothing } from "lit"; | import { html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| import { styleMap } from "lit/directives/style-map"; |  | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { computeAreaName } from "../../common/entity/compute_area_name"; | import { computeAreaName } from "../../common/entity/compute_area_name"; | ||||||
| @@ -30,28 +29,17 @@ import "../ha-icon-button"; | |||||||
| import "../ha-svg-icon"; | import "../ha-svg-icon"; | ||||||
| import "./state-badge"; | import "./state-badge"; | ||||||
|  |  | ||||||
| const FAKE_ENTITY: HassEntity = { | interface EntityComboBoxItem { | ||||||
|   entity_id: "", |  | ||||||
|   state: "", |  | ||||||
|   last_changed: "", |  | ||||||
|   last_updated: "", |  | ||||||
|   context: { id: "", user_id: null, parent_id: null }, |  | ||||||
|   attributes: {}, |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| interface EntityComboBoxItem extends HassEntity { |  | ||||||
|   // Force empty label to always display empty value by default in the search field |   // Force empty label to always display empty value by default in the search field | ||||||
|  |   id: string; | ||||||
|   label: ""; |   label: ""; | ||||||
|   primary: string; |   primary: string; | ||||||
|   secondary?: string; |   secondary?: string; | ||||||
|   translated_domain?: string; |   domain_name?: string; | ||||||
|   show_entity_id?: boolean; |   search_labels?: string[]; | ||||||
|   entity_name?: string; |  | ||||||
|   area_name?: string; |  | ||||||
|   device_name?: string; |  | ||||||
|   friendly_name?: string; |  | ||||||
|   sorting_label?: string; |   sorting_label?: string; | ||||||
|   icon_path?: string; |   icon_path?: string; | ||||||
|  |   stateObj?: HassEntity; | ||||||
| } | } | ||||||
|  |  | ||||||
| export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean; | export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean; | ||||||
| @@ -59,22 +47,6 @@ export type HaEntityComboBoxEntityFilterFunc = (entity: HassEntity) => boolean; | |||||||
| const CREATE_ID = "___create-new-entity___"; | const CREATE_ID = "___create-new-entity___"; | ||||||
| const NO_ENTITIES_ID = "___no-entities___"; | const NO_ENTITIES_ID = "___no-entities___"; | ||||||
|  |  | ||||||
| const DOMAIN_STYLE = styleMap({ |  | ||||||
|   fontSize: "var(--ha-font-size-s)", |  | ||||||
|   fontWeight: "var(--ha-font-weight-normal)", |  | ||||||
|   lineHeight: "var(--ha-line-height-normal)", |  | ||||||
|   alignSelf: "flex-end", |  | ||||||
|   maxWidth: "30%", |  | ||||||
|   textOverflow: "ellipsis", |  | ||||||
|   overflow: "hidden", |  | ||||||
|   whiteSpace: "nowrap", |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| const ENTITY_ID_STYLE = styleMap({ |  | ||||||
|   fontFamily: "var(--code-font-family, monospace)", |  | ||||||
|   fontSize: "var(--ha-font-size-xs)", |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| @customElement("ha-entity-combo-box") | @customElement("ha-entity-combo-box") | ||||||
| export class HaEntityComboBox extends LitElement { | export class HaEntityComboBox extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -177,14 +149,19 @@ export class HaEntityComboBox extends LitElement { | |||||||
|   private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = ( |   private _rowRenderer: ComboBoxLitRenderer<EntityComboBoxItem> = ( | ||||||
|     item, |     item, | ||||||
|     { index } |     { index } | ||||||
|   ) => html` |   ) => { | ||||||
|  |     const showEntityId = this.hass.userData?.showEntityIdPicker; | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|       <ha-combo-box-item type="button" compact .borderTop=${index !== 0}> |       <ha-combo-box-item type="button" compact .borderTop=${index !== 0}> | ||||||
|         ${item.icon_path |         ${item.icon_path | ||||||
|         ? html`<ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon>` |           ? html` | ||||||
|  |               <ha-svg-icon slot="start" .path=${item.icon_path}></ha-svg-icon> | ||||||
|  |             ` | ||||||
|           : html` |           : html` | ||||||
|               <state-badge |               <state-badge | ||||||
|                 slot="start" |                 slot="start" | ||||||
|               .stateObj=${item} |                 .stateObj=${item.stateObj} | ||||||
|                 .hass=${this.hass} |                 .hass=${this.hass} | ||||||
|               ></state-badge> |               ></state-badge> | ||||||
|             `} |             `} | ||||||
| @@ -192,18 +169,21 @@ export class HaEntityComboBox extends LitElement { | |||||||
|         ${item.secondary |         ${item.secondary | ||||||
|           ? html`<span slot="supporting-text">${item.secondary}</span>` |           ? html`<span slot="supporting-text">${item.secondary}</span>` | ||||||
|           : nothing} |           : nothing} | ||||||
|       ${item.entity_id && item.show_entity_id |         ${item.stateObj && showEntityId | ||||||
|         ? html`<span slot="supporting-text" style=${ENTITY_ID_STYLE} |           ? html` | ||||||
|             >${item.entity_id}</span |               <span slot="supporting-text" class="code"> | ||||||
|           >` |                 ${item.stateObj.entity_id} | ||||||
|  |               </span> | ||||||
|  |             ` | ||||||
|           : nothing} |           : nothing} | ||||||
|       ${item.translated_domain && !item.show_entity_id |         ${item.domain_name && !showEntityId | ||||||
|         ? html`<div slot="trailing-supporting-text" style=${DOMAIN_STYLE}> |           ? html` | ||||||
|             ${item.translated_domain} |               <div slot="trailing-supporting-text">${item.domain_name}</div> | ||||||
|           </div>` |             ` | ||||||
|           : nothing} |           : nothing} | ||||||
|       </ha-combo-box-item> |       </ha-combo-box-item> | ||||||
|     `; |     `; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   private _getItems = memoizeOne( |   private _getItems = memoizeOne( | ||||||
|     ( |     ( | ||||||
| @@ -218,7 +198,7 @@ export class HaEntityComboBox extends LitElement { | |||||||
|       excludeEntities: this["excludeEntities"], |       excludeEntities: this["excludeEntities"], | ||||||
|       createDomains: this["createDomains"] |       createDomains: this["createDomains"] | ||||||
|     ): EntityComboBoxItem[] => { |     ): EntityComboBoxItem[] => { | ||||||
|       let states: EntityComboBoxItem[] = []; |       let items: EntityComboBoxItem[] = []; | ||||||
|  |  | ||||||
|       let entityIds = Object.keys(hass.states); |       let entityIds = Object.keys(hass.states); | ||||||
|  |  | ||||||
| @@ -236,9 +216,8 @@ export class HaEntityComboBox extends LitElement { | |||||||
|             ); |             ); | ||||||
|  |  | ||||||
|             return { |             return { | ||||||
|               ...FAKE_ENTITY, |               id: CREATE_ID + domain, | ||||||
|               label: "", |               label: "", | ||||||
|               entity_id: CREATE_ID + domain, |  | ||||||
|               primary: primary, |               primary: primary, | ||||||
|               secondary: this.hass.localize( |               secondary: this.hass.localize( | ||||||
|                 "ui.components.entity.entity-picker.new_entity" |                 "ui.components.entity.entity-picker.new_entity" | ||||||
| @@ -251,9 +230,8 @@ export class HaEntityComboBox extends LitElement { | |||||||
|       if (!entityIds.length) { |       if (!entityIds.length) { | ||||||
|         return [ |         return [ | ||||||
|           { |           { | ||||||
|             ...FAKE_ENTITY, |             id: NO_ENTITIES_ID, | ||||||
|             label: "", |             label: "", | ||||||
|             entity_id: NO_ENTITIES_ID, |  | ||||||
|             primary: this.hass!.localize( |             primary: this.hass!.localize( | ||||||
|               "ui.components.entity.entity-picker.no_entities" |               "ui.components.entity.entity-picker.no_entities" | ||||||
|             ), |             ), | ||||||
| @@ -289,7 +267,7 @@ export class HaEntityComboBox extends LitElement { | |||||||
|  |  | ||||||
|       const isRTL = computeRTL(this.hass); |       const isRTL = computeRTL(this.hass); | ||||||
|  |  | ||||||
|       states = entityIds |       items = entityIds | ||||||
|         .map<EntityComboBoxItem>((entityId) => { |         .map<EntityComboBoxItem>((entityId) => { | ||||||
|           const stateObj = hass!.states[entityId]; |           const stateObj = hass!.states[entityId]; | ||||||
|  |  | ||||||
| @@ -300,30 +278,32 @@ export class HaEntityComboBox extends LitElement { | |||||||
|           const deviceName = device ? computeDeviceName(device) : undefined; |           const deviceName = device ? computeDeviceName(device) : undefined; | ||||||
|           const areaName = area ? computeAreaName(area) : undefined; |           const areaName = area ? computeAreaName(area) : undefined; | ||||||
|  |  | ||||||
|  |           const domainName = domainToName( | ||||||
|  |             this.hass.localize, | ||||||
|  |             computeDomain(entityId) | ||||||
|  |           ); | ||||||
|  |  | ||||||
|           const primary = entityName || deviceName || entityId; |           const primary = entityName || deviceName || entityId; | ||||||
|           const secondary = [areaName, entityName ? deviceName : undefined] |           const secondary = [areaName, entityName ? deviceName : undefined] | ||||||
|             .filter(Boolean) |             .filter(Boolean) | ||||||
|             .join(isRTL ? " ◂ " : " ▸ "); |             .join(isRTL ? " ◂ " : " ▸ "); | ||||||
|  |  | ||||||
|           const translatedDomain = domainToName( |  | ||||||
|             this.hass.localize, |  | ||||||
|             computeDomain(entityId) |  | ||||||
|           ); |  | ||||||
|  |  | ||||||
|           return { |           return { | ||||||
|             ...hass!.states[entityId], |             id: entityId, | ||||||
|             label: "", |             label: "", | ||||||
|             primary: primary, |             primary: primary, | ||||||
|             secondary: |             secondary: secondary, | ||||||
|               secondary || |             domain_name: domainName, | ||||||
|               this.hass.localize("ui.components.device-picker.no_area"), |             sorting_label: [deviceName, entityName].filter(Boolean).join("_"), | ||||||
|             translated_domain: translatedDomain, |             search_labels: [ | ||||||
|             sorting_label: [deviceName, entityName].filter(Boolean).join("-"), |               entityName, | ||||||
|             entity_name: entityName || deviceName, |               deviceName, | ||||||
|             area_name: areaName, |               areaName, | ||||||
|             device_name: deviceName, |               domainName, | ||||||
|             friendly_name: friendlyName, |               friendlyName, | ||||||
|             show_entity_id: hass.userData?.showEntityIdPicker, |               entityId, | ||||||
|  |             ].filter(Boolean) as string[], | ||||||
|  |             stateObj: stateObj, | ||||||
|           }; |           }; | ||||||
|         }) |         }) | ||||||
|         .sort((entityA, entityB) => |         .sort((entityA, entityB) => | ||||||
| @@ -335,41 +315,43 @@ export class HaEntityComboBox extends LitElement { | |||||||
|         ); |         ); | ||||||
|  |  | ||||||
|       if (includeDeviceClasses) { |       if (includeDeviceClasses) { | ||||||
|         states = states.filter( |         items = items.filter( | ||||||
|           (stateObj) => |           (item) => | ||||||
|             // We always want to include the entity of the current value |             // We always want to include the entity of the current value | ||||||
|             stateObj.entity_id === this.value || |             item.id === this.value || | ||||||
|             (stateObj.attributes.device_class && |             (item.stateObj?.attributes.device_class && | ||||||
|               includeDeviceClasses.includes(stateObj.attributes.device_class)) |               includeDeviceClasses.includes( | ||||||
|  |                 item.stateObj.attributes.device_class | ||||||
|  |               )) | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (includeUnitOfMeasurement) { |       if (includeUnitOfMeasurement) { | ||||||
|         states = states.filter( |         items = items.filter( | ||||||
|           (stateObj) => |           (item) => | ||||||
|             // We always want to include the entity of the current value |             // We always want to include the entity of the current value | ||||||
|             stateObj.entity_id === this.value || |             item.id === this.value || | ||||||
|             (stateObj.attributes.unit_of_measurement && |             (item.stateObj?.attributes.unit_of_measurement && | ||||||
|               includeUnitOfMeasurement.includes( |               includeUnitOfMeasurement.includes( | ||||||
|                 stateObj.attributes.unit_of_measurement |                 item.stateObj.attributes.unit_of_measurement | ||||||
|               )) |               )) | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (entityFilter) { |       if (entityFilter) { | ||||||
|         states = states.filter( |         items = items.filter( | ||||||
|           (stateObj) => |           (item) => | ||||||
|             // We always want to include the entity of the current value |             // We always want to include the entity of the current value | ||||||
|             stateObj.entity_id === this.value || entityFilter!(stateObj) |             item.id === this.value || | ||||||
|  |             (item.stateObj && entityFilter!(item.stateObj)) | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (!states.length) { |       if (!items.length) { | ||||||
|         return [ |         return [ | ||||||
|           { |           { | ||||||
|             ...FAKE_ENTITY, |             id: NO_ENTITIES_ID, | ||||||
|             label: "", |             label: "", | ||||||
|             entity_id: NO_ENTITIES_ID, |  | ||||||
|             primary: this.hass!.localize( |             primary: this.hass!.localize( | ||||||
|               "ui.components.entity.entity-picker.no_match" |               "ui.components.entity.entity-picker.no_match" | ||||||
|             ), |             ), | ||||||
| @@ -380,10 +362,10 @@ export class HaEntityComboBox extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (createItems?.length) { |       if (createItems?.length) { | ||||||
|         states.push(...createItems); |         items.push(...createItems); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       return states; |       return items; | ||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
| @@ -426,7 +408,7 @@ export class HaEntityComboBox extends LitElement { | |||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <ha-combo-box |       <ha-combo-box | ||||||
|         item-value-path="entity_id" |         item-value-path="id" | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .value=${this._value} |         .value=${this._value} | ||||||
|         .label=${this.label === undefined |         .label=${this.label === undefined | ||||||
| @@ -478,17 +460,7 @@ export class HaEntityComboBox extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _fuseIndex = memoizeOne((states: EntityComboBoxItem[]) => |   private _fuseIndex = memoizeOne((states: EntityComboBoxItem[]) => | ||||||
|     Fuse.createIndex( |     Fuse.createIndex(["search_labels"], states) | ||||||
|       [ |  | ||||||
|         "entity_name", |  | ||||||
|         "device_name", |  | ||||||
|         "area_name", |  | ||||||
|         "translated_domain", |  | ||||||
|         "friendly_name", // for backwards compatibility |  | ||||||
|         "entity_id", // for technical search |  | ||||||
|       ], |  | ||||||
|       states |  | ||||||
|     ) |  | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private _filterChanged(ev: CustomEvent): void { |   private _filterChanged(ev: CustomEvent): void { | ||||||
| @@ -505,9 +477,8 @@ export class HaEntityComboBox extends LitElement { | |||||||
|       if (results.length === 0) { |       if (results.length === 0) { | ||||||
|         target.filteredItems = [ |         target.filteredItems = [ | ||||||
|           { |           { | ||||||
|             ...FAKE_ENTITY, |             id: NO_ENTITIES_ID, | ||||||
|             label: "", |             label: "", | ||||||
|             entity_id: NO_ENTITIES_ID, |  | ||||||
|             primary: this.hass!.localize( |             primary: this.hass!.localize( | ||||||
|               "ui.components.entity.entity-picker.no_match" |               "ui.components.entity.entity-picker.no_match" | ||||||
|             ), |             ), | ||||||
|   | |||||||
| @@ -162,10 +162,7 @@ export class HaEntityPicker extends LitElement { | |||||||
|         slot="start" |         slot="start" | ||||||
|       ></state-badge> |       ></state-badge> | ||||||
|       <span slot="headline">${primary}</span> |       <span slot="headline">${primary}</span> | ||||||
|       <span slot="supporting-text"> |       <span slot="supporting-text">${secondary}</span> | ||||||
|         ${secondary || |  | ||||||
|         this.hass.localize("ui.components.device-picker.no_area")} |  | ||||||
|       </span> |  | ||||||
|       ${showClearIcon |       ${showClearIcon | ||||||
|         ? html`<ha-icon-button |         ? html`<ha-icon-button | ||||||
|             class="clear" |             class="clear" | ||||||
|   | |||||||
| @@ -1,11 +1,10 @@ | |||||||
| import { mdiChartLine, mdiShape } from "@mdi/js"; | import { mdiChartLine, mdiHelpCircle, mdiShape } from "@mdi/js"; | ||||||
| import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | ||||||
| import Fuse from "fuse.js"; | import Fuse from "fuse.js"; | ||||||
| import type { HassEntity } from "home-assistant-js-websocket"; | import type { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import type { PropertyValues, TemplateResult } from "lit"; | import type { PropertyValues, TemplateResult } from "lit"; | ||||||
| import { html, LitElement, nothing } from "lit"; | import { html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| import { styleMap } from "lit/directives/style-map"; |  | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { ensureArray } from "../../common/array/ensure-array"; | import { ensureArray } from "../../common/array/ensure-array"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| @@ -26,31 +25,27 @@ import type { HaComboBox } from "../ha-combo-box"; | |||||||
| import "../ha-combo-box-item"; | import "../ha-combo-box-item"; | ||||||
| import "../ha-svg-icon"; | import "../ha-svg-icon"; | ||||||
| import "./state-badge"; | import "./state-badge"; | ||||||
|  | import { documentationUrl } from "../../util/documentation-url"; | ||||||
|  |  | ||||||
| type StatisticItemType = "entity" | "external" | "no_state"; | type StatisticItemType = "entity" | "external" | "no_state"; | ||||||
|  |  | ||||||
| interface StatisticItem { | interface StatisticItem { | ||||||
|  |   // Force empty label to always display empty value by default in the search field | ||||||
|   id: string; |   id: string; | ||||||
|  |   statistic_id?: string; | ||||||
|   label: ""; |   label: ""; | ||||||
|   primary: string; |   primary: string; | ||||||
|   secondary?: string; |   secondary?: string; | ||||||
|   show_entity_id?: boolean; |   search_labels?: string[]; | ||||||
|   entity_name?: string; |  | ||||||
|   area_name?: string; |  | ||||||
|   device_name?: string; |  | ||||||
|   friendly_name?: string; |  | ||||||
|   sorting_label?: string; |   sorting_label?: string; | ||||||
|   state?: HassEntity; |   icon_path?: string; | ||||||
|   type?: StatisticItemType; |   type?: StatisticItemType; | ||||||
|   iconPath?: string; |   stateObj?: HassEntity; | ||||||
| } | } | ||||||
|  |  | ||||||
| const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[]; | const MISSING_ID = "___missing-entity___"; | ||||||
|  |  | ||||||
| const ENTITY_ID_STYLE = styleMap({ | const TYPE_ORDER = ["entity", "external", "no_state"] as StatisticItemType[]; | ||||||
|   fontFamily: "var(--code-font-family, monospace)", |  | ||||||
|   fontSize: "11px", |  | ||||||
| }); |  | ||||||
|  |  | ||||||
| @customElement("ha-statistic-combo-box") | @customElement("ha-statistic-combo-box") | ||||||
| export class HaStatisticComboBox extends LitElement { | export class HaStatisticComboBox extends LitElement { | ||||||
| @@ -131,37 +126,39 @@ export class HaStatisticComboBox extends LitElement { | |||||||
|   private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = ( |   private _rowRenderer: ComboBoxLitRenderer<StatisticItem> = ( | ||||||
|     item, |     item, | ||||||
|     { index } |     { index } | ||||||
|   ) => html` |   ) => { | ||||||
|  |     const showEntityId = this.hass.userData?.showEntityIdPicker; | ||||||
|  |     return html` | ||||||
|       <ha-combo-box-item type="button" compact .borderTop=${index !== 0}> |       <ha-combo-box-item type="button" compact .borderTop=${index !== 0}> | ||||||
|       ${!item.state |         ${item.icon_path | ||||||
|           ? html` |           ? html` | ||||||
|               <ha-svg-icon |               <ha-svg-icon | ||||||
|                 style="margin: 0 4px" |                 style="margin: 0 4px" | ||||||
|                 slot="start" |                 slot="start" | ||||||
|               .path=${item.iconPath} |                 .path=${item.icon_path} | ||||||
|               ></ha-svg-icon> |               ></ha-svg-icon> | ||||||
|             ` |             ` | ||||||
|         : html` |           : item.stateObj | ||||||
|  |             ? html` | ||||||
|                 <state-badge |                 <state-badge | ||||||
|                   slot="start" |                   slot="start" | ||||||
|               .stateObj=${item.state} |                   .stateObj=${item.stateObj} | ||||||
|                   .hass=${this.hass} |                   .hass=${this.hass} | ||||||
|                 ></state-badge> |                 ></state-badge> | ||||||
|           `} |               ` | ||||||
|  |             : nothing} | ||||||
|         <span slot="headline">${item.primary} </span> |         <span slot="headline">${item.primary} </span> | ||||||
|         ${item.secondary |         ${item.secondary | ||||||
|           ? html`<span slot="supporting-text">${item.secondary}</span>` |           ? html`<span slot="supporting-text">${item.secondary}</span>` | ||||||
|           : nothing} |           : nothing} | ||||||
|       ${item.id && item.show_entity_id |         ${item.id && showEntityId | ||||||
|         ? html` |           ? html`<span slot="supporting-text" class="code"> | ||||||
|             <span slot="supporting-text" style=${ENTITY_ID_STYLE}> |               ${item.statistic_id} | ||||||
|               ${item.id} |             </span>` | ||||||
|             </span> |  | ||||||
|           ` |  | ||||||
|           : nothing} |           : nothing} | ||||||
|       </ha-combo-box-item> |       </ha-combo-box-item> | ||||||
|     `; |     `; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   private _getItems = memoizeOne( |   private _getItems = memoizeOne( | ||||||
|     ( |     ( | ||||||
| @@ -249,19 +246,22 @@ export class HaStatisticComboBox extends LitElement { | |||||||
|                 label: "", |                 label: "", | ||||||
|                 type, |                 type, | ||||||
|                 sorting_label: label, |                 sorting_label: label, | ||||||
|                 iconPath: mdiShape, |                 search_labels: [label, id], | ||||||
|  |                 icon_path: mdiShape, | ||||||
|               }); |               }); | ||||||
|             } else if (type === "external") { |             } else if (type === "external") { | ||||||
|               const domain = id.split(":")[0]; |               const domain = id.split(":")[0]; | ||||||
|               const domainName = domainToName(this.hass.localize, domain); |               const domainName = domainToName(this.hass.localize, domain); | ||||||
|               output.push({ |               output.push({ | ||||||
|                 id, |                 id, | ||||||
|  |                 statistic_id: id, | ||||||
|                 primary: label, |                 primary: label, | ||||||
|                 secondary: domainName, |                 secondary: domainName, | ||||||
|                 label: "", |                 label: "", | ||||||
|                 type, |                 type, | ||||||
|                 sorting_label: label, |                 sorting_label: label, | ||||||
|                 iconPath: mdiChartLine, |                 search_labels: [label, domainName, id], | ||||||
|  |                 icon_path: mdiChartLine, | ||||||
|               }); |               }); | ||||||
|             } |             } | ||||||
|           } |           } | ||||||
| @@ -283,17 +283,20 @@ export class HaStatisticComboBox extends LitElement { | |||||||
|  |  | ||||||
|         output.push({ |         output.push({ | ||||||
|           id, |           id, | ||||||
|  |           statistic_id: id, | ||||||
|  |           label: "", | ||||||
|           primary, |           primary, | ||||||
|           secondary, |           secondary, | ||||||
|           label: "", |           stateObj: stateObj, | ||||||
|           state: stateObj, |  | ||||||
|           type: "entity", |           type: "entity", | ||||||
|           sorting_label: [deviceName, entityName].join("_"), |           sorting_label: [deviceName, entityName].join("_"), | ||||||
|           entity_name: entityName || deviceName, |           search_labels: [ | ||||||
|           area_name: areaName, |             entityName, | ||||||
|           device_name: deviceName, |             deviceName, | ||||||
|           friendly_name: friendlyName, |             areaName, | ||||||
|           show_entity_id: hass.userData?.showEntityIdPicker, |             friendlyName, | ||||||
|  |             id, | ||||||
|  |           ].filter(Boolean) as string[], | ||||||
|         }); |         }); | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
| @@ -323,11 +326,12 @@ export class HaStatisticComboBox extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       output.push({ |       output.push({ | ||||||
|         id: "__missing", |         id: MISSING_ID, | ||||||
|         primary: this.hass.localize( |         primary: this.hass.localize( | ||||||
|           "ui.components.statistic-picker.missing_entity" |           "ui.components.statistic-picker.missing_entity" | ||||||
|         ), |         ), | ||||||
|         label: "", |         label: "", | ||||||
|  |         icon_path: mdiHelpCircle, | ||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       return output; |       return output; | ||||||
| @@ -422,8 +426,12 @@ export class HaStatisticComboBox extends LitElement { | |||||||
|   private _statisticChanged(ev: ValueChangedEvent<string>) { |   private _statisticChanged(ev: ValueChangedEvent<string>) { | ||||||
|     ev.stopPropagation(); |     ev.stopPropagation(); | ||||||
|     let newValue = ev.detail.value; |     let newValue = ev.detail.value; | ||||||
|     if (newValue === "__missing") { |     if (newValue === MISSING_ID) { | ||||||
|       newValue = ""; |       newValue = ""; | ||||||
|  |       window.open( | ||||||
|  |         documentationUrl(this.hass, this.helpMissingEntityUrl), | ||||||
|  |         "_blank" | ||||||
|  |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (newValue !== this._value) { |     if (newValue !== this._value) { | ||||||
| @@ -436,16 +444,7 @@ export class HaStatisticComboBox extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _fuseIndex = memoizeOne((states: StatisticItem[]) => |   private _fuseIndex = memoizeOne((states: StatisticItem[]) => | ||||||
|     Fuse.createIndex( |     Fuse.createIndex(["search_labels"], states) | ||||||
|       [ |  | ||||||
|         "entity_name", |  | ||||||
|         "device_name", |  | ||||||
|         "area_name", |  | ||||||
|         "friendly_name", // for backwards compatibility |  | ||||||
|         "id", // for technical search |  | ||||||
|       ], |  | ||||||
|       states |  | ||||||
|     ) |  | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private _filterChanged(ev: CustomEvent): void { |   private _filterChanged(ev: CustomEvent): void { | ||||||
|   | |||||||
| @@ -35,6 +35,20 @@ export class HaComboBoxItem extends HaMdListItem { | |||||||
|         width: 32px; |         width: 32px; | ||||||
|         height: 32px; |         height: 32px; | ||||||
|       } |       } | ||||||
|  |       ::slotted(.code) { | ||||||
|  |         font-family: var(--ha-font-family-code); | ||||||
|  |         font-size: var(--ha-font-size-xs); | ||||||
|  |       } | ||||||
|  |       [slot="trailing-supporting-text"] { | ||||||
|  |         font-size: var(--ha-font-size-s); | ||||||
|  |         font-weight: var(--ha-font-weight-normal); | ||||||
|  |         line-height: var(--ha-line-height-normal); | ||||||
|  |         align-self: flex-end; | ||||||
|  |         max-width: 30%; | ||||||
|  |         text-overflow: ellipsis; | ||||||
|  |         overflow: hidden; | ||||||
|  |         white-space: nowrap; | ||||||
|  |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -30,8 +30,9 @@ class HaLabeledSlider extends LitElement { | |||||||
|   @property({ type: Number }) public value?: number; |   @property({ type: Number }) public value?: number; | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|  |     const title = this._getTitle(); | ||||||
|     return html` |     return html` | ||||||
|       <div class="title">${this._getTitle()}</div> |       ${title ? html`<div class="title">${title}</div>` : nothing} | ||||||
|       <div class="extra-container"><slot name="extra"></slot></div> |       <div class="extra-container"><slot name="extra"></slot></div> | ||||||
|       <div class="slider-container"> |       <div class="slider-container"> | ||||||
|         ${this.icon ? html`<ha-icon icon=${this.icon}></ha-icon>` : nothing} |         ${this.icon ? html`<ha-icon icon=${this.icon}></ha-icon>` : nothing} | ||||||
| @@ -73,17 +74,20 @@ class HaLabeledSlider extends LitElement { | |||||||
|  |  | ||||||
|     .slider-container { |     .slider-container { | ||||||
|       display: flex; |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     ha-icon { |     ha-icon { | ||||||
|       margin-top: 8px; |  | ||||||
|       color: var(--secondary-text-color); |       color: var(--secondary-text-color); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     ha-slider { |     ha-slider { | ||||||
|  |       display: flex; | ||||||
|       flex-grow: 1; |       flex-grow: 1; | ||||||
|  |       align-items: center; | ||||||
|       background-image: var(--ha-slider-background); |       background-image: var(--ha-slider-background); | ||||||
|       border-radius: 4px; |       border-radius: 4px; | ||||||
|  |       height: 32px; | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -102,7 +102,7 @@ export class HaLanguagePicker extends LitElement { | |||||||
|       localeChanged |       localeChanged | ||||||
|     ) { |     ) { | ||||||
|       this._select.layoutOptions(); |       this._select.layoutOptions(); | ||||||
|       if (this._select.value !== this.value) { |       if (!this.disabled && this._select.value !== this.value) { | ||||||
|         fireEvent(this, "value-changed", { value: this._select.value }); |         fireEvent(this, "value-changed", { value: this._select.value }); | ||||||
|       } |       } | ||||||
|       if (!this.value) { |       if (!this.value) { | ||||||
| @@ -141,7 +141,10 @@ export class HaLanguagePicker extends LitElement { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const value = |     const value = | ||||||
|       this.value ?? (this.required ? languageOptions[0]?.value : this.value); |       this.value ?? | ||||||
|  |       (this.required && !this.disabled | ||||||
|  |         ? languageOptions[0]?.value | ||||||
|  |         : this.value); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-select |       <ha-select | ||||||
| @@ -182,7 +185,7 @@ export class HaLanguagePicker extends LitElement { | |||||||
|  |  | ||||||
|   private _changed(ev): void { |   private _changed(ev): void { | ||||||
|     const target = ev.target as HaSelect; |     const target = ev.target as HaSelect; | ||||||
|     if (target.value === "" || target.value === this.value) { |     if (this.disabled || target.value === "" || target.value === this.value) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|     this.value = target.value; |     this.value = target.value; | ||||||
|   | |||||||
| @@ -6,6 +6,13 @@ import { customElement } from "lit/decorators"; | |||||||
|  |  | ||||||
| @customElement("ha-outlined-icon-button") | @customElement("ha-outlined-icon-button") | ||||||
| export class HaOutlinedIconButton extends IconButton { | export class HaOutlinedIconButton extends IconButton { | ||||||
|  |   protected override getRenderClasses() { | ||||||
|  |     return { | ||||||
|  |       ...super.getRenderClasses(), | ||||||
|  |       outlined: true, | ||||||
|  |     }; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static override styles = [ |   static override styles = [ | ||||||
|     css` |     css` | ||||||
|       .icon-button { |       .icon-button { | ||||||
|   | |||||||
| @@ -210,6 +210,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   private _unsubPersistentNotifications: UnsubscribeFunc | undefined; |   private _unsubPersistentNotifications: UnsubscribeFunc | undefined; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "sidebarPanelOrder", |     key: "sidebarPanelOrder", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -217,6 +218,7 @@ class HaSidebar extends SubscribeMixin(LitElement) { | |||||||
|   }) |   }) | ||||||
|   private _panelOrder: string[] = []; |   private _panelOrder: string[] = []; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "sidebarHiddenPanels", |     key: "sidebarHiddenPanels", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -850,8 +852,8 @@ class HaSidebar extends SubscribeMixin(LitElement) { | |||||||
|           color: var(--sidebar-icon-color); |           color: var(--sidebar-icon-color); | ||||||
|         } |         } | ||||||
|         .title { |         .title { | ||||||
|           margin-left: 19px; |           margin-left: 3px; | ||||||
|           margin-inline-start: 19px; |           margin-inline-start: 3px; | ||||||
|           margin-inline-end: initial; |           margin-inline-end: initial; | ||||||
|           width: 100%; |           width: 100%; | ||||||
|           display: none; |           display: none; | ||||||
| @@ -938,7 +940,6 @@ class HaSidebar extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|         ha-md-list-item .item-text { |         ha-md-list-item .item-text { | ||||||
|           display: none; |           display: none; | ||||||
|           max-width: calc(100% - 56px); |  | ||||||
|           font-weight: 500; |           font-weight: 500; | ||||||
|           font-size: 14px; |           font-size: 14px; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -24,6 +24,10 @@ export class HaToast extends Snackbar { | |||||||
|         max-width: 650px; |         max-width: 650px; | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       .mdc-snackbar__actions { | ||||||
|  |         color: rgba(255, 255, 255, 0.87); | ||||||
|  |       } | ||||||
|  |  | ||||||
|       /* Revert the default styles set by mwc-snackbar */ |       /* Revert the default styles set by mwc-snackbar */ | ||||||
|       @media (max-width: 480px), (max-width: 344px) { |       @media (max-width: 480px), (max-width: 344px) { | ||||||
|         .mdc-snackbar__surface { |         .mdc-snackbar__surface { | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import "@material/mwc-button/mwc-button"; | |||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { css, html, LitElement, nothing } from "lit"; | import { css, html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import { mdiContentCopy } from "@mdi/js"; | ||||||
| import { storage } from "../../common/decorators/storage"; | import { storage } from "../../common/decorators/storage"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import type { | import type { | ||||||
| @@ -17,6 +18,8 @@ import "../ha-language-picker"; | |||||||
| import "../ha-tts-voice-picker"; | import "../ha-tts-voice-picker"; | ||||||
| import "../ha-card"; | import "../ha-card"; | ||||||
| import { fetchCloudStatus } from "../../data/cloud"; | import { fetchCloudStatus } from "../../data/cloud"; | ||||||
|  | import { copyToClipboard } from "../../common/util/copy-clipboard"; | ||||||
|  | import { showToast } from "../../util/toast"; | ||||||
|  |  | ||||||
| export interface TtsMediaPickedEvent { | export interface TtsMediaPickedEvent { | ||||||
|   item: MediaPlayerItem; |   item: MediaPlayerItem; | ||||||
| @@ -42,6 +45,7 @@ class BrowseMediaTTS extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _provider?: TTSEngine; |   @state() private _provider?: TTSEngine; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "TtsMessage", |     key: "TtsMessage", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -50,7 +54,8 @@ class BrowseMediaTTS extends LitElement { | |||||||
|   private _message?: string; |   private _message?: string; | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     return html`<ha-card> |     return html` | ||||||
|  |       <ha-card> | ||||||
|         <div class="card-content"> |         <div class="card-content"> | ||||||
|           <ha-textarea |           <ha-textarea | ||||||
|             autogrow |             autogrow | ||||||
| @@ -93,7 +98,25 @@ class BrowseMediaTTS extends LitElement { | |||||||
|             )} |             )} | ||||||
|           </mwc-button> |           </mwc-button> | ||||||
|         </div> |         </div> | ||||||
|     </ha-card> `; |       </ha-card> | ||||||
|  |       ${this._voice | ||||||
|  |         ? html` | ||||||
|  |             <div class="footer"> | ||||||
|  |               ${this.hass.localize( | ||||||
|  |                 `ui.components.media-browser.tts.selected_voice_id` | ||||||
|  |               )} | ||||||
|  |               <code>${this._voice || "-"}</code> | ||||||
|  |               <ha-icon-button | ||||||
|  |                 .path=${mdiContentCopy} | ||||||
|  |                 @click=${this._copyVoiceId} | ||||||
|  |                 title=${this.hass.localize( | ||||||
|  |                   "ui.components.media-browser.tts.copy_voice_id" | ||||||
|  |                 )} | ||||||
|  |               ></ha-icon-button> | ||||||
|  |             </div> | ||||||
|  |           ` | ||||||
|  |         : nothing} | ||||||
|  |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected override willUpdate(changedProps: PropertyValues): void { |   protected override willUpdate(changedProps: PropertyValues): void { | ||||||
| @@ -196,6 +219,14 @@ class BrowseMediaTTS extends LitElement { | |||||||
|     fireEvent(this, "tts-picked", { item }); |     fireEvent(this, "tts-picked", { item }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async _copyVoiceId(ev) { | ||||||
|  |     ev.preventDefault(); | ||||||
|  |     await copyToClipboard(this._voice); | ||||||
|  |     showToast(this, { | ||||||
|  |       message: this.hass.localize("ui.common.copied_clipboard"), | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static override styles = [ |   static override styles = [ | ||||||
|     buttonLinkStyle, |     buttonLinkStyle, | ||||||
|     css` |     css` | ||||||
| @@ -217,6 +248,23 @@ class BrowseMediaTTS extends LitElement { | |||||||
|       button.link { |       button.link { | ||||||
|         color: var(--primary-color); |         color: var(--primary-color); | ||||||
|       } |       } | ||||||
|  |       .footer { | ||||||
|  |         font-size: var(--ha-font-size-s); | ||||||
|  |         color: var(--secondary-text-color); | ||||||
|  |         margin: 16px 0; | ||||||
|  |         text-align: center; | ||||||
|  |       } | ||||||
|  |       .footer code { | ||||||
|  |         font-weight: var(--ha-font-weight-bold); | ||||||
|  |       } | ||||||
|  |       .footer { | ||||||
|  |         --mdc-icon-size: 14px; | ||||||
|  |         --mdc-icon-button-size: 24px; | ||||||
|  |         display: flex; | ||||||
|  |         justify-content: center; | ||||||
|  |         align-items: center; | ||||||
|  |         gap: 6px; | ||||||
|  |       } | ||||||
|     `, |     `, | ||||||
|   ]; |   ]; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -890,12 +890,18 @@ export class HaMediaPlayerBrowse extends LitElement { | |||||||
|           display: flex; |           display: flex; | ||||||
|           flex-direction: row-reverse; |           flex-direction: row-reverse; | ||||||
|           margin-right: 48px; |           margin-right: 48px; | ||||||
|  |           margin-inline-end: 48px; | ||||||
|  |           margin-inline-start: initial; | ||||||
|  |           direction: var(--direction); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .highlight-add-button ha-svg-icon { |         .highlight-add-button ha-svg-icon { | ||||||
|           position: relative; |           position: relative; | ||||||
|           top: -0.5em; |           top: -0.5em; | ||||||
|           margin-left: 8px; |           margin-left: 8px; | ||||||
|  |           margin-inline-start: 8px; | ||||||
|  |           margin-inline-end: initial; | ||||||
|  |           transform: scaleX(var(--scale-direction)); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .content { |         .content { | ||||||
|   | |||||||
| @@ -85,7 +85,6 @@ class SearchInputOutlined extends LitElement { | |||||||
|       display: inline-flex; |       display: inline-flex; | ||||||
|       /* For iOS */ |       /* For iOS */ | ||||||
|       z-index: 0; |       z-index: 0; | ||||||
|       --mdc-icon-button-size: 24px; |  | ||||||
|     } |     } | ||||||
|     ha-outlined-text-field { |     ha-outlined-text-field { | ||||||
|       display: block; |       display: block; | ||||||
| @@ -94,6 +93,8 @@ class SearchInputOutlined extends LitElement { | |||||||
|     } |     } | ||||||
|     ha-svg-icon, |     ha-svg-icon, | ||||||
|     ha-icon-button { |     ha-icon-button { | ||||||
|  |       --mdc-icon-button-size: 24px; | ||||||
|  |       height: var(--mdc-icon-button-size); | ||||||
|       display: flex; |       display: flex; | ||||||
|       color: var(--primary-text-color); |       color: var(--primary-text-color); | ||||||
|     } |     } | ||||||
|   | |||||||
| @@ -11,8 +11,8 @@ export class HaTimeline extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean, reflect: true }) public raised = false; |   @property({ type: Boolean, reflect: true }) public raised = false; | ||||||
|  |  | ||||||
|   @property({ attribute: false, reflect: true, type: Boolean }) notEnabled = |   @property({ attribute: "not-enabled", reflect: true, type: Boolean }) | ||||||
|     false; |   notEnabled = false; | ||||||
|  |  | ||||||
|   @property({ attribute: "last-item", type: Boolean }) public lastItem = false; |   @property({ attribute: "last-item", type: Boolean }) public lastItem = false; | ||||||
|  |  | ||||||
| @@ -82,7 +82,7 @@ export class HaTimeline extends LitElement { | |||||||
|           margin-inline-start: initial; |           margin-inline-start: initial; | ||||||
|           width: 24px; |           width: 24px; | ||||||
|         } |         } | ||||||
|         :host([notEnabled]) ha-svg-icon { |         :host([not-enabled]) ha-svg-icon { | ||||||
|           opacity: 0.5; |           opacity: 0.5; | ||||||
|         } |         } | ||||||
|         ha-svg-icon { |         ha-svg-icon { | ||||||
|   | |||||||
| @@ -17,8 +17,8 @@ export class HatGraphNode extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) public error = false; |   @property({ type: Boolean }) public error = false; | ||||||
|  |  | ||||||
|   @property({ attribute: false, reflect: true, type: Boolean }) notEnabled = |   @property({ attribute: "not-enabled", reflect: true, type: Boolean }) | ||||||
|     false; |   notEnabled = false; | ||||||
|  |  | ||||||
|   @property({ attribute: "graph-start", reflect: true, type: Boolean }) |   @property({ attribute: "graph-start", reflect: true, type: Boolean }) | ||||||
|   graphStart = false; |   graphStart = false; | ||||||
| @@ -127,13 +127,13 @@ export class HatGraphNode extends LitElement { | |||||||
|       --stroke-clr: var(--hover-clr); |       --stroke-clr: var(--hover-clr); | ||||||
|       --icon-clr: var(--default-icon-clr); |       --icon-clr: var(--default-icon-clr); | ||||||
|     } |     } | ||||||
|     :host([notEnabled]) circle { |     :host([not-enabled]) circle { | ||||||
|       --stroke-clr: var(--disabled-clr); |       --stroke-clr: var(--disabled-clr); | ||||||
|     } |     } | ||||||
|     :host([notEnabled][active]) circle { |     :host([not-enabled][active]) circle { | ||||||
|       --stroke-clr: var(--disabled-active-clr); |       --stroke-clr: var(--disabled-active-clr); | ||||||
|     } |     } | ||||||
|     :host([notEnabled]:hover) circle { |     :host([not-enabled]:hover) circle { | ||||||
|       --stroke-clr: var(--disabled-hover-clr); |       --stroke-clr: var(--disabled-hover-clr); | ||||||
|     } |     } | ||||||
|     svg:not(.safari) { |     svg:not(.safari) { | ||||||
|   | |||||||
| @@ -492,6 +492,25 @@ export const getAutomationEditorInitData = () => { | |||||||
|   return data; |   return data; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const isTrigger = (config: unknown): boolean => { | ||||||
|  |   if (!config || typeof config !== "object") { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   const trigger = config as Record<string, unknown>; | ||||||
|  |   return ( | ||||||
|  |     ("trigger" in trigger && typeof trigger.trigger === "string") || | ||||||
|  |     ("platform" in trigger && typeof trigger.platform === "string") | ||||||
|  |   ); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const isCondition = (config: unknown): boolean => { | ||||||
|  |   if (!config || typeof config !== "object") { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   const condition = config as Record<string, unknown>; | ||||||
|  |   return "condition" in condition && typeof condition.condition === "string"; | ||||||
|  | }; | ||||||
|  |  | ||||||
| export const subscribeTrigger = ( | export const subscribeTrigger = ( | ||||||
|   hass: HomeAssistant, |   hass: HomeAssistant, | ||||||
|   onChange: (result: { |   onChange: (result: { | ||||||
|   | |||||||
| @@ -1,4 +1,4 @@ | |||||||
| import type { HassConfig } from "home-assistant-js-websocket"; | import type { HassConfig, HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { ensureArray } from "../common/array/ensure-array"; | import { ensureArray } from "../common/array/ensure-array"; | ||||||
| import { | import { | ||||||
|   formatDurationLong, |   formatDurationLong, | ||||||
| @@ -155,7 +155,7 @@ const tryDescribeTrigger = ( | |||||||
|  |  | ||||||
|     const stateObj = Array.isArray(trigger.entity_id) |     const stateObj = Array.isArray(trigger.entity_id) | ||||||
|       ? hass.states[trigger.entity_id[0]] |       ? hass.states[trigger.entity_id[0]] | ||||||
|       : hass.states[trigger.entity_id]; |       : (hass.states[trigger.entity_id] as HassEntity | undefined); | ||||||
|  |  | ||||||
|     if (Array.isArray(trigger.entity_id)) { |     if (Array.isArray(trigger.entity_id)) { | ||||||
|       for (const entity of trigger.entity_id.values()) { |       for (const entity of trigger.entity_id.values()) { | ||||||
| @@ -172,12 +172,14 @@ const tryDescribeTrigger = ( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const attribute = trigger.attribute |     const attribute = trigger.attribute | ||||||
|  |       ? stateObj | ||||||
|         ? computeAttributeNameDisplay( |         ? computeAttributeNameDisplay( | ||||||
|             hass.localize, |             hass.localize, | ||||||
|             stateObj, |             stateObj, | ||||||
|             hass.entities, |             hass.entities, | ||||||
|             trigger.attribute |             trigger.attribute | ||||||
|           ) |           ) | ||||||
|  |         : trigger.attribute | ||||||
|       : undefined; |       : undefined; | ||||||
|  |  | ||||||
|     const duration = trigger.for |     const duration = trigger.for | ||||||
| @@ -232,13 +234,15 @@ const tryDescribeTrigger = ( | |||||||
|     if (trigger.attribute) { |     if (trigger.attribute) { | ||||||
|       const stateObj = Array.isArray(trigger.entity_id) |       const stateObj = Array.isArray(trigger.entity_id) | ||||||
|         ? hass.states[trigger.entity_id[0]] |         ? hass.states[trigger.entity_id[0]] | ||||||
|         : hass.states[trigger.entity_id]; |         : (hass.states[trigger.entity_id] as HassEntity | undefined); | ||||||
|       attribute = computeAttributeNameDisplay( |       attribute = stateObj | ||||||
|  |         ? computeAttributeNameDisplay( | ||||||
|             hass.localize, |             hass.localize, | ||||||
|             stateObj, |             stateObj, | ||||||
|             hass.entities, |             hass.entities, | ||||||
|             trigger.attribute |             trigger.attribute | ||||||
|       ); |           ) | ||||||
|  |         : trigger.attribute; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const entityArray: string[] = ensureArray(trigger.entity_id); |     const entityArray: string[] = ensureArray(trigger.entity_id); | ||||||
| @@ -250,7 +254,7 @@ const tryDescribeTrigger = ( | |||||||
|       } |       } | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const stateObj = hass.states[entityArray[0]]; |     const stateObj = hass.states[entityArray[0]] as HassEntity | undefined; | ||||||
|  |  | ||||||
|     let fromChoice = "other"; |     let fromChoice = "other"; | ||||||
|     let fromString = ""; |     let fromString = ""; | ||||||
| @@ -266,7 +270,8 @@ const tryDescribeTrigger = ( | |||||||
|         const from: string[] = []; |         const from: string[] = []; | ||||||
|         for (const state of fromArray) { |         for (const state of fromArray) { | ||||||
|           from.push( |           from.push( | ||||||
|             trigger.attribute |             stateObj | ||||||
|  |               ? trigger.attribute | ||||||
|                 ? hass |                 ? hass | ||||||
|                     .formatEntityAttributeValue( |                     .formatEntityAttributeValue( | ||||||
|                       stateObj, |                       stateObj, | ||||||
| @@ -275,6 +280,7 @@ const tryDescribeTrigger = ( | |||||||
|                     ) |                     ) | ||||||
|                     .toString() |                     .toString() | ||||||
|                 : hass.formatEntityState(stateObj, state) |                 : hass.formatEntityState(stateObj, state) | ||||||
|  |               : state | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|         if (from.length !== 0) { |         if (from.length !== 0) { | ||||||
| @@ -298,7 +304,8 @@ const tryDescribeTrigger = ( | |||||||
|         const to: string[] = []; |         const to: string[] = []; | ||||||
|         for (const state of toArray) { |         for (const state of toArray) { | ||||||
|           to.push( |           to.push( | ||||||
|             trigger.attribute |             stateObj | ||||||
|  |               ? trigger.attribute | ||||||
|                 ? hass |                 ? hass | ||||||
|                     .formatEntityAttributeValue( |                     .formatEntityAttributeValue( | ||||||
|                       stateObj, |                       stateObj, | ||||||
| @@ -307,6 +314,7 @@ const tryDescribeTrigger = ( | |||||||
|                     ) |                     ) | ||||||
|                     .toString() |                     .toString() | ||||||
|                 : hass.formatEntityState(stateObj, state).toString() |                 : hass.formatEntityState(stateObj, state).toString() | ||||||
|  |               : state | ||||||
|           ); |           ); | ||||||
|         } |         } | ||||||
|         if (to.length !== 0) { |         if (to.length !== 0) { | ||||||
| @@ -725,7 +733,9 @@ const tryDescribeTrigger = ( | |||||||
|     if (localized) { |     if (localized) { | ||||||
|       return localized; |       return localized; | ||||||
|     } |     } | ||||||
|     const stateObj = hass.states[config.entity_id as string]; |     const stateObj = hass.states[config.entity_id as string] as | ||||||
|  |       | HassEntity | ||||||
|  |       | undefined; | ||||||
|     return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${ |     return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${ | ||||||
|       config.type |       config.type | ||||||
|     }`; |     }`; | ||||||
| @@ -894,13 +904,15 @@ const tryDescribeCondition = ( | |||||||
|     if (condition.attribute) { |     if (condition.attribute) { | ||||||
|       const stateObj = Array.isArray(condition.entity_id) |       const stateObj = Array.isArray(condition.entity_id) | ||||||
|         ? hass.states[condition.entity_id[0]] |         ? hass.states[condition.entity_id[0]] | ||||||
|         : hass.states[condition.entity_id]; |         : (hass.states[condition.entity_id] as HassEntity | undefined); | ||||||
|       attribute = computeAttributeNameDisplay( |       attribute = stateObj | ||||||
|  |         ? computeAttributeNameDisplay( | ||||||
|             hass.localize, |             hass.localize, | ||||||
|             stateObj, |             stateObj, | ||||||
|             hass.entities, |             hass.entities, | ||||||
|             condition.attribute |             condition.attribute | ||||||
|       ); |           ) | ||||||
|  |         : condition.attribute; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const entities: string[] = []; |     const entities: string[] = []; | ||||||
| @@ -919,16 +931,16 @@ const tryDescribeCondition = ( | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const states: string[] = []; |     const states: string[] = []; | ||||||
|     const stateObj = |     const stateObj = hass.states[ | ||||||
|       hass.states[ |  | ||||||
|       Array.isArray(condition.entity_id) |       Array.isArray(condition.entity_id) | ||||||
|         ? condition.entity_id[0] |         ? condition.entity_id[0] | ||||||
|         : condition.entity_id |         : condition.entity_id | ||||||
|       ]; |     ] as HassEntity | undefined; | ||||||
|     if (Array.isArray(condition.state)) { |     if (Array.isArray(condition.state)) { | ||||||
|       for (const state of condition.state.values()) { |       for (const state of condition.state.values()) { | ||||||
|         states.push( |         states.push( | ||||||
|           condition.attribute |           stateObj | ||||||
|  |             ? condition.attribute | ||||||
|               ? hass |               ? hass | ||||||
|                   .formatEntityAttributeValue( |                   .formatEntityAttributeValue( | ||||||
|                     stateObj, |                     stateObj, | ||||||
| @@ -937,11 +949,13 @@ const tryDescribeCondition = ( | |||||||
|                   ) |                   ) | ||||||
|                   .toString() |                   .toString() | ||||||
|               : hass.formatEntityState(stateObj, state) |               : hass.formatEntityState(stateObj, state) | ||||||
|  |             : state | ||||||
|         ); |         ); | ||||||
|       } |       } | ||||||
|     } else if (condition.state !== "") { |     } else if (condition.state !== "") { | ||||||
|       states.push( |       states.push( | ||||||
|         condition.attribute |         stateObj | ||||||
|  |           ? condition.attribute | ||||||
|             ? hass |             ? hass | ||||||
|                 .formatEntityAttributeValue( |                 .formatEntityAttributeValue( | ||||||
|                   stateObj, |                   stateObj, | ||||||
| @@ -950,6 +964,7 @@ const tryDescribeCondition = ( | |||||||
|                 ) |                 ) | ||||||
|                 .toString() |                 .toString() | ||||||
|             : hass.formatEntityState(stateObj, condition.state.toString()) |             : hass.formatEntityState(stateObj, condition.state.toString()) | ||||||
|  |           : condition.state.toString() | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -979,7 +994,7 @@ const tryDescribeCondition = ( | |||||||
|   // Numeric State Condition |   // Numeric State Condition | ||||||
|   if (condition.condition === "numeric_state" && condition.entity_id) { |   if (condition.condition === "numeric_state" && condition.entity_id) { | ||||||
|     const entity_ids = ensureArray(condition.entity_id); |     const entity_ids = ensureArray(condition.entity_id); | ||||||
|     const stateObj = hass.states[entity_ids[0]]; |     const stateObj = hass.states[entity_ids[0]] as HassEntity | undefined; | ||||||
|     const entity = formatListWithAnds( |     const entity = formatListWithAnds( | ||||||
|       hass.locale, |       hass.locale, | ||||||
|       entity_ids.map((id) => |       entity_ids.map((id) => | ||||||
| @@ -988,12 +1003,14 @@ const tryDescribeCondition = ( | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const attribute = condition.attribute |     const attribute = condition.attribute | ||||||
|  |       ? stateObj | ||||||
|         ? computeAttributeNameDisplay( |         ? computeAttributeNameDisplay( | ||||||
|             hass.localize, |             hass.localize, | ||||||
|             stateObj, |             stateObj, | ||||||
|             hass.entities, |             hass.entities, | ||||||
|             condition.attribute |             condition.attribute | ||||||
|           ) |           ) | ||||||
|  |         : condition.attribute | ||||||
|       : undefined; |       : undefined; | ||||||
|  |  | ||||||
|     if (condition.above !== undefined && condition.below !== undefined) { |     if (condition.above !== undefined && condition.below !== undefined) { | ||||||
| @@ -1187,7 +1204,9 @@ const tryDescribeCondition = ( | |||||||
|     if (localized) { |     if (localized) { | ||||||
|       return localized; |       return localized; | ||||||
|     } |     } | ||||||
|     const stateObj = hass.states[config.entity_id as string]; |     const stateObj = hass.states[config.entity_id as string] as | ||||||
|  |       | HassEntity | ||||||
|  |       | undefined; | ||||||
|     return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${ |     return `${stateObj ? computeStateName(stateObj) : config.entity_id} ${ | ||||||
|       config.type |       config.type | ||||||
|     }`; |     }`; | ||||||
|   | |||||||
| @@ -959,21 +959,13 @@ const computeConsumptionDataPartial = ( | |||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   data.timestamps.forEach((t) => { |   data.timestamps.forEach((t) => { | ||||||
|     const used_total = |  | ||||||
|       (data.from_grid?.[t] || 0) + |  | ||||||
|       (data.solar?.[t] || 0) + |  | ||||||
|       (data.from_battery?.[t] || 0) - |  | ||||||
|       (data.to_grid?.[t] || 0) - |  | ||||||
|       (data.to_battery?.[t] || 0); |  | ||||||
|  |  | ||||||
|     outData.used_total[t] = used_total; |  | ||||||
|     outData.total.used_total += used_total; |  | ||||||
|     const { |     const { | ||||||
|       grid_to_battery, |       grid_to_battery, | ||||||
|       battery_to_grid, |       battery_to_grid, | ||||||
|       used_solar, |       used_solar, | ||||||
|       used_grid, |       used_grid, | ||||||
|       used_battery, |       used_battery, | ||||||
|  |       used_total, | ||||||
|       solar_to_battery, |       solar_to_battery, | ||||||
|       solar_to_grid, |       solar_to_grid, | ||||||
|     } = computeConsumptionSingle({ |     } = computeConsumptionSingle({ | ||||||
| @@ -984,6 +976,8 @@ const computeConsumptionDataPartial = ( | |||||||
|       from_battery: data.from_battery && (data.from_battery[t] ?? 0), |       from_battery: data.from_battery && (data.from_battery[t] ?? 0), | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|  |     outData.used_total[t] = used_total; | ||||||
|  |     outData.total.used_total += used_total; | ||||||
|     outData.grid_to_battery[t] = grid_to_battery; |     outData.grid_to_battery[t] = grid_to_battery; | ||||||
|     outData.total.grid_to_battery += grid_to_battery; |     outData.total.grid_to_battery += grid_to_battery; | ||||||
|     outData.battery_to_grid![t] = battery_to_grid; |     outData.battery_to_grid![t] = battery_to_grid; | ||||||
| @@ -1017,12 +1011,20 @@ export const computeConsumptionSingle = (data: { | |||||||
|   used_solar: number; |   used_solar: number; | ||||||
|   used_grid: number; |   used_grid: number; | ||||||
|   used_battery: number; |   used_battery: number; | ||||||
|  |   used_total: number; | ||||||
| } => { | } => { | ||||||
|   const to_grid = data.to_grid; |   let to_grid = Math.max(data.to_grid || 0, 0); | ||||||
|   const to_battery = data.to_battery; |   let to_battery = Math.max(data.to_battery || 0, 0); | ||||||
|   const solar = data.solar; |   let solar = Math.max(data.solar || 0, 0); | ||||||
|   const from_grid = data.from_grid; |   let from_grid = Math.max(data.from_grid || 0, 0); | ||||||
|   const from_battery = data.from_battery; |   let from_battery = Math.max(data.from_battery || 0, 0); | ||||||
|  |  | ||||||
|  |   const used_total = | ||||||
|  |     (from_grid || 0) + | ||||||
|  |     (solar || 0) + | ||||||
|  |     (from_battery || 0) - | ||||||
|  |     (to_grid || 0) - | ||||||
|  |     (to_battery || 0); | ||||||
|  |  | ||||||
|   let used_solar = 0; |   let used_solar = 0; | ||||||
|   let grid_to_battery = 0; |   let grid_to_battery = 0; | ||||||
| @@ -1031,41 +1033,57 @@ export const computeConsumptionSingle = (data: { | |||||||
|   let solar_to_grid = 0; |   let solar_to_grid = 0; | ||||||
|   let used_battery = 0; |   let used_battery = 0; | ||||||
|   let used_grid = 0; |   let used_grid = 0; | ||||||
|   if ((to_grid != null || to_battery != null) && solar != null) { |  | ||||||
|     used_solar = (solar || 0) - (to_grid || 0) - (to_battery || 0); |  | ||||||
|     if (used_solar < 0) { |  | ||||||
|       if (to_battery != null) { |  | ||||||
|         grid_to_battery = used_solar * -1; |  | ||||||
|         if (grid_to_battery > (from_grid || 0)) { |  | ||||||
|           battery_to_grid = grid_to_battery - (from_grid || 0); |  | ||||||
|           grid_to_battery = from_grid || 0; |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       used_solar = 0; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (from_battery != null) { |   let used_total_remaining = Math.max(used_total, 0); | ||||||
|     used_battery = (from_battery || 0) - battery_to_grid; |   // Consumption Priority | ||||||
|   } |   // Battery_Out -> Grid_Out | ||||||
|  |   // Solar -> Grid_Out | ||||||
|  |   // Solar -> Battery_In | ||||||
|  |   // Grid_In -> Battery_In | ||||||
|  |   // Solar -> Consumption | ||||||
|  |   // Battery_Out -> Consumption | ||||||
|  |   // Grid_In -> Consumption | ||||||
|  |  | ||||||
|   if (from_grid != null) { |   // Battery_Out -> Grid_Out | ||||||
|     used_grid = from_grid - grid_to_battery; |   battery_to_grid = Math.min(from_battery, to_grid); | ||||||
|   } |   from_battery -= battery_to_grid; | ||||||
|  |   to_grid -= battery_to_grid; | ||||||
|  |  | ||||||
|   if (solar != null) { |   // Solar -> Grid_Out | ||||||
|     if (to_battery != null) { |   solar_to_grid = Math.min(solar, to_grid); | ||||||
|       solar_to_battery = Math.max(0, (to_battery || 0) - grid_to_battery); |   to_grid -= solar_to_grid; | ||||||
|     } |   solar -= solar_to_grid; | ||||||
|     if (to_grid != null) { |  | ||||||
|       solar_to_grid = Math.max(0, (to_grid || 0) - battery_to_grid); |   // Solar -> Battery_In | ||||||
|     } |   solar_to_battery = Math.min(solar, to_battery); | ||||||
|   } |   to_battery -= solar_to_battery; | ||||||
|  |   solar -= solar_to_battery; | ||||||
|  |  | ||||||
|  |   // Grid_In -> Battery_In | ||||||
|  |   grid_to_battery = Math.min(from_grid, to_battery); | ||||||
|  |   from_grid -= grid_to_battery; | ||||||
|  |   to_battery -= grid_to_battery; | ||||||
|  |  | ||||||
|  |   // Solar -> Consumption | ||||||
|  |   used_solar = Math.min(used_total_remaining, solar); | ||||||
|  |   used_total_remaining -= used_solar; | ||||||
|  |   solar -= used_solar; | ||||||
|  |  | ||||||
|  |   // Battery_Out -> Consumption | ||||||
|  |   used_battery = Math.min(from_battery, used_total_remaining); | ||||||
|  |   from_battery -= used_battery; | ||||||
|  |   used_total_remaining -= used_battery; | ||||||
|  |  | ||||||
|  |   // Grid_In -> Consumption | ||||||
|  |   used_grid = Math.min(used_total_remaining, from_grid); | ||||||
|  |   from_grid -= used_grid; | ||||||
|  |   used_total_remaining -= from_grid; | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     used_solar, |     used_solar, | ||||||
|     used_grid, |     used_grid, | ||||||
|     used_battery, |     used_battery, | ||||||
|  |     used_total, | ||||||
|     grid_to_battery, |     grid_to_battery, | ||||||
|     battery_to_grid, |     battery_to_grid, | ||||||
|     solar_to_battery, |     solar_to_battery, | ||||||
|   | |||||||
| @@ -107,3 +107,70 @@ export const DOMAIN_ATTRIBUTES_FORMATERS: Record< | |||||||
|     }, |     }, | ||||||
|   }, |   }, | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export const NON_NUMERIC_ATTRIBUTES = [ | ||||||
|  |   "access_token", | ||||||
|  |   "auto_update", | ||||||
|  |   "available_modes", | ||||||
|  |   "away_mode", | ||||||
|  |   "changed_by", | ||||||
|  |   "code_format", | ||||||
|  |   "color_modes", | ||||||
|  |   "current_activity", | ||||||
|  |   "device_class", | ||||||
|  |   "editable", | ||||||
|  |   "effect_list", | ||||||
|  |   "effect", | ||||||
|  |   "entity_picture", | ||||||
|  |   "event_type", | ||||||
|  |   "event_types", | ||||||
|  |   "fan_mode", | ||||||
|  |   "fan_modes", | ||||||
|  |   "fan_speed_list", | ||||||
|  |   "forecast", | ||||||
|  |   "friendly_name", | ||||||
|  |   "frontend_stream_type", | ||||||
|  |   "has_date", | ||||||
|  |   "has_time", | ||||||
|  |   "hs_color", | ||||||
|  |   "hvac_mode", | ||||||
|  |   "hvac_modes", | ||||||
|  |   "icon", | ||||||
|  |   "media_album_name", | ||||||
|  |   "media_artist", | ||||||
|  |   "media_content_type", | ||||||
|  |   "media_position_updated_at", | ||||||
|  |   "media_title", | ||||||
|  |   "next_dawn", | ||||||
|  |   "next_dusk", | ||||||
|  |   "next_midnight", | ||||||
|  |   "next_noon", | ||||||
|  |   "next_rising", | ||||||
|  |   "next_setting", | ||||||
|  |   "operation_list", | ||||||
|  |   "operation_mode", | ||||||
|  |   "options", | ||||||
|  |   "preset_mode", | ||||||
|  |   "preset_modes", | ||||||
|  |   "release_notes", | ||||||
|  |   "release_summary", | ||||||
|  |   "release_url", | ||||||
|  |   "restored", | ||||||
|  |   "rgb_color", | ||||||
|  |   "rgbw_color", | ||||||
|  |   "shuffle", | ||||||
|  |   "sound_mode_list", | ||||||
|  |   "sound_mode", | ||||||
|  |   "source_list", | ||||||
|  |   "source_type", | ||||||
|  |   "source", | ||||||
|  |   "state_class", | ||||||
|  |   "supported_features", | ||||||
|  |   "swing_mode", | ||||||
|  |   "swing_mode", | ||||||
|  |   "swing_modes", | ||||||
|  |   "title", | ||||||
|  |   "token", | ||||||
|  |   "unit_of_measurement", | ||||||
|  |   "xy_color", | ||||||
|  | ]; | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import "@material/mwc-button"; |  | ||||||
| import { mdiClose, mdiHelpCircle } from "@mdi/js"; | import { mdiClose, mdiHelpCircle } from "@mdi/js"; | ||||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import type { CSSResultGroup, PropertyValues } from "lit"; | import type { CSSResultGroup, PropertyValues } from "lit"; | ||||||
| @@ -177,6 +176,17 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const showDocumentationLink = | ||||||
|  |       ([ | ||||||
|  |         "form", | ||||||
|  |         "menu", | ||||||
|  |         "external", | ||||||
|  |         "progress", | ||||||
|  |         "data_entry_flow_progressed", | ||||||
|  |       ].includes(this._step?.type as any) && | ||||||
|  |         this._params.manifest?.is_built_in) || | ||||||
|  |       !!this._params.manifest?.documentation; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-dialog | ||||||
|         open |         open | ||||||
| @@ -191,7 +201,7 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|                 <step-flow-loading |                 <step-flow-loading | ||||||
|                   .flowConfig=${this._params.flowConfig} |                   .flowConfig=${this._params.flowConfig} | ||||||
|                   .hass=${this.hass} |                   .hass=${this.hass} | ||||||
|                   .loadingReason=${this._loading} |                   .loadingReason=${this._loading!} | ||||||
|                   .handler=${this._handler} |                   .handler=${this._handler} | ||||||
|                   .step=${this._step} |                   .step=${this._step} | ||||||
|                 ></step-flow-loading> |                 ></step-flow-loading> | ||||||
| @@ -199,26 +209,18 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|             : this._step === undefined |             : this._step === undefined | ||||||
|               ? // When we are going to next step, we render 1 round of empty |               ? // When we are going to next step, we render 1 round of empty | ||||||
|                 // to reset the element. |                 // to reset the element. | ||||||
|                 "" |                 nothing | ||||||
|               : html` |               : html` | ||||||
|                   <div class="dialog-actions"> |                   <div class="dialog-actions"> | ||||||
|                     ${([ |                     ${showDocumentationLink | ||||||
|                       "form", |  | ||||||
|                       "menu", |  | ||||||
|                       "external", |  | ||||||
|                       "progress", |  | ||||||
|                       "data_entry_flow_progressed", |  | ||||||
|                     ].includes(this._step?.type as any) && |  | ||||||
|                       this._params.manifest?.is_built_in) || |  | ||||||
|                     this._params.manifest?.documentation |  | ||||||
|                       ? html` |                       ? html` | ||||||
|                           <a |                           <a | ||||||
|                             href=${this._params.manifest.is_built_in |                             href=${this._params.manifest!.is_built_in | ||||||
|                               ? documentationUrl( |                               ? documentationUrl( | ||||||
|                                   this.hass, |                                   this.hass, | ||||||
|                                   `/integrations/${this._params.manifest.domain}` |                                   `/integrations/${this._params.manifest!.domain}` | ||||||
|                                 ) |                                 ) | ||||||
|                               : this._params?.manifest?.documentation} |                               : this._params.manifest!.documentation} | ||||||
|                             target="_blank" |                             target="_blank" | ||||||
|                             rel="noreferrer noopener" |                             rel="noreferrer noopener" | ||||||
|                           > |                           > | ||||||
| @@ -229,7 +231,7 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|                             </ha-icon-button |                             </ha-icon-button | ||||||
|                           ></a> |                           ></a> | ||||||
|                         ` |                         ` | ||||||
|                       : ""} |                       : nothing} | ||||||
|                     <ha-icon-button |                     <ha-icon-button | ||||||
|                       .label=${this.hass.localize("ui.common.close")} |                       .label=${this.hass.localize("ui.common.close")} | ||||||
|                       .path=${mdiClose} |                       .path=${mdiClose} | ||||||
| @@ -242,6 +244,7 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|                           .flowConfig=${this._params.flowConfig} |                           .flowConfig=${this._params.flowConfig} | ||||||
|                           .step=${this._step} |                           .step=${this._step} | ||||||
|                           .hass=${this.hass} |                           .hass=${this.hass} | ||||||
|  |                           .increasePaddingEnd=${showDocumentationLink} | ||||||
|                         ></step-flow-form> |                         ></step-flow-form> | ||||||
|                       ` |                       ` | ||||||
|                     : this._step.type === "external" |                     : this._step.type === "external" | ||||||
| @@ -250,6 +253,7 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|                             .flowConfig=${this._params.flowConfig} |                             .flowConfig=${this._params.flowConfig} | ||||||
|                             .step=${this._step} |                             .step=${this._step} | ||||||
|                             .hass=${this.hass} |                             .hass=${this.hass} | ||||||
|  |                             .increasePaddingEnd=${showDocumentationLink} | ||||||
|                           ></step-flow-external> |                           ></step-flow-external> | ||||||
|                         ` |                         ` | ||||||
|                       : this._step.type === "abort" |                       : this._step.type === "abort" | ||||||
| @@ -261,6 +265,7 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|                               .handler=${this._step.handler} |                               .handler=${this._step.handler} | ||||||
|                               .domain=${this._params.domain ?? |                               .domain=${this._params.domain ?? | ||||||
|                               this._step.handler} |                               this._step.handler} | ||||||
|  |                               .increasePaddingEnd=${showDocumentationLink} | ||||||
|                             ></step-flow-abort> |                             ></step-flow-abort> | ||||||
|                           ` |                           ` | ||||||
|                         : this._step.type === "progress" |                         : this._step.type === "progress" | ||||||
| @@ -270,6 +275,7 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|                                 .step=${this._step} |                                 .step=${this._step} | ||||||
|                                 .hass=${this.hass} |                                 .hass=${this.hass} | ||||||
|                                 .progress=${this._progress} |                                 .progress=${this._progress} | ||||||
|  |                                 .increasePaddingEnd=${showDocumentationLink} | ||||||
|                               ></step-flow-progress> |                               ></step-flow-progress> | ||||||
|                             ` |                             ` | ||||||
|                           : this._step.type === "menu" |                           : this._step.type === "menu" | ||||||
| @@ -278,6 +284,7 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|                                   .flowConfig=${this._params.flowConfig} |                                   .flowConfig=${this._params.flowConfig} | ||||||
|                                   .step=${this._step} |                                   .step=${this._step} | ||||||
|                                   .hass=${this.hass} |                                   .hass=${this.hass} | ||||||
|  |                                   .increasePaddingEnd=${showDocumentationLink} | ||||||
|                                 ></step-flow-menu> |                                 ></step-flow-menu> | ||||||
|                               ` |                               ` | ||||||
|                             : html` |                             : html` | ||||||
| @@ -286,7 +293,8 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|                                   .step=${this._step} |                                   .step=${this._step} | ||||||
|                                   .hass=${this.hass} |                                   .hass=${this.hass} | ||||||
|                                   .navigateToResult=${this._params |                                   .navigateToResult=${this._params | ||||||
|                                     .navigateToResult} |                                     .navigateToResult ?? false} | ||||||
|  |                                   .increasePaddingEnd=${showDocumentationLink} | ||||||
|                                 ></step-flow-create-entry> |                                 ></step-flow-create-entry> | ||||||
|                               `} |                               `} | ||||||
|                 `} |                 `} | ||||||
|   | |||||||
| @@ -22,6 +22,9 @@ class StepFlowAbort extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public handler!: string; |   @property({ attribute: false }) public handler!: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "increase-padding-end" }) | ||||||
|  |   public increasePaddingEnd = false; | ||||||
|  |  | ||||||
|   protected firstUpdated(changed: PropertyValues) { |   protected firstUpdated(changed: PropertyValues) { | ||||||
|     super.firstUpdated(changed); |     super.firstUpdated(changed); | ||||||
|     if (this.step.reason === "missing_credentials") { |     if (this.step.reason === "missing_credentials") { | ||||||
| @@ -34,7 +37,7 @@ class StepFlowAbort extends LitElement { | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <h2> |       <h2 class=${this.increasePaddingEnd ? "end-space" : ""}> | ||||||
|         ${this.params.flowConfig.renderAbortHeader |         ${this.params.flowConfig.renderAbortHeader | ||||||
|           ? this.params.flowConfig.renderAbortHeader(this.hass, this.step) |           ? this.params.flowConfig.renderAbortHeader(this.hass, this.step) | ||||||
|           : this.hass.localize(`component.${this.domain}.title`)} |           : this.hass.localize(`component.${this.domain}.title`)} | ||||||
|   | |||||||
| @@ -36,6 +36,9 @@ class StepFlowCreateEntry extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry; |   @property({ attribute: false }) public step!: DataEntryFlowStepCreateEntry; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "increase-padding-end" }) | ||||||
|  |   public increasePaddingEnd = false; | ||||||
|  |  | ||||||
|   public navigateToResult = false; |   public navigateToResult = false; | ||||||
|  |  | ||||||
|   @state() private _deviceUpdate: Record< |   @state() private _deviceUpdate: Record< | ||||||
| @@ -113,7 +116,7 @@ class StepFlowCreateEntry extends LitElement { | |||||||
|       this.step.result?.entry_id |       this.step.result?.entry_id | ||||||
|     ); |     ); | ||||||
|     return html` |     return html` | ||||||
|       <h2> |       <h2 class=${this.increasePaddingEnd ? "end-space" : ""}> | ||||||
|         ${devices.length |         ${devices.length | ||||||
|           ? localize("ui.panel.config.integrations.config_flow.assign_area", { |           ? localize("ui.panel.config.integrations.config_flow.assign_area", { | ||||||
|               number: devices.length, |               number: devices.length, | ||||||
| @@ -129,7 +132,10 @@ class StepFlowCreateEntry extends LitElement { | |||||||
|               )}</span |               )}</span | ||||||
|             >` |             >` | ||||||
|           : nothing} |           : nothing} | ||||||
|         ${devices.length === 0 |         ${devices.length === 0 && | ||||||
|  |         ["options_flow", "repair_flow"].includes(this.flowConfig.flowType) | ||||||
|  |           ? nothing | ||||||
|  |           : devices.length === 0 | ||||||
|             ? html`<p> |             ? html`<p> | ||||||
|                 ${localize( |                 ${localize( | ||||||
|                   "ui.panel.config.integrations.config_flow.created_config", |                   "ui.panel.config.integrations.config_flow.created_config", | ||||||
|   | |||||||
| @@ -15,11 +15,16 @@ class StepFlowExternal extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public step!: DataEntryFlowStepExternal; |   @property({ attribute: false }) public step!: DataEntryFlowStepExternal; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "increase-padding-end" }) | ||||||
|  |   public increasePaddingEnd = false; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     const localize = this.hass.localize; |     const localize = this.hass.localize; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <h2>${this.flowConfig.renderExternalStepHeader(this.hass, this.step)}</h2> |       <h2 class=${this.increasePaddingEnd ? "end-space" : ""}> | ||||||
|  |         ${this.flowConfig.renderExternalStepHeader(this.hass, this.step)} | ||||||
|  |       </h2> | ||||||
|       <div class="content"> |       <div class="content"> | ||||||
|         ${this.flowConfig.renderExternalStepDescription(this.hass, this.step)} |         ${this.flowConfig.renderExternalStepDescription(this.hass, this.step)} | ||||||
|         <div class="open-button"> |         <div class="open-button"> | ||||||
| @@ -51,6 +56,9 @@ class StepFlowExternal extends LitElement { | |||||||
|         .open-button a { |         .open-button a { | ||||||
|           text-decoration: none; |           text-decoration: none; | ||||||
|         } |         } | ||||||
|  |         h2.end-space { | ||||||
|  |           padding-inline-end: 72px; | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -27,6 +27,9 @@ class StepFlowForm extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "increase-padding-end" }) | ||||||
|  |   public increasePaddingEnd = false; | ||||||
|  |  | ||||||
|   @state() private _loading = false; |   @state() private _loading = false; | ||||||
|  |  | ||||||
|   @state() private _stepData?: Record<string, any>; |   @state() private _stepData?: Record<string, any>; | ||||||
| @@ -43,7 +46,9 @@ class StepFlowForm extends LitElement { | |||||||
|     const stepData = this._stepDataProcessed; |     const stepData = this._stepDataProcessed; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <h2>${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)}</h2> |       <h2 class=${this.increasePaddingEnd ? "end-space" : ""}> | ||||||
|  |         ${this.flowConfig.renderShowFormStepHeader(this.hass, this.step)} | ||||||
|  |       </h2> | ||||||
|       <div class="content" @click=${this._clickHandler}> |       <div class="content" @click=${this._clickHandler}> | ||||||
|         ${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)} |         ${this.flowConfig.renderShowFormStepDescription(this.hass, this.step)} | ||||||
|         ${this._errorMsg |         ${this._errorMsg | ||||||
| @@ -278,8 +283,6 @@ class StepFlowForm extends LitElement { | |||||||
|         } |         } | ||||||
|         h2 { |         h2 { | ||||||
|           word-break: break-word; |           word-break: break-word; | ||||||
|           padding-inline-end: 72px; |  | ||||||
|           direction: var(--direction); |  | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -17,6 +17,9 @@ class StepFlowMenu extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public step!: DataEntryFlowStepMenu; |   @property({ attribute: false }) public step!: DataEntryFlowStepMenu; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "increase-padding-end" }) | ||||||
|  |   public increasePaddingEnd = false; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     let options: string[]; |     let options: string[]; | ||||||
|     let translations: Record<string, string>; |     let translations: Record<string, string>; | ||||||
| @@ -42,7 +45,9 @@ class StepFlowMenu extends LitElement { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <h2>${this.flowConfig.renderMenuHeader(this.hass, this.step)}</h2> |       <h2 class=${this.increasePaddingEnd ? "end-space" : ""}> | ||||||
|  |         ${this.flowConfig.renderMenuHeader(this.hass, this.step)} | ||||||
|  |       </h2> | ||||||
|       ${description ? html`<div class="content">${description}</div>` : ""} |       ${description ? html`<div class="content">${description}</div>` : ""} | ||||||
|       <div class="options"> |       <div class="options"> | ||||||
|         ${options.map( |         ${options.map( | ||||||
|   | |||||||
| @@ -24,9 +24,12 @@ class StepFlowProgress extends LitElement { | |||||||
|   @property({ type: Number }) |   @property({ type: Number }) | ||||||
|   public progress?: number; |   public progress?: number; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "increase-padding-end" }) | ||||||
|  |   public increasePaddingEnd = false; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|     return html` |     return html` | ||||||
|       <h2> |       <h2 class=${this.increasePaddingEnd ? "end-space" : ""}> | ||||||
|         ${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)} |         ${this.flowConfig.renderShowFormProgressHeader(this.hass, this.step)} | ||||||
|       </h2> |       </h2> | ||||||
|       <div class="content"> |       <div class="content"> | ||||||
|   | |||||||
| @@ -22,6 +22,9 @@ export const configFlowContentStyles = css` | |||||||
|     text-transform: var(--mdc-typography-headline6-text-transform, inherit); |     text-transform: var(--mdc-typography-headline6-text-transform, inherit); | ||||||
|     box-sizing: border-box; |     box-sizing: border-box; | ||||||
|   } |   } | ||||||
|  |   h2.end-space { | ||||||
|  |     padding-inline-end: 72px; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   .content, |   .content, | ||||||
|   .preview { |   .preview { | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ class MoreInfoCover extends LitElement { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (positionStateDisplay) { |     if (positionStateDisplay) { | ||||||
|       return `${stateDisplay} ⸱ ${positionStateDisplay}`; |       return `${stateDisplay} · ${positionStateDisplay}`; | ||||||
|     } |     } | ||||||
|     return stateDisplay; |     return stateDisplay; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -57,7 +57,7 @@ class MoreInfoValve extends LitElement { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     if (positionStateDisplay) { |     if (positionStateDisplay) { | ||||||
|       return `${stateDisplay} ⸱ ${positionStateDisplay}`; |       return `${stateDisplay} · ${positionStateDisplay}`; | ||||||
|     } |     } | ||||||
|     return stateDisplay; |     return stateDisplay; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -15,24 +15,26 @@ import { customElement, property, query, state } from "lit/decorators"; | |||||||
| import { ifDefined } from "lit/directives/if-defined"; | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { styleMap } from "lit/directives/style-map"; | import { styleMap } from "lit/directives/style-map"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
|  | import Fuse from "fuse.js"; | ||||||
| import { canShowPage } from "../../common/config/can_show_page"; | import { canShowPage } from "../../common/config/can_show_page"; | ||||||
| import { componentsWithService } from "../../common/config/components_with_service"; | import { componentsWithService } from "../../common/config/components_with_service"; | ||||||
| import { isComponentLoaded } from "../../common/config/is_component_loaded"; | import { isComponentLoaded } from "../../common/config/is_component_loaded"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name"; | import { | ||||||
| import { computeStateName } from "../../common/entity/compute_state_name"; |   computeDeviceName, | ||||||
|  |   computeDeviceNameDisplay, | ||||||
|  | } from "../../common/entity/compute_device_name"; | ||||||
| import { navigate } from "../../common/navigate"; | import { navigate } from "../../common/navigate"; | ||||||
| import { caseInsensitiveStringCompare } from "../../common/string/compare"; | import { caseInsensitiveStringCompare } from "../../common/string/compare"; | ||||||
| import type { ScorableTextItem } from "../../common/string/filter/sequence-matching"; | import type { ScorableTextItem } from "../../common/string/filter/sequence-matching"; | ||||||
| import { fuzzyFilterSort } from "../../common/string/filter/sequence-matching"; |  | ||||||
| import { debounce } from "../../common/util/debounce"; | import { debounce } from "../../common/util/debounce"; | ||||||
| import "../../components/ha-icon-button"; | import "../../components/ha-icon-button"; | ||||||
| import "../../components/ha-label"; | import "../../components/ha-label"; | ||||||
| import "../../components/ha-list"; | import "../../components/ha-list"; | ||||||
| import "../../components/ha-list-item"; |  | ||||||
| import "../../components/ha-spinner"; | import "../../components/ha-spinner"; | ||||||
| import "../../components/ha-textfield"; | import "../../components/ha-textfield"; | ||||||
| import "../../components/ha-tip"; | import "../../components/ha-tip"; | ||||||
|  | import "../../components/ha-md-list-item"; | ||||||
| import { fetchHassioAddonsInfo } from "../../data/hassio/addon"; | import { fetchHassioAddonsInfo } from "../../data/hassio/addon"; | ||||||
| import { domainToName } from "../../data/integration"; | import { domainToName } from "../../data/integration"; | ||||||
| import { getPanelNameTranslationKey } from "../../data/panel"; | import { getPanelNameTranslationKey } from "../../data/panel"; | ||||||
| @@ -44,6 +46,13 @@ import type { HomeAssistant } from "../../types"; | |||||||
| import { showConfirmationDialog } from "../generic/show-dialog-box"; | import { showConfirmationDialog } from "../generic/show-dialog-box"; | ||||||
| import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog"; | import { showShortcutsDialog } from "../shortcuts/show-shortcuts-dialog"; | ||||||
| import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar"; | import { QuickBarMode, type QuickBarParams } from "./show-dialog-quick-bar"; | ||||||
|  | import { getEntityContext } from "../../common/entity/context/get_entity_context"; | ||||||
|  | import { computeEntityName } from "../../common/entity/compute_entity_name"; | ||||||
|  | import { computeAreaName } from "../../common/entity/compute_area_name"; | ||||||
|  | import { computeRTL } from "../../common/util/compute_rtl"; | ||||||
|  | import { computeDomain } from "../../common/entity/compute_domain"; | ||||||
|  | import { computeStateName } from "../../common/entity/compute_state_name"; | ||||||
|  | import { HaFuse } from "../../resources/fuse"; | ||||||
|  |  | ||||||
| interface QuickBarItem extends ScorableTextItem { | interface QuickBarItem extends ScorableTextItem { | ||||||
|   primaryText: string; |   primaryText: string; | ||||||
| @@ -59,6 +68,9 @@ interface CommandItem extends QuickBarItem { | |||||||
| interface EntityItem extends QuickBarItem { | interface EntityItem extends QuickBarItem { | ||||||
|   altText: string; |   altText: string; | ||||||
|   icon?: TemplateResult; |   icon?: TemplateResult; | ||||||
|  |   translatedDomain: string; | ||||||
|  |   entityId: string; | ||||||
|  |   friendlyName: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| interface DeviceItem extends QuickBarItem { | interface DeviceItem extends QuickBarItem { | ||||||
| @@ -82,6 +94,7 @@ type BaseNavigationCommand = Pick< | |||||||
|   QuickBarNavigationItem, |   QuickBarNavigationItem, | ||||||
|   "primaryText" | "path" |   "primaryText" | "path" | ||||||
| >; | >; | ||||||
|  |  | ||||||
| @customElement("ha-quick-bar") | @customElement("ha-quick-bar") | ||||||
| export class QuickBar extends LitElement { | export class QuickBar extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -139,6 +152,11 @@ export class QuickBar extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   protected firstUpdated(changedProps) { | ||||||
|  |     super.firstUpdated(changedProps); | ||||||
|  |     this.hass.loadBackendTranslation("title"); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _getItems = memoizeOne( |   private _getItems = memoizeOne( | ||||||
|     ( |     ( | ||||||
|       mode: QuickBarMode, |       mode: QuickBarMode, | ||||||
| @@ -323,61 +341,67 @@ export class QuickBar extends LitElement { | |||||||
|  |  | ||||||
|   private _renderDeviceItem(item: DeviceItem, index?: number) { |   private _renderDeviceItem(item: DeviceItem, index?: number) { | ||||||
|     return html` |     return html` | ||||||
|       <ha-list-item |       <ha-md-list-item | ||||||
|         .twoline=${Boolean(item.area)} |         class="two-line" | ||||||
|         .item=${item} |         .item=${item} | ||||||
|         index=${ifDefined(index)} |         index=${ifDefined(index)} | ||||||
|         tabindex="0" |         tabindex="0" | ||||||
|  |         type="button" | ||||||
|       > |       > | ||||||
|         <span>${item.primaryText}</span> |         <span slot="headline">${item.primaryText}</span> | ||||||
|         ${item.area |         ${item.area | ||||||
|           ? html` |           ? html` <span slot="supporting-text">${item.area}</span> ` | ||||||
|               <span slot="secondary" class="item-text secondary" |  | ||||||
|                 >${item.area}</span |  | ||||||
|               > |  | ||||||
|             ` |  | ||||||
|           : nothing} |           : nothing} | ||||||
|       </ha-list-item> |       </ha-md-list-item> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _renderEntityItem(item: EntityItem, index?: number) { |   private _renderEntityItem(item: EntityItem, index?: number) { | ||||||
|  |     const showEntityId = this.hass.userData?.showEntityIdPicker; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-list-item |       <ha-md-list-item | ||||||
|         .twoline=${Boolean(item.altText)} |         class=${showEntityId ? "three-line" : "two-line"} | ||||||
|         .item=${item} |         .item=${item} | ||||||
|         index=${ifDefined(index)} |         index=${ifDefined(index)} | ||||||
|         graphic="icon" |  | ||||||
|         tabindex="0" |         tabindex="0" | ||||||
|  |         type="button" | ||||||
|       > |       > | ||||||
|         ${item.iconPath |         ${item.iconPath | ||||||
|           ? html` |           ? html` | ||||||
|               <ha-svg-icon |               <ha-svg-icon | ||||||
|                 .path=${item.iconPath} |                 .path=${item.iconPath} | ||||||
|                 class="entity" |                 class="entity" | ||||||
|                 slot="graphic" |                 slot="start" | ||||||
|               ></ha-svg-icon> |               ></ha-svg-icon> | ||||||
|             ` |             ` | ||||||
|           : html`<span slot="graphic">${item.icon}</span>`} |           : html`<span slot="start">${item.icon}</span>`} | ||||||
|         <span>${item.primaryText}</span> |         <span slot="headline">${item.primaryText}</span> | ||||||
|         ${item.altText |         ${item.altText | ||||||
|  |           ? html` <span slot="supporting-text">${item.altText}</span> ` | ||||||
|  |           : nothing} | ||||||
|  |         ${item.entityId && showEntityId | ||||||
|           ? html` |           ? html` | ||||||
|               <span slot="secondary" class="item-text secondary" |               <span slot="supporting-text" class="code">${item.entityId}</span> | ||||||
|                 >${item.altText}</span |  | ||||||
|               > |  | ||||||
|             ` |             ` | ||||||
|           : nothing} |           : nothing} | ||||||
|       </ha-list-item> |         ${item.translatedDomain && !showEntityId | ||||||
|  |           ? html`<div slot="trailing-supporting-text"> | ||||||
|  |               ${item.translatedDomain} | ||||||
|  |             </div>` | ||||||
|  |           : nothing} | ||||||
|  |       </ha-md-list-item> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _renderCommandItem(item: CommandItem, index?: number) { |   private _renderCommandItem(item: CommandItem, index?: number) { | ||||||
|     return html` |     return html` | ||||||
|       <ha-list-item |       <ha-md-list-item | ||||||
|         .item=${item} |         .item=${item} | ||||||
|         index=${ifDefined(index)} |         index=${ifDefined(index)} | ||||||
|         hasMeta |         hasMeta | ||||||
|         tabindex="0" |         tabindex="0" | ||||||
|  |         type="button" | ||||||
|       > |       > | ||||||
|         <span> |         <span> | ||||||
|           <ha-label |           <ha-label | ||||||
| @@ -386,7 +410,10 @@ export class QuickBar extends LitElement { | |||||||
|           > |           > | ||||||
|             ${item.iconPath |             ${item.iconPath | ||||||
|               ? html` |               ? html` | ||||||
|                   <ha-svg-icon .path=${item.iconPath} slot="icon"></ha-svg-icon> |                   <ha-svg-icon | ||||||
|  |                     .path=${item.iconPath} | ||||||
|  |                     slot="start" | ||||||
|  |                   ></ha-svg-icon> | ||||||
|                 ` |                 ` | ||||||
|               : nothing} |               : nothing} | ||||||
|             ${item.categoryText} |             ${item.categoryText} | ||||||
| @@ -394,7 +421,7 @@ export class QuickBar extends LitElement { | |||||||
|         </span> |         </span> | ||||||
|  |  | ||||||
|         <span class="command-text">${item.primaryText}</span> |         <span class="command-text">${item.primaryText}</span> | ||||||
|       </ha-list-item> |       </ha-md-list-item> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -421,7 +448,7 @@ export class QuickBar extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _getItemAtIndex(index: number): ListItem | null { |   private _getItemAtIndex(index: number): ListItem | null { | ||||||
|     return this.renderRoot.querySelector(`ha-list-item[index="${index}"]`); |     return this.renderRoot.querySelector(`ha-md-list-item[index="${index}"]`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _addSpinnerToCommandItem(index: number): void { |   private _addSpinnerToCommandItem(index: number): void { | ||||||
| @@ -519,7 +546,7 @@ export class QuickBar extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _handleItemClick(ev) { |   private _handleItemClick(ev) { | ||||||
|     const listItem = ev.target.closest("ha-list-item"); |     const listItem = ev.target.closest("ha-md-list-item"); | ||||||
|     this._processItemAndCloseDialog( |     this._processItemAndCloseDialog( | ||||||
|       listItem.item, |       listItem.item, | ||||||
|       Number(listItem.getAttribute("index")) |       Number(listItem.getAttribute("index")) | ||||||
| @@ -555,18 +582,41 @@ export class QuickBar extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _generateEntityItems(): EntityItem[] { |   private _generateEntityItems(): EntityItem[] { | ||||||
|  |     const isRTL = computeRTL(this.hass); | ||||||
|  |  | ||||||
|     return Object.keys(this.hass.states) |     return Object.keys(this.hass.states) | ||||||
|       .map((entityId) => { |       .map((entityId) => { | ||||||
|         const entityState = this.hass.states[entityId]; |         const stateObj = this.hass.states[entityId]; | ||||||
|  |  | ||||||
|  |         const { area, device } = getEntityContext(stateObj, this.hass); | ||||||
|  |  | ||||||
|  |         const friendlyName = computeStateName(stateObj); // Keep this for search | ||||||
|  |         const entityName = computeEntityName(stateObj, this.hass); | ||||||
|  |         const deviceName = device ? computeDeviceName(device) : undefined; | ||||||
|  |         const areaName = area ? computeAreaName(area) : undefined; | ||||||
|  |  | ||||||
|  |         const primary = entityName || deviceName || entityId; | ||||||
|  |         const secondary = [areaName, entityName ? deviceName : undefined] | ||||||
|  |           .filter(Boolean) | ||||||
|  |           .join(isRTL ? " ◂ " : " ▸ "); | ||||||
|  |  | ||||||
|  |         const translatedDomain = domainToName( | ||||||
|  |           this.hass.localize, | ||||||
|  |           computeDomain(entityId) | ||||||
|  |         ); | ||||||
|  |  | ||||||
|         const entityItem = { |         const entityItem = { | ||||||
|           primaryText: computeStateName(entityState), |           primaryText: primary, | ||||||
|           altText: entityId, |           altText: secondary, | ||||||
|           icon: html` |           icon: html` | ||||||
|             <ha-state-icon |             <ha-state-icon | ||||||
|               .hass=${this.hass} |               .hass=${this.hass} | ||||||
|               .stateObj=${entityState} |               .stateObj=${stateObj} | ||||||
|             ></ha-state-icon> |             ></ha-state-icon> | ||||||
|           `, |           `, | ||||||
|  |           translatedDomain: translatedDomain, | ||||||
|  |           entityId: entityId, | ||||||
|  |           friendlyName: friendlyName, | ||||||
|           action: () => fireEvent(this, "hass-more-info", { entityId }), |           action: () => fireEvent(this, "hass-more-info", { entityId }), | ||||||
|         }; |         }; | ||||||
|  |  | ||||||
| @@ -846,9 +896,30 @@ export class QuickBar extends LitElement { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _fuseIndex = memoizeOne((items: QuickBarItem[]) => | ||||||
|  |     Fuse.createIndex( | ||||||
|  |       [ | ||||||
|  |         "primaryText", | ||||||
|  |         "altText", | ||||||
|  |         "friendlyName", | ||||||
|  |         "translatedDomain", | ||||||
|  |         "entityId", // for technical search | ||||||
|  |       ], | ||||||
|  |       items | ||||||
|  |     ) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   private _filterItems = memoizeOne( |   private _filterItems = memoizeOne( | ||||||
|     (items: QuickBarItem[], filter: string): QuickBarItem[] => |     (items: QuickBarItem[], filter: string): QuickBarItem[] => { | ||||||
|       fuzzyFilterSort<QuickBarItem>(filter.trimLeft(), items) |       const index = this._fuseIndex(items); | ||||||
|  |       const fuse = new HaFuse(items, {}, index); | ||||||
|  |  | ||||||
|  |       const results = fuse.multiTermsSearch(filter.trim()); | ||||||
|  |       if (!results || !results.length) { | ||||||
|  |         return items; | ||||||
|  |       } | ||||||
|  |       return results.map((result) => result.item); | ||||||
|  |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   static get styles() { |   static get styles() { | ||||||
| @@ -930,9 +1001,41 @@ export class QuickBar extends LitElement { | |||||||
|           direction: var(--direction); |           direction: var(--direction); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         ha-list-item { |         ha-md-list-item { | ||||||
|           width: 100%; |           width: 100%; | ||||||
|           --mdc-list-item-graphic-margin: 20px; |         } | ||||||
|  |  | ||||||
|  |         /* Fixed height for items because we are use virtualizer */ | ||||||
|  |         ha-md-list-item.two-line { | ||||||
|  |           --md-list-item-one-line-container-height: 64px; | ||||||
|  |           --md-list-item-two-line-container-height: 64px; | ||||||
|  |           --md-list-item-top-space: 8px; | ||||||
|  |           --md-list-item-bottom-space: 8px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-md-list-item.three-line { | ||||||
|  |           width: 100%; | ||||||
|  |           --md-list-item-one-line-container-height: 72px; | ||||||
|  |           --md-list-item-two-line-container-height: 72px; | ||||||
|  |           --md-list-item-three-line-container-height: 72px; | ||||||
|  |           --md-list-item-top-space: 8px; | ||||||
|  |           --md-list-item-bottom-space: 8px; | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-md-list-item .code { | ||||||
|  |           font-family: var(--ha-font-family-code); | ||||||
|  |           font-size: var(--ha-font-size-xs); | ||||||
|  |         } | ||||||
|  |  | ||||||
|  |         ha-md-list-item [slot="trailing-supporting-text"] { | ||||||
|  |           font-size: var(--ha-font-size-s); | ||||||
|  |           font-weight: var(--ha-font-weight-normal); | ||||||
|  |           line-height: var(--ha-line-height-normal); | ||||||
|  |           align-self: flex-end; | ||||||
|  |           max-width: 30%; | ||||||
|  |           text-overflow: ellipsis; | ||||||
|  |           overflow: hidden; | ||||||
|  |           white-space: nowrap; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         ha-tip { |         ha-tip { | ||||||
|   | |||||||
| @@ -69,12 +69,17 @@ const _SHORTCUTS: Section[] = [ | |||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|   { |   { | ||||||
|     key: "ui.dialogs.shortcuts.automations.title", |     key: "ui.dialogs.shortcuts.automation_script.title", | ||||||
|     items: [ |     items: [ | ||||||
|       { |       { | ||||||
|         type: "shortcut", |         type: "shortcut", | ||||||
|         shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "V"], |         shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "V"], | ||||||
|         key: "ui.dialogs.shortcuts.automations.paste", |         key: "ui.dialogs.shortcuts.automation_script.paste", | ||||||
|  |       }, | ||||||
|  |       { | ||||||
|  |         type: "shortcut", | ||||||
|  |         shortcut: [{ key: "ui.dialogs.shortcuts.shortcuts.ctrl_cmd" }, "S"], | ||||||
|  |         key: "ui.dialogs.shortcuts.automation_script.save", | ||||||
|       }, |       }, | ||||||
|     ], |     ], | ||||||
|   }, |   }, | ||||||
|   | |||||||
| @@ -407,6 +407,7 @@ export class HaVoiceAssistantSetupDialog extends LitElement { | |||||||
|           align-items: center; |           align-items: center; | ||||||
|           margin-right: 12px; |           margin-right: 12px; | ||||||
|           margin-inline-end: 12px; |           margin-inline-end: 12px; | ||||||
|  |           margin-inline-start: initial; | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ export class HaVoiceCommandDialog extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _opened = false; |   @state() private _opened = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "AssistPipelineId", |     key: "AssistPipelineId", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -1,6 +1,8 @@ | |||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { css, html, LitElement } from "lit"; | import { css, html, LitElement } from "lit"; | ||||||
| import { property, state } from "lit/decorators"; | import { property, state } from "lit/decorators"; | ||||||
|  | import "@material/mwc-button"; | ||||||
|  | import "../components/ha-spinner"; | ||||||
|  |  | ||||||
| class HaInitPage extends LitElement { | class HaInitPage extends LitElement { | ||||||
|   @property({ type: Boolean }) public error = false; |   @property({ type: Boolean }) public error = false; | ||||||
|   | |||||||
| @@ -42,6 +42,7 @@ class PanelCalendar extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _error?: string = undefined; |   @state() private _error?: string = undefined; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "deSelectedCalendars", |     key: "deSelectedCalendars", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -69,6 +69,7 @@ export class HaConfigApplicationCredentials extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _activeHiddenColumns?: string[]; |   private _activeHiddenColumns?: string[]; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "application-credentials-table-search", |     key: "application-credentials-table-search", | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ export default class HaAutomationAction extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _showReorder = false; |   @state() private _showReorder = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "automationClipboard", |     key: "automationClipboard", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -36,6 +36,7 @@ export default class HaAutomationCondition extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _showReorder = false; |   @state() private _showReorder = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "automationClipboard", |     key: "automationClipboard", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ import "../../../../../components/ha-form/ha-form"; | |||||||
| import type { SchemaUnion } from "../../../../../components/ha-form/types"; | import type { SchemaUnion } from "../../../../../components/ha-form/types"; | ||||||
| import type { NumericStateCondition } from "../../../../../data/automation"; | import type { NumericStateCondition } from "../../../../../data/automation"; | ||||||
| import type { HomeAssistant } from "../../../../../types"; | import type { HomeAssistant } from "../../../../../types"; | ||||||
|  | import { NON_NUMERIC_ATTRIBUTES } from "../../../../../data/entity_attributes"; | ||||||
|  |  | ||||||
| const numericStateConditionStruct = object({ | const numericStateConditionStruct = object({ | ||||||
|   alias: optional(string()), |   alias: optional(string()), | ||||||
| @@ -85,72 +86,7 @@ export default class HaNumericStateCondition extends LitElement { | |||||||
|           name: "attribute", |           name: "attribute", | ||||||
|           selector: { |           selector: { | ||||||
|             attribute: { |             attribute: { | ||||||
|               hide_attributes: [ |               hide_attributes: NON_NUMERIC_ATTRIBUTES, | ||||||
|                 "access_token", |  | ||||||
|                 "auto_update", |  | ||||||
|                 "available_modes", |  | ||||||
|                 "away_mode", |  | ||||||
|                 "changed_by", |  | ||||||
|                 "code_format", |  | ||||||
|                 "color_modes", |  | ||||||
|                 "current_activity", |  | ||||||
|                 "device_class", |  | ||||||
|                 "editable", |  | ||||||
|                 "effect_list", |  | ||||||
|                 "effect", |  | ||||||
|                 "entity_picture", |  | ||||||
|                 "event_type", |  | ||||||
|                 "event_types", |  | ||||||
|                 "fan_mode", |  | ||||||
|                 "fan_modes", |  | ||||||
|                 "fan_speed_list", |  | ||||||
|                 "forecast", |  | ||||||
|                 "friendly_name", |  | ||||||
|                 "frontend_stream_type", |  | ||||||
|                 "has_date", |  | ||||||
|                 "has_time", |  | ||||||
|                 "hs_color", |  | ||||||
|                 "hvac_mode", |  | ||||||
|                 "hvac_modes", |  | ||||||
|                 "icon", |  | ||||||
|                 "media_album_name", |  | ||||||
|                 "media_artist", |  | ||||||
|                 "media_content_type", |  | ||||||
|                 "media_position_updated_at", |  | ||||||
|                 "media_title", |  | ||||||
|                 "next_dawn", |  | ||||||
|                 "next_dusk", |  | ||||||
|                 "next_midnight", |  | ||||||
|                 "next_noon", |  | ||||||
|                 "next_rising", |  | ||||||
|                 "next_setting", |  | ||||||
|                 "operation_list", |  | ||||||
|                 "operation_mode", |  | ||||||
|                 "options", |  | ||||||
|                 "preset_mode", |  | ||||||
|                 "preset_modes", |  | ||||||
|                 "release_notes", |  | ||||||
|                 "release_summary", |  | ||||||
|                 "release_url", |  | ||||||
|                 "restored", |  | ||||||
|                 "rgb_color", |  | ||||||
|                 "rgbw_color", |  | ||||||
|                 "shuffle", |  | ||||||
|                 "sound_mode_list", |  | ||||||
|                 "sound_mode", |  | ||||||
|                 "source_list", |  | ||||||
|                 "source_type", |  | ||||||
|                 "source", |  | ||||||
|                 "state_class", |  | ||||||
|                 "supported_features", |  | ||||||
|                 "swing_mode", |  | ||||||
|                 "swing_mode", |  | ||||||
|                 "swing_modes", |  | ||||||
|                 "title", |  | ||||||
|                 "token", |  | ||||||
|                 "unit_of_measurement", |  | ||||||
|                 "xy_color", |  | ||||||
|               ], |  | ||||||
|             }, |             }, | ||||||
|           }, |           }, | ||||||
|           context: { |           context: { | ||||||
|   | |||||||
| @@ -135,6 +135,7 @@ export class HaAutomationEditor extends PreventUnsavedMixin( | |||||||
|  |  | ||||||
|   @state() private _blueprintConfig?: BlueprintAutomationConfig; |   @state() private _blueprintConfig?: BlueprintAutomationConfig; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @consume({ context: fullEntitiesContext, subscribe: true }) |   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||||
|   @transform<EntityRegistryEntry[], EntityRegistryEntry>({ |   @transform<EntityRegistryEntry[], EntityRegistryEntry>({ | ||||||
|     transformer: function (this: HaAutomationEditor, value) { |     transformer: function (this: HaAutomationEditor, value) { | ||||||
|   | |||||||
| @@ -138,6 +138,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _filteredAutomations?: string[] | null; |   @state() private _filteredAutomations?: string[] | null; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "automation-table-search", |     key: "automation-table-search", | ||||||
| @@ -146,6 +147,7 @@ class HaAutomationPicker extends SubscribeMixin(LitElement) { | |||||||
|   }) |   }) | ||||||
|   private _filter = ""; |   private _filter = ""; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "automation-table-filters-full", |     key: "automation-table-filters-full", | ||||||
|   | |||||||
| @@ -26,8 +26,12 @@ import type { | |||||||
|   ManualAutomationConfig, |   ManualAutomationConfig, | ||||||
|   Trigger, |   Trigger, | ||||||
| } from "../../../data/automation"; | } from "../../../data/automation"; | ||||||
| import { normalizeAutomationConfig } from "../../../data/automation"; | import { | ||||||
| import type { Action } from "../../../data/script"; |   isCondition, | ||||||
|  |   isTrigger, | ||||||
|  |   normalizeAutomationConfig, | ||||||
|  | } from "../../../data/automation"; | ||||||
|  | import { getActionType, type Action } from "../../../data/script"; | ||||||
| import { haStyle } from "../../../resources/styles"; | import { haStyle } from "../../../resources/styles"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { documentationUrl } from "../../../util/documentation-url"; | import { documentationUrl } from "../../../util/documentation-url"; | ||||||
| @@ -310,12 +314,77 @@ export class HaManualAutomationEditor extends LitElement { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const loaded: any = load(paste); |     let loaded: any; | ||||||
|     if (loaded) { |     try { | ||||||
|       let normalized: AutomationConfig | undefined; |       loaded = load(paste); | ||||||
|  |     } catch (_err: any) { | ||||||
|  |       showToast(this, { | ||||||
|  |         message: this.hass.localize( | ||||||
|  |           "ui.panel.config.automation.editor.paste_invalid_yaml" | ||||||
|  |         ), | ||||||
|  |         duration: 4000, | ||||||
|  |         dismissable: true, | ||||||
|  |       }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!loaded || typeof loaded !== "object") { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let config = loaded; | ||||||
|  |  | ||||||
|  |     if ("automation" in config) { | ||||||
|  |       config = config.automation; | ||||||
|  |       if (Array.isArray(config)) { | ||||||
|  |         config = config[0]; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (Array.isArray(config)) { | ||||||
|  |       if (config.length === 1) { | ||||||
|  |         config = config[0]; | ||||||
|  |       } else { | ||||||
|  |         const newConfig: AutomationConfig = { | ||||||
|  |           triggers: [], | ||||||
|  |           conditions: [], | ||||||
|  |           actions: [], | ||||||
|  |         }; | ||||||
|  |         let found = false; | ||||||
|  |         config.forEach((cfg: any) => { | ||||||
|  |           if (isTrigger(cfg)) { | ||||||
|  |             found = true; | ||||||
|  |             (newConfig.triggers as Trigger[]).push(cfg); | ||||||
|  |           } | ||||||
|  |           if (isCondition(cfg)) { | ||||||
|  |             found = true; | ||||||
|  |             (newConfig.conditions as Condition[]).push(cfg); | ||||||
|  |           } | ||||||
|  |           if (getActionType(cfg) !== "unknown") { | ||||||
|  |             found = true; | ||||||
|  |             (newConfig.actions as Action[]).push(cfg); | ||||||
|  |           } | ||||||
|  |         }); | ||||||
|  |         if (found) { | ||||||
|  |           config = newConfig; | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (isTrigger(config)) { | ||||||
|  |       config = { triggers: [config] }; | ||||||
|  |     } | ||||||
|  |     if (isCondition(config)) { | ||||||
|  |       config = { conditions: [config] }; | ||||||
|  |     } | ||||||
|  |     if (getActionType(config) !== "unknown") { | ||||||
|  |       config = { actions: [config] }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let normalized: AutomationConfig; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|         normalized = normalizeAutomationConfig(loaded); |       normalized = normalizeAutomationConfig(config); | ||||||
|     } catch (_err: any) { |     } catch (_err: any) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -358,7 +427,6 @@ export class HaManualAutomationEditor extends LitElement { | |||||||
|       // replace the config completely |       // replace the config completely | ||||||
|       this._replaceExistingConfig(normalized); |       this._replaceExistingConfig(normalized); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   private _appendToExistingConfig(config: ManualAutomationConfig) { |   private _appendToExistingConfig(config: ManualAutomationConfig) { | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ export default class HaAutomationOption extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _showReorder = false; |   @state() private _showReorder = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "automationClipboard", |     key: "automationClipboard", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -38,6 +38,7 @@ export default class HaAutomationTrigger extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _showReorder = false; |   @state() private _showReorder = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "automationClipboard", |     key: "automationClipboard", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { mdiCog, mdiDelete, mdiHarddisk, mdiNas } from "@mdi/js"; | import { mdiCog, mdiDelete, mdiHarddisk, mdiNas } from "@mdi/js"; | ||||||
| import { css, html, LitElement, nothing } from "lit"; | import { css, html, LitElement, nothing, type TemplateResult } from "lit"; | ||||||
|  | import { join } from "lit/directives/join"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../../../../common/dom/fire_event"; | import { fireEvent } from "../../../../../common/dom/fire_event"; | ||||||
| @@ -57,26 +58,51 @@ class HaBackupConfigAgents extends LitElement { | |||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const texts: (TemplateResult | string)[] = []; | ||||||
|  |  | ||||||
|  |     if (isNetworkMountAgent(agentId)) { | ||||||
|  |       texts.push( | ||||||
|  |         this.hass.localize( | ||||||
|  |           "ui.panel.config.backup.agents.network_mount_agent_description" | ||||||
|  |         ) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const encryptionTurnedOff = |     const encryptionTurnedOff = | ||||||
|       this.agentsConfig?.[agentId]?.protected === false; |       this.agentsConfig?.[agentId]?.protected === false; | ||||||
|  |  | ||||||
|     if (encryptionTurnedOff) { |     if (encryptionTurnedOff) { | ||||||
|       return html` |       texts.push( | ||||||
|  |         html`<div class="unencrypted-warning"> | ||||||
|           <span class="dot warning"></span> |           <span class="dot warning"></span> | ||||||
|           <span> |           <span> | ||||||
|             ${this.hass.localize( |             ${this.hass.localize( | ||||||
|               "ui.panel.config.backup.agents.encryption_turned_off" |               "ui.panel.config.backup.agents.encryption_turned_off" | ||||||
|             )} |             )} | ||||||
|           </span> |           </span> | ||||||
|       `; |         </div>` | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (isNetworkMountAgent(agentId)) { |  | ||||||
|       return this.hass.localize( |  | ||||||
|         "ui.panel.config.backup.agents.network_mount_agent_description" |  | ||||||
|       ); |       ); | ||||||
|     } |     } | ||||||
|     return ""; |  | ||||||
|  |     const retention = this.agentsConfig?.[agentId]?.retention; | ||||||
|  |  | ||||||
|  |     if (retention) { | ||||||
|  |       if (retention.copies === null && retention.days === null) { | ||||||
|  |         texts.push( | ||||||
|  |           this.hass.localize("ui.panel.config.backup.agents.retention_all") | ||||||
|  |         ); | ||||||
|  |       } else { | ||||||
|  |         texts.push( | ||||||
|  |           this.hass.localize( | ||||||
|  |             `ui.panel.config.backup.agents.retention_${retention.copies ? "backups" : "days"}`, | ||||||
|  |             { | ||||||
|  |               count: retention.copies || retention.days, | ||||||
|  |             } | ||||||
|  |           ) | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     return join(texts, html`<span class="separator"> · </span>`); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _availableAgents = memoizeOne( |   private _availableAgents = memoizeOne( | ||||||
| @@ -287,6 +313,11 @@ class HaBackupConfigAgents extends LitElement { | |||||||
|       gap: 8px; |       gap: 8px; | ||||||
|       line-height: normal; |       line-height: normal; | ||||||
|     } |     } | ||||||
|  |     .unencrypted-warning { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       gap: 4px; | ||||||
|  |     } | ||||||
|     .dot { |     .dot { | ||||||
|       display: block; |       display: block; | ||||||
|       position: relative; |       position: relative; | ||||||
| @@ -294,11 +325,22 @@ class HaBackupConfigAgents extends LitElement { | |||||||
|       height: 8px; |       height: 8px; | ||||||
|       background-color: var(--disabled-color); |       background-color: var(--disabled-color); | ||||||
|       border-radius: 50%; |       border-radius: 50%; | ||||||
|       flex: none; |  | ||||||
|     } |     } | ||||||
|     .dot.warning { |     .dot.warning { | ||||||
|       background-color: var(--warning-color); |       background-color: var(--warning-color); | ||||||
|     } |     } | ||||||
|  |     @media all and (max-width: 500px) { | ||||||
|  |       .separator { | ||||||
|  |         display: none; | ||||||
|  |       } | ||||||
|  |       ha-md-list-item [slot="supporting-text"] { | ||||||
|  |         display: flex; | ||||||
|  |         align-items: flex-start; | ||||||
|  |         flex-direction: column; | ||||||
|  |         justify-content: flex-start; | ||||||
|  |         gap: 4px; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -98,6 +98,7 @@ class HaConfigBackupBackups extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _selected: string[] = []; |   @state() private _selected: string[] = []; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "backups-table-filters", |     key: "backups-table-filters", | ||||||
|   | |||||||
| @@ -118,8 +118,7 @@ class HaConfigBackupDetails extends LitElement { | |||||||
|                             </p> |                             </p> | ||||||
|                           </div> |                           </div> | ||||||
|                         ` |                         ` | ||||||
|                       : this.config?.agents[this.agentId] |                       : html`<ha-backup-config-retention | ||||||
|                         ? html`<ha-backup-config-retention |  | ||||||
|                           location-specific |                           location-specific | ||||||
|                           .headline=${this.hass.localize( |                           .headline=${this.hass.localize( | ||||||
|                             `ui.panel.config.backup.location.retention_for_${isLocalAgent(this.agentId) ? "this_system" : "location"}`, |                             `ui.panel.config.backup.location.retention_for_${isLocalAgent(this.agentId) ? "this_system" : "location"}`, | ||||||
| @@ -129,8 +128,7 @@ class HaConfigBackupDetails extends LitElement { | |||||||
|                           .retention=${this.config?.agents[this.agentId] |                           .retention=${this.config?.agents[this.agentId] | ||||||
|                             ?.retention} |                             ?.retention} | ||||||
|                           @value-changed=${this._retentionChanged} |                           @value-changed=${this._retentionChanged} | ||||||
|                           ></ha-backup-config-retention>` |                         ></ha-backup-config-retention>`} | ||||||
|                         : nothing} |  | ||||||
|                   </ha-card> |                   </ha-card> | ||||||
|                   <ha-card> |                   <ha-card> | ||||||
|                     <div class="card-header"> |                     <div class="card-header"> | ||||||
|   | |||||||
| @@ -9,7 +9,7 @@ import { | |||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
| import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; | import type { CSSResultGroup, PropertyValues, TemplateResult } from "lit"; | ||||||
| import { LitElement, html } from "lit"; | import { LitElement, html } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import type { HASSDomEvent } from "../../../common/dom/fire_event"; | import type { HASSDomEvent } from "../../../common/dom/fire_event"; | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| @@ -118,6 +118,7 @@ class HaBlueprintOverview extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _activeHiddenColumns?: string[]; |   private _activeHiddenColumns?: string[]; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "blueprint-table-search", |     key: "blueprint-table-search", | ||||||
| @@ -499,9 +500,11 @@ class HaBlueprintOverview extends LitElement { | |||||||
|             list: html`<ul> |             list: html`<ul> | ||||||
|               ${[...(related.automation || []), ...(related.script || [])].map( |               ${[...(related.automation || []), ...(related.script || [])].map( | ||||||
|                 (item) => { |                 (item) => { | ||||||
|                   const state = this.hass.states[item]; |                   const automationState = this.hass.states[item]; | ||||||
|                   return html`<li> |                   return html`<li> | ||||||
|                     ${state ? `${computeStateName(state)} (${item})` : item} |                     ${automationState | ||||||
|  |                       ? `${computeStateName(automationState)} (${item})` | ||||||
|  |                       : item} | ||||||
|                   </li>`; |                   </li>`; | ||||||
|                 } |                 } | ||||||
|               )} |               )} | ||||||
|   | |||||||
| @@ -106,7 +106,7 @@ export class HaDeviceCard extends LitElement { | |||||||
|               <div class="extra-info"> |               <div class="extra-info"> | ||||||
|                 ${type === "bluetooth" && |                 ${type === "bluetooth" && | ||||||
|                 isComponentLoaded(this.hass, "bluetooth") |                 isComponentLoaded(this.hass, "bluetooth") | ||||||
|                   ? html`${titleCase(type)} |                   ? html`${titleCase(type)}: | ||||||
|                       <a |                       <a | ||||||
|                         href="/config/bluetooth/advertisement-monitor?${createSearchParam( |                         href="/config/bluetooth/advertisement-monitor?${createSearchParam( | ||||||
|                           { address: value } |                           { address: value } | ||||||
| @@ -114,7 +114,7 @@ export class HaDeviceCard extends LitElement { | |||||||
|                         >${value.toUpperCase()}</a |                         >${value.toUpperCase()}</a | ||||||
|                       >` |                       >` | ||||||
|                   : type === "mac" && isComponentLoaded(this.hass, "dhcp") |                   : type === "mac" && isComponentLoaded(this.hass, "dhcp") | ||||||
|                     ? html`${titleCase(type)} |                     ? html`MAC: | ||||||
|                         <a |                         <a | ||||||
|                           href="/config/dhcp?${createSearchParam({ |                           href="/config/dhcp?${createSearchParam({ | ||||||
|                             mac_address: value, |                             mac_address: value, | ||||||
|   | |||||||
| @@ -1559,6 +1559,7 @@ export class HaConfigDevicePage extends LitElement { | |||||||
|           align-items: center; |           align-items: center; | ||||||
|           padding-left: 8px; |           padding-left: 8px; | ||||||
|           padding-inline-start: 8px; |           padding-inline-start: 8px; | ||||||
|  |           padding-inline-end: initial; | ||||||
|           direction: var(--direction); |           direction: var(--direction); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -120,6 +120,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _selected: string[] = []; |   @state() private _selected: string[] = []; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "devices-table-search", |     key: "devices-table-search", | ||||||
| @@ -128,6 +129,7 @@ export class HaConfigDeviceDashboard extends SubscribeMixin(LitElement) { | |||||||
|   }) |   }) | ||||||
|   private _filter: string = history.state?.filter || ""; |   private _filter: string = history.state?.filter || ""; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "devices-table-filters-full", |     key: "devices-table-filters-full", | ||||||
|   | |||||||
| @@ -159,6 +159,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|   @consume({ context: fullEntitiesContext, subscribe: true }) |   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||||
|   _entities!: EntityRegistryEntry[]; |   _entities!: EntityRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "entities-table-search", |     key: "entities-table-search", | ||||||
| @@ -169,6 +170,7 @@ export class HaConfigEntities extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _searchParms = new URLSearchParams(window.location.search); |   @state() private _searchParms = new URLSearchParams(window.location.search); | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "entities-table-filters", |     key: "entities-table-filters", | ||||||
|   | |||||||
| @@ -168,6 +168,7 @@ export class HaConfigHelpers extends SubscribeMixin(LitElement) { | |||||||
|   }) |   }) | ||||||
|   private _activeCollapsed?: string; |   private _activeCollapsed?: string; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "helpers-table-search", |     key: "helpers-table-search", | ||||||
|   | |||||||
| @@ -156,7 +156,7 @@ class HaConfigInfo extends LitElement { | |||||||
|                   )} |                   )} | ||||||
|                 </span> |                 </span> | ||||||
|                 <span class="version"> |                 <span class="version"> | ||||||
|                   ${JS_VERSION}${JS_TYPE !== "modern" ? ` ⸱ ${JS_TYPE}` : ""} |                   ${JS_VERSION}${JS_TYPE !== "modern" ? ` · ${JS_TYPE}` : ""} | ||||||
|                 </span> |                 </span> | ||||||
|               </li> |               </li> | ||||||
|             </ul> |             </ul> | ||||||
|   | |||||||
| @@ -210,6 +210,9 @@ export class BluetoothAdvertisementMonitorPanel extends LitElement { | |||||||
|         .route=${this.route} |         .route=${this.route} | ||||||
|         .columns=${this._columns(this.hass.localize)} |         .columns=${this._columns(this.hass.localize)} | ||||||
|         .data=${this._dataWithNamedSourceAndIds(this._data)} |         .data=${this._dataWithNamedSourceAndIds(this._data)} | ||||||
|  |         .noDataText=${this.hass.localize( | ||||||
|  |           "ui.panel.config.bluetooth.no_advertisements_found" | ||||||
|  |         )} | ||||||
|         @row-click=${this._handleRowClicked} |         @row-click=${this._handleRowClicked} | ||||||
|         .initialGroupColumn=${this._activeGrouping} |         .initialGroupColumn=${this._activeGrouping} | ||||||
|         .initialCollapsedGroups=${this._activeCollapsed} |         .initialCollapsedGroups=${this._activeCollapsed} | ||||||
|   | |||||||
| @@ -96,6 +96,9 @@ export class DHCPConfigPanel extends SubscribeMixin(LitElement) { | |||||||
|         .route=${this.route} |         .route=${this.route} | ||||||
|         .columns=${this._columns(this.hass.localize)} |         .columns=${this._columns(this.hass.localize)} | ||||||
|         .data=${this._dataWithIds(this._data)} |         .data=${this._dataWithIds(this._data)} | ||||||
|  |         .noDataText=${this.hass.localize( | ||||||
|  |           "ui.panel.config.dhcp.no_devices_found" | ||||||
|  |         )} | ||||||
|         filter=${this._macAddress || ""} |         filter=${this._macAddress || ""} | ||||||
|       ></hass-tabs-subpage-data-table> |       ></hass-tabs-subpage-data-table> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -1,7 +1,7 @@ | |||||||
| import "@material/mwc-button"; | import "@material/mwc-button"; | ||||||
| import type { CSSResultGroup, TemplateResult } from "lit"; | import type { CSSResultGroup, TemplateResult } from "lit"; | ||||||
| import { css, html, LitElement } from "lit"; | import { css, html, LitElement } from "lit"; | ||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { storage } from "../../../../../common/decorators/storage"; | import { storage } from "../../../../../common/decorators/storage"; | ||||||
| import "../../../../../components/ha-card"; | import "../../../../../components/ha-card"; | ||||||
| import "../../../../../components/ha-code-editor"; | import "../../../../../components/ha-code-editor"; | ||||||
| @@ -23,6 +23,7 @@ export class MQTTConfigPanel extends LitElement { | |||||||
|  |  | ||||||
|   @property({ type: Boolean }) public narrow = false; |   @property({ type: Boolean }) public narrow = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-mqtt-topic-ls", |     key: "panel-dev-mqtt-topic-ls", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -30,6 +31,7 @@ export class MQTTConfigPanel extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _topic = ""; |   private _topic = ""; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-mqtt-payload-ls", |     key: "panel-dev-mqtt-payload-ls", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -37,6 +39,7 @@ export class MQTTConfigPanel extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _payload = ""; |   private _payload = ""; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-mqtt-qos-ls", |     key: "panel-dev-mqtt-qos-ls", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -44,6 +47,7 @@ export class MQTTConfigPanel extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _qos = "0"; |   private _qos = "0"; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-mqtt-retain-ls", |     key: "panel-dev-mqtt-retain-ls", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -51,6 +55,7 @@ export class MQTTConfigPanel extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _retain = false; |   private _retain = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-mqtt-allow-template-ls", |     key: "panel-dev-mqtt-allow-template-ls", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ const qosLevel = ["0", "1", "2"]; | |||||||
| class MqttSubscribeCard extends LitElement { | class MqttSubscribeCard extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-mqtt-topic-subscribe", |     key: "panel-dev-mqtt-topic-subscribe", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -28,6 +29,7 @@ class MqttSubscribeCard extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _topic = ""; |   private _topic = ""; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-mqtt-qos-subscribe", |     key: "panel-dev-mqtt-qos-subscribe", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -35,6 +37,7 @@ class MqttSubscribeCard extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _qos = "0"; |   private _qos = "0"; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-mqtt-json-format", |     key: "panel-dev-mqtt-json-format", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -105,6 +105,9 @@ export class SSDPConfigPanel extends SubscribeMixin(LitElement) { | |||||||
|         @grouping-changed=${this._handleGroupingChanged} |         @grouping-changed=${this._handleGroupingChanged} | ||||||
|         @collapsed-changed=${this._handleCollapseChanged} |         @collapsed-changed=${this._handleCollapseChanged} | ||||||
|         .data=${this._dataWithIds(this._data)} |         .data=${this._dataWithIds(this._data)} | ||||||
|  |         .noDataText=${this.hass.localize( | ||||||
|  |           "ui.panel.config.ssdp.no_devices_found" | ||||||
|  |         )} | ||||||
|         @row-click=${this._handleRowClicked} |         @row-click=${this._handleRowClicked} | ||||||
|         clickable |         clickable | ||||||
|       ></hass-tabs-subpage-data-table> |       ></hass-tabs-subpage-data-table> | ||||||
|   | |||||||
| @@ -112,6 +112,9 @@ export class ZeroconfConfigPanel extends SubscribeMixin(LitElement) { | |||||||
|         @grouping-changed=${this._handleGroupingChanged} |         @grouping-changed=${this._handleGroupingChanged} | ||||||
|         @collapsed-changed=${this._handleCollapseChanged} |         @collapsed-changed=${this._handleCollapseChanged} | ||||||
|         .data=${this._dataWithIds(this._data)} |         .data=${this._dataWithIds(this._data)} | ||||||
|  |         .noDataText=${this.hass.localize( | ||||||
|  |           "ui.panel.config.zeroconf.no_devices_found" | ||||||
|  |         )} | ||||||
|         @row-click=${this._handleRowClicked} |         @row-click=${this._handleRowClicked} | ||||||
|         clickable |         clickable | ||||||
|       ></hass-tabs-subpage-data-table> |       ></hass-tabs-subpage-data-table> | ||||||
|   | |||||||
| @@ -55,6 +55,7 @@ export class HaConfigLabels extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _labels: LabelRegistryEntry[] = []; |   @state() private _labels: LabelRegistryEntry[] = []; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "labels-table-search", |     key: "labels-table-search", | ||||||
|   | |||||||
| @@ -70,7 +70,7 @@ class DownloadLogsDialog extends LitElement { | |||||||
|           <span slot="subtitle"> |           <span slot="subtitle"> | ||||||
|             ${this._dialogParams.header}${this._dialogParams.boot === 0 |             ${this._dialogParams.header}${this._dialogParams.boot === 0 | ||||||
|               ? "" |               ? "" | ||||||
|               : ` ⸱ ${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`} |               : ` · ${this._dialogParams.boot === -1 ? this.hass.localize("ui.panel.config.logs.previous") : this.hass.localize("ui.panel.config.logs.startups_ago", { boot: this._dialogParams.boot * -1 })}`} | ||||||
|           </span> |           </span> | ||||||
|         </ha-dialog-header> |         </ha-dialog-header> | ||||||
|         <div slot="content" class="content"> |         <div slot="content" class="content"> | ||||||
|   | |||||||
| @@ -74,6 +74,7 @@ export class HaConfigLovelaceDashboards extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _dashboards: LovelaceDashboard[] = []; |   @state() private _dashboards: LovelaceDashboard[] = []; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "lovelace-dashboards-table-search", |     key: "lovelace-dashboards-table-search", | ||||||
| @@ -161,7 +162,7 @@ export class HaConfigLovelaceDashboards extends LitElement { | |||||||
|                         placement="right" |                         placement="right" | ||||||
|                       > |                       > | ||||||
|                         <ha-svg-icon |                         <ha-svg-icon | ||||||
|                           style="padding-left: 10px; padding-inline-start: 10px; direction: var(--direction);" |                           style="padding-left: 10px; padding-inline-start: 10px; padding-inline-end: initial; direction: var(--direction);" | ||||||
|                           .path=${mdiCheckCircleOutline} |                           .path=${mdiCheckCircleOutline} | ||||||
|                         ></ha-svg-icon> |                         ></ha-svg-icon> | ||||||
|                       </ha-tooltip> |                       </ha-tooltip> | ||||||
|   | |||||||
| @@ -46,6 +46,7 @@ export class HaConfigLovelaceRescources extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _resources: LovelaceResource[] = []; |   @state() private _resources: LovelaceResource[] = []; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "lovelace-resources-table-search", |     key: "lovelace-resources-table-search", | ||||||
|   | |||||||
| @@ -1,66 +0,0 @@ | |||||||
| import "@material/mwc-button/mwc-button"; |  | ||||||
| import type { CSSResultGroup } from "lit"; |  | ||||||
| import { css, html, LitElement } from "lit"; |  | ||||||
| import { customElement, property } from "lit/decorators"; |  | ||||||
| import "../../../components/ha-button"; |  | ||||||
| import "../../../components/ha-card"; |  | ||||||
| import { haStyle } from "../../../resources/styles"; |  | ||||||
| import type { HomeAssistant } from "../../../types"; |  | ||||||
|  |  | ||||||
| @customElement("ha-config-network-ssdp") |  | ||||||
| class ConfigNetworkSSDP extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   protected render() { |  | ||||||
|     return html` |  | ||||||
|       <ha-card |  | ||||||
|         outlined |  | ||||||
|         header=${this.hass.localize("ui.panel.config.network.discovery.ssdp")} |  | ||||||
|       > |  | ||||||
|         <div class="card-content"> |  | ||||||
|           <p> |  | ||||||
|             ${this.hass.localize("ui.panel.config.network.discovery.ssdp_info")} |  | ||||||
|           </p> |  | ||||||
|         </div> |  | ||||||
|         <div class="card-actions"> |  | ||||||
|           <a |  | ||||||
|             href="/config/ssdp" |  | ||||||
|             aria-label=${this.hass.localize( |  | ||||||
|               "ui.panel.config.network.discovery.ssdp_browser" |  | ||||||
|             )} |  | ||||||
|           > |  | ||||||
|             <ha-button> |  | ||||||
|               ${this.hass.localize( |  | ||||||
|                 "ui.panel.config.network.discovery.ssdp_browser" |  | ||||||
|               )} |  | ||||||
|             </ha-button> |  | ||||||
|           </a> |  | ||||||
|         </div> |  | ||||||
|       </ha-card> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return [ |  | ||||||
|       haStyle, |  | ||||||
|       css` |  | ||||||
|         ha-settings-row { |  | ||||||
|           padding: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .card-actions { |  | ||||||
|           display: flex; |  | ||||||
|           flex-direction: row-reverse; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           align-items: center; |  | ||||||
|         } |  | ||||||
|       `, // row-reverse so we tab first to "save" |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-config-network-ssdp": ConfigNetworkSSDP; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -1,70 +0,0 @@ | |||||||
| import "@material/mwc-button/mwc-button"; |  | ||||||
| import type { CSSResultGroup } from "lit"; |  | ||||||
| import { css, html, LitElement } from "lit"; |  | ||||||
| import { customElement, property } from "lit/decorators"; |  | ||||||
| import "../../../components/ha-button"; |  | ||||||
| import "../../../components/ha-card"; |  | ||||||
| import { haStyle } from "../../../resources/styles"; |  | ||||||
| import type { HomeAssistant } from "../../../types"; |  | ||||||
|  |  | ||||||
| @customElement("ha-config-network-zeroconf") |  | ||||||
| class ConfigNetworkZeroconf extends LitElement { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   protected render() { |  | ||||||
|     return html` |  | ||||||
|       <ha-card |  | ||||||
|         outlined |  | ||||||
|         header=${this.hass.localize( |  | ||||||
|           "ui.panel.config.network.discovery.zeroconf" |  | ||||||
|         )} |  | ||||||
|       > |  | ||||||
|         <div class="card-content"> |  | ||||||
|           <p> |  | ||||||
|             ${this.hass.localize( |  | ||||||
|               "ui.panel.config.network.discovery.zeroconf_info" |  | ||||||
|             )} |  | ||||||
|           </p> |  | ||||||
|         </div> |  | ||||||
|         <div class="card-actions"> |  | ||||||
|           <a |  | ||||||
|             href="/config/zeroconf" |  | ||||||
|             aria-label=${this.hass.localize( |  | ||||||
|               "ui.panel.config.network.discovery.zeroconf_browser" |  | ||||||
|             )} |  | ||||||
|           > |  | ||||||
|             <ha-button> |  | ||||||
|               ${this.hass.localize( |  | ||||||
|                 "ui.panel.config.network.discovery.zeroconf_browser" |  | ||||||
|               )} |  | ||||||
|             </ha-button> |  | ||||||
|           </a> |  | ||||||
|         </div> |  | ||||||
|       </ha-card> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return [ |  | ||||||
|       haStyle, |  | ||||||
|       css` |  | ||||||
|         ha-settings-row { |  | ||||||
|           padding: 0; |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         .card-actions { |  | ||||||
|           display: flex; |  | ||||||
|           flex-direction: row-reverse; |  | ||||||
|           justify-content: space-between; |  | ||||||
|           align-items: center; |  | ||||||
|         } |  | ||||||
|       `, // row-reverse so we tab first to "save" |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "ha-config-network-zeroconf": ConfigNetworkZeroconf; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -3,14 +3,18 @@ import { css, html, LitElement } from "lit"; | |||||||
| import { customElement, property } from "lit/decorators"; | import { customElement, property } from "lit/decorators"; | ||||||
| import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | ||||||
| import "../../../layouts/hass-subpage"; | import "../../../layouts/hass-subpage"; | ||||||
|  | import "../../../components/ha-card"; | ||||||
|  | import "../../../components/ha-md-list"; | ||||||
|  | import "../../../components/ha-md-list-item"; | ||||||
|  | import "../../../components/ha-icon-next"; | ||||||
| import type { HomeAssistant, Route } from "../../../types"; | import type { HomeAssistant, Route } from "../../../types"; | ||||||
| import "./ha-config-network"; | import "./ha-config-network"; | ||||||
| import "./ha-config-network-ssdp"; |  | ||||||
| import "./ha-config-network-zeroconf"; |  | ||||||
| import "./ha-config-url-form"; | import "./ha-config-url-form"; | ||||||
| import "./supervisor-hostname"; | import "./supervisor-hostname"; | ||||||
| import "./supervisor-network"; | import "./supervisor-network"; | ||||||
|  |  | ||||||
|  | const NETWORK_BROWSERS = ["dhcp", "ssdp", "zeroconf"] as const; | ||||||
|  |  | ||||||
| @customElement("ha-config-section-network") | @customElement("ha-config-section-network") | ||||||
| class HaConfigSectionNetwork extends LitElement { | class HaConfigSectionNetwork extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -37,15 +41,38 @@ class HaConfigSectionNetwork extends LitElement { | |||||||
|             : ""} |             : ""} | ||||||
|           <ha-config-url-form .hass=${this.hass}></ha-config-url-form> |           <ha-config-url-form .hass=${this.hass}></ha-config-url-form> | ||||||
|           <ha-config-network .hass=${this.hass}></ha-config-network> |           <ha-config-network .hass=${this.hass}></ha-config-network> | ||||||
|           ${isComponentLoaded(this.hass, "ssdp") |           ${NETWORK_BROWSERS.some((component) => | ||||||
|             ? html`<ha-config-network-ssdp |             isComponentLoaded(this.hass, component) | ||||||
|                 .hass=${this.hass} |           ) | ||||||
|               ></ha-config-network-ssdp>` |             ? html` | ||||||
|             : ""} |                 <ha-card | ||||||
|           ${isComponentLoaded(this.hass, "zeroconf") |                   outlined | ||||||
|             ? html`<ha-config-network-zeroconf |                   class="discovery-card" | ||||||
|                 .hass=${this.hass} |                   header=${this.hass.localize( | ||||||
|               ></ha-config-network-zeroconf>` |                     "ui.panel.config.network.discovery.title" | ||||||
|  |                   )} | ||||||
|  |                 > | ||||||
|  |                   <ha-md-list> | ||||||
|  |                     ${NETWORK_BROWSERS.map( | ||||||
|  |                       (domain) => html` | ||||||
|  |                         <ha-md-list-item type="link" href="/config/${domain}"> | ||||||
|  |                           <div slot="headline"> | ||||||
|  |                             ${this.hass.localize( | ||||||
|  |                               `ui.panel.config.network.discovery.${domain}` | ||||||
|  |                             )} | ||||||
|  |                           </div> | ||||||
|  |                           <div slot="supporting-text"> | ||||||
|  |                             ${this.hass.localize( | ||||||
|  |                               `ui.panel.config.network.discovery.${domain}_info` | ||||||
|  |                             )} | ||||||
|  |                           </div> | ||||||
|  |                           <ha-icon-next slot="end"></ha-icon-next> | ||||||
|  |                         </ha-md-list-item> | ||||||
|  |                       ` | ||||||
|  |                     )} | ||||||
|  |                   </ha-md-list> | ||||||
|  |                 </ha-card> | ||||||
|  |               ` | ||||||
|             : ""} |             : ""} | ||||||
|         </div> |         </div> | ||||||
|       </hass-subpage> |       </hass-subpage> | ||||||
| @@ -62,13 +89,15 @@ class HaConfigSectionNetwork extends LitElement { | |||||||
|     supervisor-network, |     supervisor-network, | ||||||
|     ha-config-url-form, |     ha-config-url-form, | ||||||
|     ha-config-network, |     ha-config-network, | ||||||
|     ha-config-network-ssdp, |     .discovery-card { | ||||||
|     ha-config-network-zeroconf { |  | ||||||
|       display: block; |       display: block; | ||||||
|       margin: 0 auto; |       margin: 0 auto; | ||||||
|       margin-bottom: 24px; |       margin-bottom: 24px; | ||||||
|       max-width: 600px; |       max-width: 600px; | ||||||
|     } |     } | ||||||
|  |     .discovery-card ha-md-list { | ||||||
|  |       padding-top: 0; | ||||||
|  |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -20,7 +20,7 @@ class DialogRepairsIssueSubtitle extends LitElement { | |||||||
|   protected render() { |   protected render() { | ||||||
|     const domainName = domainToName(this.hass.localize, this.issue.domain); |     const domainName = domainToName(this.hass.localize, this.issue.domain); | ||||||
|     const reportedBy = domainName |     const reportedBy = domainName | ||||||
|       ? ` ⸱ ${this.hass.localize("ui.panel.config.repairs.reported_by", { |       ? ` · ${this.hass.localize("ui.panel.config.repairs.reported_by", { | ||||||
|           integration: domainName, |           integration: domainName, | ||||||
|         })}` |         })}` | ||||||
|       : ""; |       : ""; | ||||||
|   | |||||||
| @@ -100,13 +100,13 @@ class HaConfigRepairs extends LitElement { | |||||||
|                 ${(issue.severity === "critical" || |                 ${(issue.severity === "critical" || | ||||||
|                   issue.severity === "error") && |                   issue.severity === "error") && | ||||||
|                 issue.created |                 issue.created | ||||||
|                   ? " ⸱ " |                   ? " · " | ||||||
|                   : ""} |                   : ""} | ||||||
|                 ${createdBy |                 ${createdBy | ||||||
|                   ? html`<span .title=${createdBy}>${createdBy}</span>` |                   ? html`<span .title=${createdBy}>${createdBy}</span>` | ||||||
|                   : nothing} |                   : nothing} | ||||||
|                 ${issue.ignored |                 ${issue.ignored | ||||||
|                   ? ` ⸱ ${this.hass.localize( |                   ? ` · ${this.hass.localize( | ||||||
|                       "ui.panel.config.repairs.dialog.ignored_in_version_short", |                       "ui.panel.config.repairs.dialog.ignored_in_version_short", | ||||||
|                       { version: issue.dismissed_version } |                       { version: issue.dismissed_version } | ||||||
|                     )}` |                     )}` | ||||||
|   | |||||||
| @@ -133,6 +133,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _filteredScenes?: string[] | null; |   @state() private _filteredScenes?: string[] | null; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "scene-table-search", |     key: "scene-table-search", | ||||||
| @@ -141,6 +142,7 @@ class HaSceneDashboard extends SubscribeMixin(LitElement) { | |||||||
|   }) |   }) | ||||||
|   private _filter = ""; |   private _filter = ""; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "scene-table-filters-full", |     key: "scene-table-filters-full", | ||||||
|   | |||||||
| @@ -105,6 +105,7 @@ export class HaScriptEditor extends SubscribeMixin( | |||||||
|  |  | ||||||
|   @state() private _readOnly = false; |   @state() private _readOnly = false; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @consume({ context: fullEntitiesContext, subscribe: true }) |   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||||
|   @transform<EntityRegistryEntry[], EntityRegistryEntry>({ |   @transform<EntityRegistryEntry[], EntityRegistryEntry>({ | ||||||
|     transformer: function (this: HaScriptEditor, value) { |     transformer: function (this: HaScriptEditor, value) { | ||||||
|   | |||||||
| @@ -138,6 +138,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() private _filteredScripts?: string[] | null; |   @state() private _filteredScripts?: string[] | null; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "script-table-search", |     key: "script-table-search", | ||||||
| @@ -146,6 +147,7 @@ class HaScriptPicker extends SubscribeMixin(LitElement) { | |||||||
|   }) |   }) | ||||||
|   private _filter = ""; |   private _filter = ""; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "script-table-filters-full", |     key: "script-table-filters-full", | ||||||
|   | |||||||
| @@ -24,7 +24,11 @@ import "../../../components/ha-card"; | |||||||
| import "../../../components/ha-icon-button"; | import "../../../components/ha-icon-button"; | ||||||
| import "../../../components/ha-markdown"; | import "../../../components/ha-markdown"; | ||||||
| import type { Action, Fields, ScriptConfig } from "../../../data/script"; | import type { Action, Fields, ScriptConfig } from "../../../data/script"; | ||||||
| import { MODES, normalizeScriptConfig } from "../../../data/script"; | import { | ||||||
|  |   getActionType, | ||||||
|  |   MODES, | ||||||
|  |   normalizeScriptConfig, | ||||||
|  | } from "../../../data/script"; | ||||||
| import { haStyle } from "../../../resources/styles"; | import { haStyle } from "../../../resources/styles"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { documentationUrl } from "../../../util/documentation-url"; | import { documentationUrl } from "../../../util/documentation-url"; | ||||||
| @@ -234,12 +238,49 @@ export class HaManualScriptEditor extends LitElement { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const loaded: any = load(paste); |     let loaded: any; | ||||||
|     if (loaded) { |     try { | ||||||
|  |       loaded = load(paste); | ||||||
|  |     } catch (_err: any) { | ||||||
|  |       showToast(this, { | ||||||
|  |         message: this.hass.localize( | ||||||
|  |           "ui.panel.config.script.editor.paste_invalid_config" | ||||||
|  |         ), | ||||||
|  |         duration: 4000, | ||||||
|  |         dismissable: true, | ||||||
|  |       }); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!loaded || typeof loaded !== "object") { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     let config = loaded; | ||||||
|  |  | ||||||
|  |     if ("script" in config) { | ||||||
|  |       config = config.script; | ||||||
|  |       if (Object.keys(config).length) { | ||||||
|  |         config = config[Object.keys(config)[0]]; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (Array.isArray(config)) { | ||||||
|  |       if (config.length === 1) { | ||||||
|  |         config = config[0]; | ||||||
|  |       } else { | ||||||
|  |         config = { sequence: config }; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!["sequence", "unknown"].includes(getActionType(config))) { | ||||||
|  |       config = { sequence: [config] }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     let normalized: ScriptConfig | undefined; |     let normalized: ScriptConfig | undefined; | ||||||
|  |  | ||||||
|     try { |     try { | ||||||
|         normalized = normalizeScriptConfig(loaded); |       normalized = normalizeScriptConfig(config); | ||||||
|     } catch (_err: any) { |     } catch (_err: any) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
| @@ -282,7 +323,6 @@ export class HaManualScriptEditor extends LitElement { | |||||||
|       // replace the config completely |       // replace the config completely | ||||||
|       this._replaceExistingConfig(normalized); |       this._replaceExistingConfig(normalized); | ||||||
|     } |     } | ||||||
|     } |  | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   private _appendToExistingConfig(config: ScriptConfig) { |   private _appendToExistingConfig(config: ScriptConfig) { | ||||||
|   | |||||||
| @@ -62,6 +62,7 @@ export class HaConfigTags extends SubscribeMixin(LitElement) { | |||||||
|     return this.hass.auth.external?.config.canWriteTag; |     return this.hass.auth.external?.config.canWriteTag; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "tags-table-search", |     key: "tags-table-search", | ||||||
|   | |||||||
| @@ -76,6 +76,7 @@ export class VoiceAssistantsExpose extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _extEntities?: Record<string, ExtEntityRegistryEntry>; |   @state() private _extEntities?: Record<string, ExtEntityRegistryEntry>; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     storage: "sessionStorage", |     storage: "sessionStorage", | ||||||
|     key: "voice-expose-table-search", |     key: "voice-expose-table-search", | ||||||
|   | |||||||
| @@ -52,6 +52,7 @@ class HaPanelDevAction extends LitElement { | |||||||
|  |  | ||||||
|   private _yamlValid = true; |   private _yamlValid = true; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-action-state-service-data", |     key: "panel-dev-action-state-service-data", | ||||||
|     state: true, |     state: true, | ||||||
| @@ -59,6 +60,7 @@ class HaPanelDevAction extends LitElement { | |||||||
|   }) |   }) | ||||||
|   private _serviceData?: ServiceAction = { action: "", target: {}, data: {} }; |   private _serviceData?: ServiceAction = { action: "", target: {}, data: {} }; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "panel-dev-action-state-yaml-mode", |     key: "panel-dev-action-state-yaml-mode", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -33,6 +33,7 @@ class HaPanelDevAssist extends SubscribeMixin(LitElement) { | |||||||
|  |  | ||||||
|   @state() supportedLanguages?: string[]; |   @state() supportedLanguages?: string[]; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "assist_debug_language", |     key: "assist_debug_language", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -62,6 +62,7 @@ class HaPanelDevState extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _validJSON = true; |   @state() private _validJSON = true; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "devToolsShowAttributes", |     key: "devToolsShowAttributes", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -22,11 +22,14 @@ import type { | |||||||
|   DeviceConsumptionEnergyPreference, |   DeviceConsumptionEnergyPreference, | ||||||
| } from "../../data/energy"; | } from "../../data/energy"; | ||||||
| import { | import { | ||||||
|  |   computeConsumptionData, | ||||||
|   getEnergyDataCollection, |   getEnergyDataCollection, | ||||||
|   getEnergyGasUnit, |   getEnergyGasUnit, | ||||||
|   getEnergyWaterUnit, |   getEnergyWaterUnit, | ||||||
|  |   getSummedData, | ||||||
| } from "../../data/energy"; | } from "../../data/energy"; | ||||||
| import { fileDownload } from "../../util/file_download"; | import { fileDownload } from "../../util/file_download"; | ||||||
|  | import type { StatisticValue } from "../../data/recorder"; | ||||||
|  |  | ||||||
| const ENERGY_LOVELACE_CONFIG: LovelaceConfig = { | const ENERGY_LOVELACE_CONFIG: LovelaceConfig = { | ||||||
|   views: [ |   views: [ | ||||||
| @@ -177,18 +180,20 @@ class PanelEnergy extends LitElement { | |||||||
|     const csv: string[] = []; |     const csv: string[] = []; | ||||||
|     csv[0] = headers; |     csv[0] = headers; | ||||||
|  |  | ||||||
|     const processStat = function (stat: string, type: string, unit: string) { |     const processCsvRow = function ( | ||||||
|  |       id: string, | ||||||
|  |       type: string, | ||||||
|  |       unit: string, | ||||||
|  |       data: StatisticValue[] | ||||||
|  |     ) { | ||||||
|       let n = 0; |       let n = 0; | ||||||
|       const row: string[] = []; |       const row: string[] = []; | ||||||
|       if (!stats[stat]) { |       row.push(id); | ||||||
|         return; |  | ||||||
|       } |  | ||||||
|       row.push(stat); |  | ||||||
|       row.push(type); |       row.push(type); | ||||||
|       row.push(unit.normalize("NFKD")); |       row.push(unit.normalize("NFKD")); | ||||||
|       times.forEach((t) => { |       times.forEach((t) => { | ||||||
|         if (n < stats[stat].length && stats[stat][n].start === t) { |         if (n < data.length && data[n].start === t) { | ||||||
|           row.push((stats[stat][n].change ?? "").toString()); |           row.push((data[n].change ?? "").toString()); | ||||||
|           n++; |           n++; | ||||||
|         } else { |         } else { | ||||||
|           row.push(""); |           row.push(""); | ||||||
| @@ -197,6 +202,14 @@ class PanelEnergy extends LitElement { | |||||||
|       csv.push(row.join(",") + "\n"); |       csv.push(row.join(",") + "\n"); | ||||||
|     }; |     }; | ||||||
|  |  | ||||||
|  |     const processStat = function (stat: string, type: string, unit: string) { | ||||||
|  |       if (!stats[stat]) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       processCsvRow(stat, type, unit, stats[stat]); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|     const currency = this.hass.config.currency; |     const currency = this.hass.config.currency; | ||||||
|  |  | ||||||
|     const printCategory = function ( |     const printCategory = function ( | ||||||
| @@ -335,6 +348,99 @@ class PanelEnergy extends LitElement { | |||||||
|  |  | ||||||
|     printCategory("device_consumption", devices, electricUnit); |     printCategory("device_consumption", devices, electricUnit); | ||||||
|  |  | ||||||
|  |     const { summedData, compareSummedData: _ } = getSummedData( | ||||||
|  |       energyData.state | ||||||
|  |     ); | ||||||
|  |     const { consumption, compareConsumption: __ } = computeConsumptionData( | ||||||
|  |       summedData, | ||||||
|  |       undefined | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const processConsumptionData = function ( | ||||||
|  |       type: string, | ||||||
|  |       unit: string, | ||||||
|  |       data: Record<number, number> | ||||||
|  |     ) { | ||||||
|  |       const data2: StatisticValue[] = []; | ||||||
|  |  | ||||||
|  |       Object.entries(data).forEach(([t, value]) => { | ||||||
|  |         data2.push({ | ||||||
|  |           start: Number(t), | ||||||
|  |           end: NaN, | ||||||
|  |           change: value, | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       processCsvRow("", type, unit, data2); | ||||||
|  |     }; | ||||||
|  |  | ||||||
|  |     const hasSolar = !!solar_productions.length; | ||||||
|  |     const hasBattery = !!battery_ins.length; | ||||||
|  |     const hasGridReturn = !!grid_productions.length; | ||||||
|  |     const hasGridSource = !!grid_consumptions.length; | ||||||
|  |  | ||||||
|  |     if (hasGridSource) { | ||||||
|  |       processConsumptionData( | ||||||
|  |         "calculated_consumed_grid", | ||||||
|  |         electricUnit, | ||||||
|  |         consumption.used_grid | ||||||
|  |       ); | ||||||
|  |       if (hasBattery) { | ||||||
|  |         processConsumptionData( | ||||||
|  |           "calculated_grid_to_battery", | ||||||
|  |           electricUnit, | ||||||
|  |           consumption.grid_to_battery | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (hasGridReturn && hasBattery) { | ||||||
|  |       processConsumptionData( | ||||||
|  |         "calculated_battery_to_grid", | ||||||
|  |         electricUnit, | ||||||
|  |         consumption.battery_to_grid | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     if (hasBattery) { | ||||||
|  |       processConsumptionData( | ||||||
|  |         "calculated_consumed_battery", | ||||||
|  |         electricUnit, | ||||||
|  |         consumption.used_battery | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (hasSolar) { | ||||||
|  |       processConsumptionData( | ||||||
|  |         "calculated_consumed_solar", | ||||||
|  |         electricUnit, | ||||||
|  |         consumption.used_solar | ||||||
|  |       ); | ||||||
|  |       if (hasBattery) { | ||||||
|  |         processConsumptionData( | ||||||
|  |           "calculated_solar_to_battery", | ||||||
|  |           electricUnit, | ||||||
|  |           consumption.solar_to_battery | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |       if (hasGridReturn) { | ||||||
|  |         processConsumptionData( | ||||||
|  |           "calculated_solar_to_grid", | ||||||
|  |           electricUnit, | ||||||
|  |           consumption.solar_to_grid | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       (hasGridSource ? 1 : 0) + (hasSolar ? 1 : 0) + (hasBattery ? 1 : 0) > | ||||||
|  |       1 | ||||||
|  |     ) { | ||||||
|  |       processConsumptionData( | ||||||
|  |         "calculated_total_consumption", | ||||||
|  |         electricUnit, | ||||||
|  |         consumption.used_total | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     const blob = new Blob(csv, { |     const blob = new Blob(csv, { | ||||||
|       type: "text/csv", |       type: "text/csv", | ||||||
|     }); |     }); | ||||||
|   | |||||||
| @@ -63,6 +63,7 @@ class HaPanelHistory extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _endDate: Date; |   @state() private _endDate: Date; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "historyPickedValue", |     key: "historyPickedValue", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -39,6 +39,7 @@ export class HaPanelLogbook extends LitElement { | |||||||
|   @state() |   @state() | ||||||
|   private _showBack?: boolean; |   private _showBack?: boolean; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "logbookPickedValue", |     key: "logbookPickedValue", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -210,10 +210,6 @@ export class HuiViewBadges extends LitElement { | |||||||
|       min-width: fit-content; |       min-width: fit-content; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     .badges > *:last-child:not(.add) { |  | ||||||
|       padding-right: var(--badges-padding-right, 0); |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     hui-badge-edit-mode { |     hui-badge-edit-mode { | ||||||
|       display: block; |       display: block; | ||||||
|       position: relative; |       position: relative; | ||||||
|   | |||||||
| @@ -62,6 +62,7 @@ export class HuiEnergyDevicesDetailGraphCard | |||||||
|  |  | ||||||
|   @state() private _compareEnd?: Date; |   @state() private _compareEnd?: Date; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "energy-devices-hidden-stats", |     key: "energy-devices-hidden-stats", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
| @@ -86,6 +86,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { | |||||||
|  |  | ||||||
|   @state() private _config?: ButtonCardConfig; |   @state() private _config?: ButtonCardConfig; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @consume<any>({ context: statesContext, subscribe: true }) |   @consume<any>({ context: statesContext, subscribe: true }) | ||||||
|   @transform({ |   @transform({ | ||||||
|     transformer: function (this: HuiButtonCard, value: HassEntities) { |     transformer: function (this: HuiButtonCard, value: HassEntities) { | ||||||
| @@ -111,6 +112,7 @@ export class HuiButtonCard extends LitElement implements LovelaceCard { | |||||||
|   @consume({ context: configContext, subscribe: true }) |   @consume({ context: configContext, subscribe: true }) | ||||||
|   _hassConfig!: HassConfig; |   _hassConfig!: HassConfig; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @consume<any>({ context: entitiesContext, subscribe: true }) |   @consume<any>({ context: entitiesContext, subscribe: true }) | ||||||
|   @transform<HomeAssistant["entities"], EntityRegistryDisplayEntry>({ |   @transform<HomeAssistant["entities"], EntityRegistryDisplayEntry>({ | ||||||
|     transformer: function (this: HuiButtonCard, value) { |     transformer: function (this: HuiButtonCard, value) { | ||||||
|   | |||||||
| @@ -109,12 +109,18 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { | |||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (isNaN(entityState)) { |     const valueToDisplay = this._config.attribute | ||||||
|  |       ? stateObj.attributes[this._config.attribute] | ||||||
|  |       : stateObj.state; | ||||||
|  |  | ||||||
|  |     if (isNaN(valueToDisplay)) { | ||||||
|       return html` |       return html` | ||||||
|         <hui-warning |         <hui-warning | ||||||
|           >${this.hass.localize( |           >${this.hass.localize( | ||||||
|             "ui.panel.lovelace.warning.entity_non_numeric", |             this._config.attribute | ||||||
|             { entity: this._config.entity } |               ? "ui.panel.lovelace.warning.attribute_not_numeric" | ||||||
|  |               : "ui.panel.lovelace.warning.entity_non_numeric", | ||||||
|  |             { entity: this._config.entity, attribute: this._config.attribute } | ||||||
|           )}</hui-warning |           )}</hui-warning | ||||||
|         > |         > | ||||||
|       `; |       `; | ||||||
| @@ -141,7 +147,7 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { | |||||||
|         <ha-gauge |         <ha-gauge | ||||||
|           .min=${this._config.min!} |           .min=${this._config.min!} | ||||||
|           .max=${this._config.max!} |           .max=${this._config.max!} | ||||||
|           .value=${stateObj.state} |           .value=${valueToDisplay} | ||||||
|           .formatOptions=${getNumberFormatOptions( |           .formatOptions=${getNumberFormatOptions( | ||||||
|             stateObj, |             stateObj, | ||||||
|             this.hass.entities[stateObj.entity_id] |             this.hass.entities[stateObj.entity_id] | ||||||
|   | |||||||
| @@ -268,6 +268,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | |||||||
|         <div |         <div | ||||||
|           class="content ${classMap({ |           class="content ${classMap({ | ||||||
|             "has-header": !!this._config.title, |             "has-header": !!this._config.title, | ||||||
|  |             "has-rows": !!this._config.grid_options?.rows, | ||||||
|           })}" |           })}" | ||||||
|         > |         > | ||||||
|           <statistics-chart |           <statistics-chart | ||||||
| @@ -389,6 +390,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | |||||||
|     statistics-chart { |     statistics-chart { | ||||||
|       height: 100%; |       height: 100%; | ||||||
|     } |     } | ||||||
|  |     .has-rows { | ||||||
|  |       --chart-max-height: 100%; | ||||||
|  |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -233,6 +233,7 @@ export interface GaugeSegment { | |||||||
|  |  | ||||||
| export interface GaugeCardConfig extends LovelaceCardConfig { | export interface GaugeCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|  |   attribute?: string; | ||||||
|   name?: string; |   name?: string; | ||||||
|   unit?: string; |   unit?: string; | ||||||
|   min?: number; |   min?: number; | ||||||
|   | |||||||
| @@ -163,6 +163,7 @@ export class HuiGenericEntityRow extends LitElement { | |||||||
|               @touchend=${stopPropagation} |               @touchend=${stopPropagation} | ||||||
|               @keydown=${stopPropagation} |               @keydown=${stopPropagation} | ||||||
|               @click=${stopPropagation} |               @click=${stopPropagation} | ||||||
|  |               @action=${stopPropagation} | ||||||
|             ></slot>`} |             ></slot>`} | ||||||
|       </div> |       </div> | ||||||
|     `; |     `; | ||||||
|   | |||||||
| @@ -43,6 +43,7 @@ export class HuiBadgePicker extends LitElement { | |||||||
|  |  | ||||||
|   @property({ attribute: false }) public suggestedBadges?: string[]; |   @property({ attribute: false }) public suggestedBadges?: string[]; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|   @storage({ |   @storage({ | ||||||
|     key: "dashboardBadgeClipboard", |     key: "dashboardBadgeClipboard", | ||||||
|     state: true, |     state: true, | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user