mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 06:29:43 +00:00 
			
		
		
		
	Compare commits
	
		
			118 Commits
		
	
	
		
			chat-log-s
			...
			rc
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 0408734ec5 | ||
|   | 317519fc08 | ||
|   | 843d79eab4 | ||
|   | 165a757f06 | ||
|   | ea8b730142 | ||
|   | e88c97d625 | ||
|   | 7560988b76 | ||
|   | eecd8077b6 | ||
|   | cbab5c3f7b | ||
|   | a5d27c8bb8 | ||
|   | a6a340b5db | ||
|   | 8f5875c30f | ||
|   | 517cd49f35 | ||
|   | 25d9fc94b2 | ||
|   | 7b188759e3 | ||
|   | 76772d1098 | ||
|   | 6052745ca0 | ||
|   | 89b9780345 | ||
|   | a607edca96 | ||
|   | 52eb3d8063 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 1361fc36bf | ||
|   | 505ef2bd11 | ||
|   | c0cc66c1ab | ||
|   | 7cfbc521c7 | ||
|   | e064ce56cc | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 8d688aa3a9 | ||
|   | d122483449 | ||
|   | f17bbc3f79 | ||
|   | c88f8fcce0 | ||
|   | 8efabde916 | ||
|   | e821e1ec83 | ||
|   | dc7516da94 | ||
|   | a545a377a7 | ||
|   | 3634dbcbbf | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 75af4f939e | ||
|   | 453a2ac7f3 | ||
|   | 8fbd0226fc | ||
|   | 2a8d935601 | ||
|   | a6328fb6d7 | ||
|   | a78b61006f | ||
|   | d506aa23b6 | ||
|   | 48b4df43ab | ||
|   | 8cdcd9cb55 | ||
|   | a1e2ac1d99 | ||
|   | 8ecddbc42c | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 6f70ef52a5 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 7dff02d7c8 | ||
|   | 8bbd7a6a06 | ||
|   | 5c73a06f76 | ||
|   | 9943dae82c | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 70bf049df0 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | f9d9fbb7f0 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 9cb84d3f37 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c1bcf27cf8 | ||
|   | 3e749ec085 | ||
|   | ee2ec00069 | ||
|   | 0aa2941868 | ||
|   | 46cd1d5156 | ||
|   | 07a5c41fd4 | ||
|   | 4ad3c553d5 | ||
|   | d40cc448a5 | ||
|   | e2f3f9d348 | ||
|   | 98d44950f8 | ||
|   | 8ae9edb1ef | ||
|   | 84c4396c13 | ||
|   | 2b937a30e3 | ||
|   | b7815bfd86 | ||
|   | d94fa03411 | ||
|   | 0a7007ef9e | ||
|   | dd12136dee | ||
|   | 6e2f89fe3d | ||
|   | 092085b9af | ||
|   | 1c06eb8661 | ||
|   | c7e87b06b5 | ||
|   | 38c738c199 | ||
|   | e899587307 | ||
|   | c9feb0b75f | ||
|   | 10718c35d1 | ||
|   | 4dc6a37bad | ||
|   | ac49fc7aba | ||
|   | e4f008800b | ||
|   | 0b0ffd7bab | ||
|   | dfa77526a2 | ||
|   | 9a3bd6c613 | ||
|   | 1161de5746 | ||
|   | 9df8e20391 | ||
|   | 11047a9c95 | ||
|   | 18fa66f61c | ||
|   | 758a048f34 | ||
|   | ee0fc360b0 | ||
|   | 4012f95ec1 | ||
|   | 0336ce4606 | ||
|   | 9ba36ab7e2 | ||
|   | fe7a08a1b0 | ||
|   | 87a8f9cedc | ||
|   | 01df7e20ca | ||
|   | d181219522 | ||
|   | 6ae24b8135 | ||
|   | 8e009f24f9 | ||
|   | 53031f44ac | ||
|   | af5a988457 | ||
|   | bab0391a19 | ||
|   | 444123c47e | ||
|   | f123d34046 | ||
|   | 1b40f99f68 | ||
|   | b314b3ed2b | ||
|   | 59b8932969 | ||
|   | 107af753ec | ||
|   | 1f0acb3046 | ||
|   | 431e533929 | ||
|   | 02c845cbc6 | ||
|   | 628111ed20 | ||
|   | e825a9c02f | ||
|   | 7a35bddf36 | ||
|   | ad69270af8 | ||
|   | 404edf9483 | ||
|   | a166b4e9b6 | ||
|   | 7a285f11db | 
							
								
								
									
										4
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/ci.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -89,7 +89,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: frontend-bundle-stats | ||||
|           path: build/stats/*.json | ||||
| @@ -113,7 +113,7 @@ jobs: | ||||
|         env: | ||||
|           IS_TEST: "true" | ||||
|       - name: Upload bundle stats | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: supervisor-bundle-stats | ||||
|           path: build/stats/*.json | ||||
|   | ||||
							
								
								
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										6
									
								
								.github/workflows/codeql-analysis.yml
									
									
									
									
										vendored
									
									
								
							| @@ -36,14 +36,14 @@ jobs: | ||||
|  | ||||
|       # Initializes the CodeQL tools for scanning. | ||||
|       - name: Initialize CodeQL | ||||
|         uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 | ||||
|         uses: github/codeql-action/init@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|         with: | ||||
|           languages: ${{ matrix.language }} | ||||
|  | ||||
|       # Autobuild attempts to build any compiled languages  (C/C++, C#, or Java). | ||||
|       # If this step fails, then you should remove it and run the build manually (see below) | ||||
|       - name: Autobuild | ||||
|         uses: github/codeql-action/autobuild@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 | ||||
|         uses: github/codeql-action/autobuild@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|  | ||||
|       # ℹ️ Command-line programs to run using the OS shell. | ||||
|       # 📚 https://git.io/JvXDl | ||||
| @@ -57,4 +57,4 @@ jobs: | ||||
|       #   make release | ||||
|  | ||||
|       - name: Perform CodeQL Analysis | ||||
|         uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9 | ||||
|         uses: github/codeql-action/analyze@4e94bd11f71e507f7f87df81788dff88d1dacbfb # v4.31.0 | ||||
|   | ||||
							
								
								
									
										4
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/nightly.yaml
									
									
									
									
										vendored
									
									
								
							| @@ -57,14 +57,14 @@ jobs: | ||||
|         run: tar -czvf translations.tar.gz translations | ||||
|  | ||||
|       - name: Upload build artifacts | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: wheels | ||||
|           path: dist/home_assistant_frontend*.whl | ||||
|           if-no-files-found: error | ||||
|  | ||||
|       - name: Upload translations | ||||
|         uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 | ||||
|         uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0 | ||||
|         with: | ||||
|           name: translations | ||||
|           path: translations.tar.gz | ||||
|   | ||||
| @@ -16,9 +16,9 @@ import { | ||||
| } from "../../../../src/common/auth/token_storage"; | ||||
| import { atLeastVersion } from "../../../../src/common/config/version"; | ||||
| import { toggleAttribute } from "../../../../src/common/dom/toggle_attribute"; | ||||
| import "../../../../src/components/ha-button"; | ||||
| import "../../../../src/components/ha-icon"; | ||||
| import "../../../../src/components/ha-list"; | ||||
| import "../../../../src/components/ha-button"; | ||||
| import "../../../../src/components/ha-list-item"; | ||||
| import "../../../../src/components/ha-svg-icon"; | ||||
| import { | ||||
| @@ -28,7 +28,6 @@ import { | ||||
| import { isStrategyDashboard } from "../../../../src/data/lovelace/config/types"; | ||||
| import type { LovelaceViewConfig } from "../../../../src/data/lovelace/config/view"; | ||||
| import "../../../../src/layouts/hass-loading-screen"; | ||||
| import { generateDefaultViewConfig } from "../../../../src/panels/lovelace/common/generate-lovelace-config"; | ||||
| import "./hc-layout"; | ||||
|  | ||||
| @customElement("hc-cast") | ||||
| @@ -96,7 +95,9 @@ class HcCast extends LitElement { | ||||
|                 <ha-list @action=${this._handlePickView} activatable> | ||||
|                   ${( | ||||
|                     this.lovelaceViews ?? [ | ||||
|                       generateDefaultViewConfig({}, {}, {}, {}, () => ""), | ||||
|                       { | ||||
|                         title: "Home", | ||||
|                       }, | ||||
|                     ] | ||||
|                   ).map( | ||||
|                     (view, idx) => html` | ||||
|   | ||||
| @@ -5,14 +5,14 @@ subtitle: Dialogs provide important prompts in a user flow. | ||||
|  | ||||
| # Material Design 3 | ||||
|  | ||||
| Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guideliness. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview). | ||||
| Our dialogs are based on the latest version of Material Design. Please note that we have made some well-considered adjustments to these guidelines. Specs and guidelines can be found on its [website](https://m3.material.io/components/dialogs/overview). | ||||
|  | ||||
| # Guidelines | ||||
|  | ||||
| ## Design | ||||
|  | ||||
| - Dialogs have a max width of 560px. Alert and confirmation dialogs got a fixed width of 320px. If you need more width, consider a dedicated page instead. | ||||
| - The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guideliness. | ||||
| - Dialogs have a max width of 560px. Alert and confirmation dialogs have a fixed width of 320px. If you need more width, consider a dedicated page instead. | ||||
| - The close X-icon is on the top left, on all screen sizes. Except for alert and confirmation dialogs, they only have buttons and no X-icon. This is different compared to the Material guidelines. | ||||
| - Dialogs can't be closed with ESC or clicked outside of the dialog when there is a form that the user needs to fill out. Instead it will animate "no" by a little shake. | ||||
| - Extra icon buttons are on the top right, for example help, settings and expand dialog. More than 2 icon buttons, they will be in an overflow menu. | ||||
| - The submit button is grouped with a cancel button at the bottom right, on all screen sizes. Fullscreen mobile dialogs have them sticky at the bottom. | ||||
| @@ -26,7 +26,7 @@ Our dialogs are based on the latest version of Material Design. Please note that | ||||
|  | ||||
| - A best practice is to always use a title, even if it is optional by Material guidelines. | ||||
| - People mainly read the title and a button. Put the most important information in those two. | ||||
| - Try to avoid user generated content in the title, this could make the title unreadable long. | ||||
| - Try to avoid user generated content in the title, this could make the title unreadably long. | ||||
| - If users become unsure, they read the description. Make sure this explains what will happen. | ||||
| - Strive for minimalism. | ||||
|  | ||||
|   | ||||
							
								
								
									
										21
									
								
								package.json
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								package.json
									
									
									
									
									
								
							| @@ -53,7 +53,7 @@ | ||||
|     "@fullcalendar/luxon3": "6.1.19", | ||||
|     "@fullcalendar/timegrid": "6.1.19", | ||||
|     "@home-assistant/webawesome": "3.0.0-beta.6.ha.6", | ||||
|     "@lezer/highlight": "1.2.2", | ||||
|     "@lezer/highlight": "1.2.3", | ||||
|     "@lit-labs/motion": "1.0.9", | ||||
|     "@lit-labs/observers": "2.0.6", | ||||
|     "@lit-labs/virtualizer": "2.1.1", | ||||
| @@ -148,10 +148,10 @@ | ||||
|     "xss": "1.0.15" | ||||
|   }, | ||||
|   "devDependencies": { | ||||
|     "@babel/core": "7.28.4", | ||||
|     "@babel/core": "7.28.5", | ||||
|     "@babel/helper-define-polyfill-provider": "0.6.5", | ||||
|     "@babel/plugin-transform-runtime": "7.28.3", | ||||
|     "@babel/preset-env": "7.28.3", | ||||
|     "@babel/plugin-transform-runtime": "7.28.5", | ||||
|     "@babel/preset-env": "7.28.5", | ||||
|     "@bundle-stats/plugin-webpack-filter": "4.21.5", | ||||
|     "@lokalise/node-api": "15.3.1", | ||||
|     "@octokit/auth-oauth-device": "8.0.2", | ||||
| @@ -173,12 +173,12 @@ | ||||
|     "@types/lodash.merge": "4.6.9", | ||||
|     "@types/luxon": "3.7.1", | ||||
|     "@types/mocha": "10.0.10", | ||||
|     "@types/qrcode": "1.5.5", | ||||
|     "@types/sortablejs": "1.15.8", | ||||
|     "@types/qrcode": "1.5.6", | ||||
|     "@types/sortablejs": "1.15.9", | ||||
|     "@types/tar": "6.1.13", | ||||
|     "@types/ua-parser-js": "0.7.39", | ||||
|     "@types/webspeechapi": "0.0.29", | ||||
|     "@vitest/coverage-v8": "4.0.1", | ||||
|     "@vitest/coverage-v8": "4.0.3", | ||||
|     "babel-loader": "10.0.0", | ||||
|     "babel-plugin-template-html-minifier": "4.1.0", | ||||
|     "browserslist-useragent-regexp": "4.1.3", | ||||
| @@ -219,7 +219,7 @@ | ||||
|     "typescript": "5.9.3", | ||||
|     "typescript-eslint": "8.46.2", | ||||
|     "vite-tsconfig-paths": "5.1.4", | ||||
|     "vitest": "4.0.1", | ||||
|     "vitest": "4.0.3", | ||||
|     "webpack-stats-plugin": "1.1.3", | ||||
|     "webpackbar": "7.0.0", | ||||
|     "workbox-build": "patch:workbox-build@npm%3A7.1.1#~/.yarn/patches/workbox-build-npm-7.1.1-a854f3faae.patch" | ||||
| @@ -235,5 +235,8 @@ | ||||
|     "tslib": "2.8.1", | ||||
|     "@material/mwc-list@^0.27.0": "patch:@material/mwc-list@npm%3A0.27.0#~/.yarn/patches/@material-mwc-list-npm-0.27.0-5344fc9de4.patch" | ||||
|   }, | ||||
|   "packageManager": "yarn@4.10.3" | ||||
|   "packageManager": "yarn@4.10.3", | ||||
|   "volta": { | ||||
|     "node": "22.21.1" | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" | ||||
|  | ||||
| [project] | ||||
| name         = "home-assistant-frontend" | ||||
| version      = "20250924.0" | ||||
| version      = "20251029.1" | ||||
| license      = "Apache-2.0" | ||||
| license-files = ["LICENSE*"] | ||||
| description  = "The Home Assistant frontend" | ||||
|   | ||||
							
								
								
									
										116
									
								
								src/common/util/swipe-gesture-recognizer.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										116
									
								
								src/common/util/swipe-gesture-recognizer.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,116 @@ | ||||
| export interface SwipeGestureResult { | ||||
|   velocity: number; | ||||
|   delta: number; | ||||
|   isSwipe: boolean; | ||||
|   isDownwardSwipe: boolean; | ||||
| } | ||||
|  | ||||
| export interface SwipeGestureConfig { | ||||
|   velocitySwipeThreshold?: number; | ||||
|   movementTimeThreshold?: number; | ||||
| } | ||||
|  | ||||
| const VELOCITY_SWIPE_THRESHOLD = 0.5; // px/ms | ||||
| const MOVEMENT_TIME_THRESHOLD = 100; // ms | ||||
|  | ||||
| /** | ||||
|  * Recognizes swipe gestures and calculates velocity for touch interactions. | ||||
|  * Tracks touch movement and provides velocity-based and position-based gesture detection. | ||||
|  */ | ||||
| export class SwipeGestureRecognizer { | ||||
|   private _startY = 0; | ||||
|  | ||||
|   private _delta = 0; | ||||
|  | ||||
|   private _startTime = 0; | ||||
|  | ||||
|   private _lastY = 0; | ||||
|  | ||||
|   private _lastTime = 0; | ||||
|  | ||||
|   private _velocityThreshold: number; | ||||
|  | ||||
|   private _movementTimeThreshold: number; | ||||
|  | ||||
|   constructor(config: SwipeGestureConfig = {}) { | ||||
|     this._velocityThreshold = | ||||
|       config.velocitySwipeThreshold ?? VELOCITY_SWIPE_THRESHOLD; // px/ms | ||||
|     this._movementTimeThreshold = | ||||
|       config.movementTimeThreshold ?? MOVEMENT_TIME_THRESHOLD; // ms | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Initialize gesture tracking with starting touch position | ||||
|    */ | ||||
|   public start(clientY: number): void { | ||||
|     const now = Date.now(); | ||||
|     this._startY = clientY; | ||||
|     this._startTime = now; | ||||
|     this._lastY = clientY; | ||||
|     this._lastTime = now; | ||||
|     this._delta = 0; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Update gesture state during movement | ||||
|    * Returns the current delta (negative when dragging down) | ||||
|    */ | ||||
|   public move(clientY: number): number { | ||||
|     const now = Date.now(); | ||||
|     this._delta = this._startY - clientY; | ||||
|     this._lastY = clientY; | ||||
|     this._lastTime = now; | ||||
|     return this._delta; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Calculate final gesture result when touch ends | ||||
|    */ | ||||
|   public end(): SwipeGestureResult { | ||||
|     const velocity = this.getVelocity(); | ||||
|     const hasSignificantVelocity = Math.abs(velocity) > this._velocityThreshold; | ||||
|  | ||||
|     return { | ||||
|       velocity, | ||||
|       delta: this._delta, | ||||
|       isSwipe: hasSignificantVelocity, | ||||
|       isDownwardSwipe: velocity > 0, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Get current drag delta (negative when dragging down) | ||||
|    */ | ||||
|   public getDelta(): number { | ||||
|     return this._delta; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Calculate velocity based on recent movement | ||||
|    * Returns 0 if no recent movement detected | ||||
|    * Positive velocity means downward swipe | ||||
|    */ | ||||
|   public getVelocity(): number { | ||||
|     const now = Date.now(); | ||||
|     const timeSinceLastMove = now - this._lastTime; | ||||
|  | ||||
|     // Only consider velocity if the last movement was recent | ||||
|     if (timeSinceLastMove >= this._movementTimeThreshold) { | ||||
|       return 0; | ||||
|     } | ||||
|  | ||||
|     const timeDelta = this._lastTime - this._startTime; | ||||
|     return timeDelta > 0 ? (this._lastY - this._startY) / timeDelta : 0; | ||||
|   } | ||||
|  | ||||
|   /** | ||||
|    * Reset all tracking state | ||||
|    */ | ||||
|   public reset(): void { | ||||
|     this._startY = 0; | ||||
|     this._delta = 0; | ||||
|     this._startTime = 0; | ||||
|     this._lastY = 0; | ||||
|     this._lastTime = 0; | ||||
|   } | ||||
| } | ||||
| @@ -35,6 +35,7 @@ export const MIN_TIME_BETWEEN_UPDATES = 60 * 5 * 1000; | ||||
| const LEGEND_OVERFLOW_LIMIT = 10; | ||||
| const LEGEND_OVERFLOW_LIMIT_MOBILE = 6; | ||||
| const DOUBLE_TAP_TIME = 300; | ||||
| const RESIZE_ANIMATION_DURATION = 250; | ||||
|  | ||||
| export type CustomLegendOption = ECOption["legend"] & { | ||||
|   type: "custom"; | ||||
| @@ -205,6 +206,15 @@ export class HaChartBase extends LitElement { | ||||
|     } | ||||
|     if (changedProps.has("options")) { | ||||
|       chartOptions = { ...chartOptions, ...this._createOptions() }; | ||||
|       if ( | ||||
|         this._compareCustomLegendOptions( | ||||
|           changedProps.get("options"), | ||||
|           this.options | ||||
|         ) | ||||
|       ) { | ||||
|         // custom legend changes may require a resize to layout properly | ||||
|         this._shouldResizeChart = true; | ||||
|       } | ||||
|     } else if (this._isTouchDevice && changedProps.has("_isZoomed")) { | ||||
|       chartOptions.dataZoom = this._getDataZoomConfig(); | ||||
|     } | ||||
| @@ -296,7 +306,7 @@ export class HaChartBase extends LitElement { | ||||
|           itemStyle = { | ||||
|             color: dataset?.color as string, | ||||
|             ...(dataset?.itemStyle as { borderColor?: string }), | ||||
|             itemStyle, | ||||
|             ...itemStyle, | ||||
|           }; | ||||
|           const color = itemStyle?.color as string; | ||||
|           const borderColor = itemStyle?.borderColor as string; | ||||
| @@ -508,6 +518,7 @@ export class HaChartBase extends LitElement { | ||||
|         ); | ||||
|       } | ||||
|     }); | ||||
|     this.requestUpdate("_hiddenDatasets"); | ||||
|   } | ||||
|  | ||||
|   private _getDataZoomConfig(): DataZoomComponentOption | undefined { | ||||
| @@ -958,11 +969,31 @@ export class HaChartBase extends LitElement { | ||||
|  | ||||
|   private _handleChartRenderFinished = () => { | ||||
|     if (this._shouldResizeChart) { | ||||
|       this.chart?.resize(); | ||||
|       this.chart?.resize({ | ||||
|         animation: this._reducedMotion | ||||
|           ? undefined | ||||
|           : { duration: RESIZE_ANIMATION_DURATION }, | ||||
|       }); | ||||
|       this._shouldResizeChart = false; | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   private _compareCustomLegendOptions( | ||||
|     oldOptions: ECOption | undefined, | ||||
|     newOptions: ECOption | undefined | ||||
|   ): boolean { | ||||
|     const oldLegends = ensureArray( | ||||
|       oldOptions?.legend || [] | ||||
|     ) as LegendComponentOption[]; | ||||
|     const newLegends = ensureArray( | ||||
|       newOptions?.legend || [] | ||||
|     ) as LegendComponentOption[]; | ||||
|     return ( | ||||
|       oldLegends.some((l) => l.show && l.type === "custom") !== | ||||
|       newLegends.some((l) => l.show && l.type === "custom") | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     :host { | ||||
|       display: block; | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/components/entity/const.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/components/entity/const.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| export const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__"; | ||||
| @@ -147,7 +147,7 @@ class HaEntitiesPicker extends LitElement { | ||||
|           .createDomains=${this.createDomains} | ||||
|           .required=${this.required && !currentEntities.length} | ||||
|           @value-changed=${this._addEntity} | ||||
|           add-button | ||||
|           .addButton=${currentEntities.length > 0} | ||||
|         ></ha-entity-picker> | ||||
|       </div> | ||||
|     `; | ||||
|   | ||||
| @@ -312,7 +312,7 @@ export class HaEntityNamePicker extends LitElement { | ||||
|   private _toValue = memoizeOne( | ||||
|     (items: EntityNameItem[]): typeof this.value => { | ||||
|       if (items.length === 0) { | ||||
|         return ""; | ||||
|         return undefined; | ||||
|       } | ||||
|       if (items.length === 1) { | ||||
|         const item = items[0]; | ||||
|   | ||||
| @@ -4,6 +4,7 @@ import { customElement, property } from "lit/decorators"; | ||||
| import { keyed } from "lit/directives/keyed"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { ANY_STATE_VALUE } from "./const"; | ||||
| import { ensureArray } from "../../common/array/ensure-array"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import "./ha-entity-state-picker"; | ||||
| @@ -57,6 +58,7 @@ export class HaEntityStatesPicker extends LitElement { | ||||
|  | ||||
|     const value = this.value || []; | ||||
|     const hide = [...(this.hideStates || []), ...value]; | ||||
|     const hideValue = value.includes(ANY_STATE_VALUE); | ||||
|  | ||||
|     return html` | ||||
|       ${repeat( | ||||
| @@ -84,7 +86,7 @@ export class HaEntityStatesPicker extends LitElement { | ||||
|         ` | ||||
|       )} | ||||
|       <div> | ||||
|         ${this.disabled && value.length | ||||
|         ${(this.disabled && value.length) || hideValue | ||||
|           ? nothing | ||||
|           : keyed( | ||||
|               value.length, | ||||
|   | ||||
| @@ -118,7 +118,7 @@ export class HaAutomationRow extends LitElement { | ||||
|     } | ||||
|     .row { | ||||
|       display: flex; | ||||
|       padding: 0 8px; | ||||
|       padding: var(--ha-space-0) var(--ha-space-2); | ||||
|       min-height: 48px; | ||||
|       align-items: center; | ||||
|       cursor: pointer; | ||||
| @@ -134,12 +134,12 @@ export class HaAutomationRow extends LitElement { | ||||
|     .expand-button { | ||||
|       transition: transform 150ms cubic-bezier(0.4, 0, 0.2, 1); | ||||
|       color: var(--ha-color-on-neutral-quiet); | ||||
|       margin-left: -8px; | ||||
|       margin-left: calc(var(--ha-space-2) * -1); | ||||
|     } | ||||
|     :host([building-block]) .leading-icon-wrapper { | ||||
|       background-color: var(--ha-color-fill-neutral-loud-resting); | ||||
|       border-radius: var(--ha-border-radius-md); | ||||
|       padding: 4px; | ||||
|       padding: var(--ha-space-1); | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       align-items: center; | ||||
| @@ -149,7 +149,7 @@ export class HaAutomationRow extends LitElement { | ||||
|       color: var(--ha-color-on-neutral-quiet); | ||||
|     } | ||||
|     :host([building-block]) ::slotted([slot="leading-icon"]) { | ||||
|       --mdc-icon-size: 20px; | ||||
|       --mdc-icon-size: var(--ha-space-5); | ||||
|       color: var(--white-color); | ||||
|       transform: rotate(-45deg); | ||||
|     } | ||||
| @@ -170,7 +170,7 @@ export class HaAutomationRow extends LitElement { | ||||
|     ::slotted([slot="header"]) { | ||||
|       flex: 1; | ||||
|       overflow-wrap: anywhere; | ||||
|       margin: 0 12px; | ||||
|       margin: var(--ha-space-0) var(--ha-space-3); | ||||
|     } | ||||
|     :host([sort-selected]) .row { | ||||
|       outline: solid; | ||||
|   | ||||
| @@ -1,6 +1,7 @@ | ||||
| import "@home-assistant/webawesome/dist/components/drawer/drawer"; | ||||
| import { css, html, LitElement, type PropertyValues } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { SwipeGestureRecognizer } from "../common/util/swipe-gesture-recognizer"; | ||||
| import { haStyleScrollbar } from "../resources/styles"; | ||||
|  | ||||
| export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; | ||||
| @@ -14,6 +15,12 @@ export class HaBottomSheet extends LitElement { | ||||
|  | ||||
|   @state() private _drawerOpen = false; | ||||
|  | ||||
|   @query("#drawer") private _drawer!: HTMLElement; | ||||
|  | ||||
|   private _gestureRecognizer = new SwipeGestureRecognizer(); | ||||
|  | ||||
|   private _isDragging = false; | ||||
|  | ||||
|   private _handleAfterHide() { | ||||
|     this.open = false; | ||||
|     const ev = new Event("closed", { | ||||
| @@ -33,19 +40,132 @@ export class HaBottomSheet extends LitElement { | ||||
|   render() { | ||||
|     return html` | ||||
|       <wa-drawer | ||||
|         id="drawer" | ||||
|         placement="bottom" | ||||
|         .open=${this._drawerOpen} | ||||
|         @wa-after-hide=${this._handleAfterHide} | ||||
|         without-header | ||||
|         @touchstart=${this._handleTouchStart} | ||||
|       > | ||||
|         <slot name="header"></slot> | ||||
|         <div class="body ha-scrollbar"> | ||||
|         <div id="body" class="body ha-scrollbar"> | ||||
|           <slot></slot> | ||||
|         </div> | ||||
|       </wa-drawer> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _handleTouchStart = (ev: TouchEvent) => { | ||||
|     // Check if any element inside drawer in the composed path has scrollTop > 0 | ||||
|     for (const path of ev.composedPath()) { | ||||
|       const el = path as HTMLElement; | ||||
|       if (el === this._drawer) { | ||||
|         break; | ||||
|       } | ||||
|       if (el.scrollTop > 0) { | ||||
|         return; | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     this._startResizing(ev.touches[0].clientY); | ||||
|   }; | ||||
|  | ||||
|   private _startResizing(clientY: number) { | ||||
|     // register event listeners for drag handling | ||||
|     document.addEventListener("touchmove", this._handleTouchMove, { | ||||
|       passive: false, | ||||
|     }); | ||||
|     document.addEventListener("touchend", this._handleTouchEnd); | ||||
|     document.addEventListener("touchcancel", this._handleTouchEnd); | ||||
|  | ||||
|     this._gestureRecognizer.start(clientY); | ||||
|   } | ||||
|  | ||||
|   private _handleTouchMove = (ev: TouchEvent) => { | ||||
|     const currentY = ev.touches[0].clientY; | ||||
|     const delta = this._gestureRecognizer.move(currentY); | ||||
|  | ||||
|     if (delta < 0) { | ||||
|       ev.preventDefault(); | ||||
|       this._isDragging = true; | ||||
|       requestAnimationFrame(() => { | ||||
|         if (this._isDragging) { | ||||
|           this.style.setProperty( | ||||
|             "--dialog-transform", | ||||
|             `translateY(${delta * -1}px)` | ||||
|           ); | ||||
|         } | ||||
|       }); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   private _animateSnapBack() { | ||||
|     // Add transition for smooth animation | ||||
|     this.style.setProperty( | ||||
|       "--dialog-transition", | ||||
|       `transform ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms ease-out` | ||||
|     ); | ||||
|  | ||||
|     // Reset transform to snap back | ||||
|     this.style.removeProperty("--dialog-transform"); | ||||
|  | ||||
|     // Remove transition after animation completes | ||||
|     setTimeout(() => { | ||||
|       this.style.removeProperty("--dialog-transition"); | ||||
|     }, BOTTOM_SHEET_ANIMATION_DURATION_MS); | ||||
|   } | ||||
|  | ||||
|   private _handleTouchEnd = () => { | ||||
|     this._unregisterResizeHandlers(); | ||||
|  | ||||
|     this._isDragging = false; | ||||
|  | ||||
|     const result = this._gestureRecognizer.end(); | ||||
|  | ||||
|     // If velocity exceeds threshold, use velocity direction to determine action | ||||
|     if (result.isSwipe) { | ||||
|       if (result.isDownwardSwipe) { | ||||
|         // Downward swipe - close the bottom sheet | ||||
|         this._drawerOpen = false; | ||||
|       } else { | ||||
|         // Upward swipe - keep open and animate back | ||||
|         this._animateSnapBack(); | ||||
|       } | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // If velocity is below threshold, use position-based logic | ||||
|     // Get the drawer height to calculate 50% threshold | ||||
|     const drawerBody = this._drawer.shadowRoot?.querySelector( | ||||
|       '[part="body"]' | ||||
|     ) as HTMLElement; | ||||
|     const drawerHeight = drawerBody?.offsetHeight || 0; | ||||
|  | ||||
|     // delta is negative when dragging down | ||||
|     // Close if dragged down past 50% of the drawer height | ||||
|     if ( | ||||
|       drawerHeight > 0 && | ||||
|       result.delta < 0 && | ||||
|       Math.abs(result.delta) > drawerHeight * 0.5 | ||||
|     ) { | ||||
|       this._drawerOpen = false; | ||||
|     } else { | ||||
|       this._animateSnapBack(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   private _unregisterResizeHandlers = () => { | ||||
|     document.removeEventListener("touchmove", this._handleTouchMove); | ||||
|     document.removeEventListener("touchend", this._handleTouchEnd); | ||||
|     document.removeEventListener("touchcancel", this._handleTouchEnd); | ||||
|   }; | ||||
|  | ||||
|   disconnectedCallback() { | ||||
|     super.disconnectedCallback(); | ||||
|     this._unregisterResizeHandlers(); | ||||
|     this._isDragging = false; | ||||
|   } | ||||
|  | ||||
|   static styles = [ | ||||
|     haStyleScrollbar, | ||||
|     css` | ||||
| @@ -59,6 +179,8 @@ export class HaBottomSheet extends LitElement { | ||||
|       wa-drawer::part(dialog) { | ||||
|         max-height: var(--ha-bottom-sheet-max-height, 90vh); | ||||
|         align-items: center; | ||||
|         transform: var(--dialog-transform); | ||||
|         transition: var(--dialog-transition); | ||||
|       } | ||||
|       wa-drawer::part(body) { | ||||
|         max-width: var(--ha-bottom-sheet-max-width); | ||||
| @@ -90,6 +212,11 @@ export class HaBottomSheet extends LitElement { | ||||
|         max-width: 100%; | ||||
|         display: flex; | ||||
|         flex-direction: column; | ||||
|         padding: var( | ||||
|           --ha-bottom-sheet-padding, | ||||
|           0 var(--safe-area-inset-right) var(--safe-area-inset-bottom) | ||||
|             var(--safe-area-inset-left) | ||||
|         ); | ||||
|       } | ||||
|     `, | ||||
|   ]; | ||||
|   | ||||
| @@ -44,26 +44,26 @@ export class HaCard extends LitElement { | ||||
|       font-size: var(--ha-card-header-font-size, var(--ha-font-size-2xl)); | ||||
|       letter-spacing: -0.012em; | ||||
|       line-height: var(--ha-line-height-expanded); | ||||
|       padding: 12px 16px 16px; | ||||
|       padding: var(--ha-space-3) var(--ha-space-4) var(--ha-space-4); | ||||
|       display: block; | ||||
|       margin-block-start: 0px; | ||||
|       margin-block-end: 0px; | ||||
|       margin-block-start: var(--ha-space-0); | ||||
|       margin-block-end: var(--ha-space-0); | ||||
|       font-weight: var(--ha-font-weight-normal); | ||||
|     } | ||||
|  | ||||
|     :host ::slotted(.card-content:not(:first-child)), | ||||
|     slot:not(:first-child)::slotted(.card-content) { | ||||
|       padding-top: 0px; | ||||
|       margin-top: -8px; | ||||
|       padding-top: var(--ha-space-0); | ||||
|       margin-top: calc(var(--ha-space-2) * -1); | ||||
|     } | ||||
|  | ||||
|     :host ::slotted(.card-content) { | ||||
|       padding: 16px; | ||||
|       padding: var(--ha-space-4); | ||||
|     } | ||||
|  | ||||
|     :host ::slotted(.card-actions) { | ||||
|       border-top: 1px solid var(--divider-color, #e8e8e8); | ||||
|       padding: 8px; | ||||
|       padding: var(--ha-space-2); | ||||
|     } | ||||
|   `; | ||||
|  | ||||
|   | ||||
| @@ -148,7 +148,7 @@ export class HaForm extends LitElement implements HaFormElement { | ||||
|                   .value=${getValue(this.data, item)} | ||||
|                   .label=${this._computeLabel(item, this.data)} | ||||
|                   .disabled=${item.disabled || this.disabled || false} | ||||
|                   .placeholder=${item.required ? "" : item.default} | ||||
|                   .placeholder=${item.required ? undefined : item.default} | ||||
|                   .helper=${this._computeHelper(item)} | ||||
|                   .localizeValue=${this.localizeValue} | ||||
|                   .required=${item.required || false} | ||||
|   | ||||
| @@ -24,7 +24,7 @@ import "./ha-svg-icon"; | ||||
|  | ||||
| @customElement("ha-generic-picker") | ||||
| export class HaGenericPicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   // eslint-disable-next-line lit/no-native-attributes | ||||
|   @property({ type: Boolean }) public autofocus = false; | ||||
| @@ -68,6 +68,21 @@ export class HaGenericPicker extends LitElement { | ||||
|   @property({ attribute: "not-found-label", type: String }) | ||||
|   public notFoundLabel?: string; | ||||
|  | ||||
|   @property({ attribute: "popover-placement" }) | ||||
|   public popoverPlacement: | ||||
|     | "bottom" | ||||
|     | "top" | ||||
|     | "left" | ||||
|     | "right" | ||||
|     | "top-start" | ||||
|     | "top-end" | ||||
|     | "right-start" | ||||
|     | "right-end" | ||||
|     | "bottom-start" | ||||
|     | "bottom-end" | ||||
|     | "left-start" | ||||
|     | "left-end" = "bottom-start"; | ||||
|  | ||||
|   /** If set picker shows an add button instead of textbox when value isn't set */ | ||||
|   @property({ attribute: "add-button-label" }) public addButtonLabel?: string; | ||||
|  | ||||
| @@ -135,7 +150,7 @@ export class HaGenericPicker extends LitElement { | ||||
|                 style="--body-width: ${this._popoverWidth}px;" | ||||
|                 without-arrow | ||||
|                 distance="-4" | ||||
|                 placement="bottom-start" | ||||
|                 .placement=${this.popoverPlacement} | ||||
|                 for="picker" | ||||
|                 auto-size="vertical" | ||||
|                 auto-size-padding="16" | ||||
| @@ -144,9 +159,7 @@ export class HaGenericPicker extends LitElement { | ||||
|                 trap-focus | ||||
|                 role="dialog" | ||||
|                 aria-modal="true" | ||||
|                 aria-label=${this.hass.localize( | ||||
|                   "ui.components.target-picker.add_target" | ||||
|                 )} | ||||
|                 aria-label=${this.label || "Select option"} | ||||
|               > | ||||
|                 ${this._renderComboBox()} | ||||
|               </wa-popover> | ||||
| @@ -159,9 +172,7 @@ export class HaGenericPicker extends LitElement { | ||||
|                 @closed=${this._hidePicker} | ||||
|                 role="dialog" | ||||
|                 aria-modal="true" | ||||
|                 aria-label=${this.hass.localize( | ||||
|                   "ui.components.target-picker.add_target" | ||||
|                 )} | ||||
|                 aria-label=${this.label || "Select option"} | ||||
|               > | ||||
|                 ${this._renderComboBox(true)} | ||||
|               </ha-bottom-sheet>` | ||||
| @@ -179,7 +190,8 @@ export class HaGenericPicker extends LitElement { | ||||
|       <ha-picker-combo-box | ||||
|         .hass=${this.hass} | ||||
|         .allowCustomValue=${this.allowCustomValue} | ||||
|         .label=${this.searchLabel ?? this.hass.localize("ui.common.search")} | ||||
|         .label=${this.searchLabel ?? | ||||
|         (this.hass?.localize("ui.common.search") || "Search")} | ||||
|         .value=${this.value} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         .rowRenderer=${this.rowRenderer} | ||||
|   | ||||
| @@ -1,56 +1,58 @@ | ||||
| import type { PropertyValues } from "lit"; | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| import { stopPropagation } from "../common/dom/stop_propagation"; | ||||
| import { formatLanguageCode } from "../common/language/format_language"; | ||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||
| import type { FrontendLocaleData } from "../data/translation"; | ||||
| import { translationMetadata } from "../resources/translations-metadata"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import type { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import "./ha-generic-picker"; | ||||
| import "./ha-list-item"; | ||||
| import type { PickerComboBoxItem } from "./ha-picker-combo-box"; | ||||
| import "./ha-select"; | ||||
| import type { HaSelect } from "./ha-select"; | ||||
|  | ||||
| export const getLanguageOptions = ( | ||||
|   languages: string[], | ||||
|   nativeName: boolean, | ||||
|   noSort: boolean, | ||||
|   locale?: FrontendLocaleData | ||||
| ) => { | ||||
|   let options: { label: string; value: string }[] = []; | ||||
| ): PickerComboBoxItem[] => { | ||||
|   let options: PickerComboBoxItem[] = []; | ||||
|  | ||||
|   if (nativeName) { | ||||
|     const translations = translationMetadata.translations; | ||||
|     options = languages.map((lang) => { | ||||
|       let label = translations[lang]?.nativeName; | ||||
|       if (!label) { | ||||
|       let primary = translations[lang]?.nativeName; | ||||
|       if (!primary) { | ||||
|         try { | ||||
|           // this will not work if Intl.DisplayNames is polyfilled, it will return in the language of the user | ||||
|           label = new Intl.DisplayNames(lang, { | ||||
|           primary = new Intl.DisplayNames(lang, { | ||||
|             type: "language", | ||||
|             fallback: "code", | ||||
|           }).of(lang)!; | ||||
|         } catch (_err) { | ||||
|           label = lang; | ||||
|           primary = lang; | ||||
|         } | ||||
|       } | ||||
|       return { | ||||
|         value: lang, | ||||
|         label, | ||||
|         id: lang, | ||||
|         primary, | ||||
|         search_labels: [primary], | ||||
|       }; | ||||
|     }); | ||||
|   } else if (locale) { | ||||
|     options = languages.map((lang) => ({ | ||||
|       value: lang, | ||||
|       label: formatLanguageCode(lang, locale), | ||||
|       id: lang, | ||||
|       primary: formatLanguageCode(lang, locale), | ||||
|       search_labels: [formatLanguageCode(lang, locale)], | ||||
|     })); | ||||
|   } | ||||
|  | ||||
|   if (!noSort && locale) { | ||||
|     options.sort((a, b) => | ||||
|       caseInsensitiveStringCompare(a.label, b.label, locale.language) | ||||
|       caseInsensitiveStringCompare(a.primary, b.primary, locale.language) | ||||
|     ); | ||||
|   } | ||||
|   return options; | ||||
| @@ -80,115 +82,69 @@ export class HaLanguagePicker extends LitElement { | ||||
|  | ||||
|   @state() _defaultLanguages: string[] = []; | ||||
|  | ||||
|   @query("ha-select") private _select!: HaSelect; | ||||
|  | ||||
|   protected firstUpdated(changedProps: PropertyValues) { | ||||
|     super.firstUpdated(changedProps); | ||||
|     this._computeDefaultLanguageOptions(); | ||||
|   } | ||||
|  | ||||
|   protected updated(changedProperties: PropertyValues) { | ||||
|     super.updated(changedProperties); | ||||
|  | ||||
|     const localeChanged = | ||||
|       changedProperties.has("hass") && | ||||
|       this.hass && | ||||
|       changedProperties.get("hass") && | ||||
|       changedProperties.get("hass").locale.language !== | ||||
|         this.hass.locale.language; | ||||
|     if ( | ||||
|       changedProperties.has("languages") || | ||||
|       changedProperties.has("value") || | ||||
|       localeChanged | ||||
|     ) { | ||||
|       this._select.layoutOptions(); | ||||
|       if (!this.disabled && this._select.value !== this.value) { | ||||
|         fireEvent(this, "value-changed", { value: this._select.value }); | ||||
|       } | ||||
|       if (!this.value) { | ||||
|         return; | ||||
|       } | ||||
|       const languageOptions = this._getLanguagesOptions( | ||||
|         this.languages ?? this._defaultLanguages, | ||||
|         this.nativeName, | ||||
|         this.noSort, | ||||
|         this.hass?.locale | ||||
|       ); | ||||
|       const selectedItemIndex = languageOptions.findIndex( | ||||
|         (option) => option.value === this.value | ||||
|       ); | ||||
|       if (selectedItemIndex === -1) { | ||||
|         this.value = undefined; | ||||
|       } | ||||
|       if (localeChanged) { | ||||
|         this._select.select(selectedItemIndex); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private _getLanguagesOptions = memoizeOne(getLanguageOptions); | ||||
|  | ||||
|   private _computeDefaultLanguageOptions() { | ||||
|     this._defaultLanguages = Object.keys(translationMetadata.translations); | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     const languageOptions = this._getLanguagesOptions( | ||||
|   private _getItems = () => | ||||
|     this._getLanguagesOptions( | ||||
|       this.languages ?? this._defaultLanguages, | ||||
|       this.nativeName, | ||||
|       this.noSort, | ||||
|       this.hass?.locale | ||||
|     ); | ||||
|  | ||||
|   private _valueRenderer = (value) => { | ||||
|     const language = this._getItems().find( | ||||
|       (lang) => lang.id === value | ||||
|     )?.primary; | ||||
|     return html`<span slot="headline">${language ?? value}</span> `; | ||||
|   }; | ||||
|  | ||||
|   protected render() { | ||||
|     const value = | ||||
|       this.value ?? | ||||
|       (this.required && !this.disabled | ||||
|         ? languageOptions[0]?.value | ||||
|         : this.value); | ||||
|       (this.required && !this.disabled ? this._getItems()[0].id : this.value); | ||||
|  | ||||
|     return html` | ||||
|       <ha-select | ||||
|         .label=${this.label ?? | ||||
|       <ha-generic-picker | ||||
|         .hass=${this.hass} | ||||
|         .autofocus=${this.autofocus} | ||||
|         popover-placement="bottom-end" | ||||
|         .notFoundLabel=${this.hass?.localize( | ||||
|           "ui.components.language-picker.no_match" | ||||
|         )} | ||||
|         .placeholder=${this.label ?? | ||||
|         (this.hass?.localize("ui.components.language-picker.language") || | ||||
|           "Language")} | ||||
|         .value=${value || ""} | ||||
|         .required=${this.required} | ||||
|         .value=${value} | ||||
|         .valueRenderer=${this._valueRenderer} | ||||
|         .disabled=${this.disabled} | ||||
|         @selected=${this._changed} | ||||
|         @closed=${stopPropagation} | ||||
|         fixedMenuPosition | ||||
|         naturalMenuWidth | ||||
|         .inlineArrow=${this.inlineArrow} | ||||
|       > | ||||
|         ${languageOptions.length === 0 | ||||
|           ? html`<ha-list-item value="" | ||||
|               >${this.hass?.localize( | ||||
|                 "ui.components.language-picker.no_languages" | ||||
|               ) || "No languages"}</ha-list-item | ||||
|             >` | ||||
|           : languageOptions.map( | ||||
|               (option) => html` | ||||
|                 <ha-list-item .value=${option.value} | ||||
|                   >${option.label}</ha-list-item | ||||
|                 > | ||||
|               ` | ||||
|             )} | ||||
|       </ha-select> | ||||
|         .getItems=${this._getItems} | ||||
|         @value-changed=${this._changed} | ||||
|         hide-clear-icon | ||||
|       ></ha-generic-picker> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     ha-select { | ||||
|     ha-generic-picker { | ||||
|       width: 100%; | ||||
|       min-width: 200px; | ||||
|       display: block; | ||||
|     } | ||||
|   `; | ||||
|  | ||||
|   private _changed(ev): void { | ||||
|     const target = ev.target as HaSelect; | ||||
|     if (this.disabled || target.value === "" || target.value === this.value) { | ||||
|       return; | ||||
|     } | ||||
|     this.value = target.value; | ||||
|   private _changed(ev: ValueChangedEvent<string>): void { | ||||
|     ev.stopPropagation(); | ||||
|     this.value = ev.detail.value; | ||||
|     fireEvent(this, "value-changed", { value: this.value }); | ||||
|   } | ||||
| } | ||||
|   | ||||
| @@ -50,7 +50,7 @@ export class HaMarkdown extends LitElement { | ||||
|     } | ||||
|     ha-alert { | ||||
|       display: block; | ||||
|       margin: 4px 0; | ||||
|       margin: var(--ha-space-1) 0; | ||||
|     } | ||||
|     a { | ||||
|       color: var(--primary-color); | ||||
| @@ -75,7 +75,7 @@ export class HaMarkdown extends LitElement { | ||||
|       padding: 0; | ||||
|     } | ||||
|     pre { | ||||
|       padding: 16px; | ||||
|       padding: var(--ha-space-4); | ||||
|       overflow: auto; | ||||
|       line-height: var(--ha-line-height-condensed); | ||||
|       font-family: var(--ha-font-family-code); | ||||
| @@ -95,7 +95,7 @@ export class HaMarkdown extends LitElement { | ||||
|     hr { | ||||
|       border-color: var(--divider-color); | ||||
|       border-bottom: none; | ||||
|       margin: 16px 0; | ||||
|       margin: var(--ha-space-4) 0; | ||||
|     } | ||||
|   ` as CSSResultGroup; | ||||
| } | ||||
|   | ||||
| @@ -69,7 +69,7 @@ export type PickerComboBoxSearchFn<T extends PickerComboBoxItem> = ( | ||||
|  | ||||
| @customElement("ha-picker-combo-box") | ||||
| export class HaPickerComboBox extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   // eslint-disable-next-line lit/no-native-attributes | ||||
|   @property({ type: Boolean }) public autofocus = false; | ||||
| @@ -140,7 +140,9 @@ export class HaPickerComboBox extends LitElement { | ||||
|  | ||||
|   protected render() { | ||||
|     return html`<ha-textfield | ||||
|         .label=${this.label ?? this.hass.localize("ui.common.search")} | ||||
|         .label=${this.label ?? | ||||
|         this.hass?.localize("ui.common.search") ?? | ||||
|         "Search"} | ||||
|         @input=${this._filterChanged} | ||||
|       ></ha-textfield> | ||||
|       <lit-virtualizer | ||||
| @@ -159,12 +161,18 @@ export class HaPickerComboBox extends LitElement { | ||||
|   private _defaultNotFoundItem = memoizeOne( | ||||
|     ( | ||||
|       label: this["notFoundLabel"], | ||||
|       localize: LocalizeFunc | ||||
|       localize?: LocalizeFunc | ||||
|     ): PickerComboBoxItemWithLabel => ({ | ||||
|       id: NO_MATCHING_ITEMS_FOUND_ID, | ||||
|       primary: label || localize("ui.components.combo-box.no_match"), | ||||
|       primary: | ||||
|         label || | ||||
|         (localize && localize("ui.components.combo-box.no_match")) || | ||||
|         "No matching items found", | ||||
|       icon_path: mdiMagnify, | ||||
|       a11y_label: label || localize("ui.components.combo-box.no_match"), | ||||
|       a11y_label: | ||||
|         label || | ||||
|         (localize && localize("ui.components.combo-box.no_match")) || | ||||
|         "No matching items found", | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
| @@ -189,13 +197,13 @@ export class HaPickerComboBox extends LitElement { | ||||
|         caseInsensitiveStringCompare( | ||||
|           entityA.sorting_label!, | ||||
|           entityB.sorting_label!, | ||||
|           this.hass.locale.language | ||||
|           this.hass?.locale.language ?? navigator.language | ||||
|         ) | ||||
|       ); | ||||
|  | ||||
|     if (!sortedItems.length) { | ||||
|       sortedItems.push( | ||||
|         this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize) | ||||
|         this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize) | ||||
|       ); | ||||
|     } | ||||
|  | ||||
| @@ -249,8 +257,20 @@ export class HaPickerComboBox extends LitElement { | ||||
|     const textfield = ev.target as HaTextField; | ||||
|     const searchString = textfield.value.trim(); | ||||
|  | ||||
|     if (!searchString) { | ||||
|       this._items = this._allItems; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const index = this._fuseIndex(this._allItems); | ||||
|     const fuse = new HaFuse(this._allItems, { shouldSort: false }, index); | ||||
|     const fuse = new HaFuse( | ||||
|       this._allItems, | ||||
|       { | ||||
|         shouldSort: false, | ||||
|         minMatchCharLength: Math.min(searchString.length, 2), | ||||
|       }, | ||||
|       index | ||||
|     ); | ||||
|  | ||||
|     const results = fuse.multiTermsSearch(searchString); | ||||
|     let filteredItems = this._allItems as PickerComboBoxItem[]; | ||||
| @@ -258,7 +278,7 @@ export class HaPickerComboBox extends LitElement { | ||||
|       const items = results.map((result) => result.item); | ||||
|       if (items.length === 0) { | ||||
|         items.push( | ||||
|           this._defaultNotFoundItem(this.notFoundLabel, this.hass.localize) | ||||
|           this._defaultNotFoundItem(this.notFoundLabel, this.hass?.localize) | ||||
|         ); | ||||
|       } | ||||
|       const additionalItems = this._getAdditionalItems(searchString); | ||||
| @@ -431,6 +451,17 @@ export class HaPickerComboBox extends LitElement { | ||||
|  | ||||
|   private _pickSelectedItem = (ev: KeyboardEvent) => { | ||||
|     ev.stopPropagation(); | ||||
|     const firstItem = this._virtualizerElement?.items[0] as PickerComboBoxItem; | ||||
|  | ||||
|     if ( | ||||
|       this._virtualizerElement?.items.length === 1 && | ||||
|       firstItem.id !== NO_MATCHING_ITEMS_FOUND_ID | ||||
|     ) { | ||||
|       fireEvent(this, "value-changed", { | ||||
|         value: firstItem.id, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if (this._selectedItemIndex === -1) { | ||||
|       return; | ||||
|     } | ||||
| @@ -438,7 +469,9 @@ export class HaPickerComboBox extends LitElement { | ||||
|     // if filter button is focused | ||||
|     ev.preventDefault(); | ||||
|  | ||||
|     const item: any = this._virtualizerElement?.items[this._selectedItemIndex]; | ||||
|     const item = this._virtualizerElement?.items[ | ||||
|       this._selectedItemIndex | ||||
|     ] as PickerComboBoxItem; | ||||
|     if (item && item.id !== NO_MATCHING_ITEMS_FOUND_ID) { | ||||
|       fireEvent(this, "value-changed", { value: item.id }); | ||||
|     } | ||||
|   | ||||
| @@ -1,122 +0,0 @@ | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import type { BackgroundSelector } from "../../data/selector"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import "../ha-picture-upload"; | ||||
| import "../ha-alert"; | ||||
| import type { HaPictureUpload } from "../ha-picture-upload"; | ||||
| import { URL_PREFIX } from "../../data/image_upload"; | ||||
|  | ||||
| @customElement("ha-selector-background") | ||||
| export class HaBackgroundSelector extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
|  | ||||
|   @property() public value?: any; | ||||
|  | ||||
|   @property({ attribute: false }) public selector!: BackgroundSelector; | ||||
|  | ||||
|   @property({ type: Boolean }) public disabled = false; | ||||
|  | ||||
|   @property({ type: Boolean }) public required = true; | ||||
|  | ||||
|   @state() private yamlBackground = false; | ||||
|  | ||||
|   protected updated(changedProps) { | ||||
|     super.updated(changedProps); | ||||
|  | ||||
|     if (changedProps.has("value")) { | ||||
|       this.yamlBackground = !!this.value && !this.value.startsWith(URL_PREFIX); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     return html` | ||||
|       <div> | ||||
|         ${this.yamlBackground | ||||
|           ? html` | ||||
|               <div class="value"> | ||||
|                 <img | ||||
|                   src=${this.value} | ||||
|                   alt=${this.hass.localize( | ||||
|                     "ui.components.picture-upload.current_image_alt" | ||||
|                   )} | ||||
|                 /> | ||||
|               </div> | ||||
|               <ha-alert alert-type="info"> | ||||
|                 ${this.hass.localize( | ||||
|                   `ui.components.selectors.background.yaml_info` | ||||
|                 )} | ||||
|                 <ha-button slot="action" @click=${this._clearValue}> | ||||
|                   ${this.hass.localize( | ||||
|                     `ui.components.picture-upload.clear_picture` | ||||
|                   )} | ||||
|                 </ha-button> | ||||
|               </ha-alert> | ||||
|             ` | ||||
|           : html` | ||||
|               <ha-picture-upload | ||||
|                 .hass=${this.hass} | ||||
|                 .value=${this.value?.startsWith(URL_PREFIX) ? this.value : null} | ||||
|                 .original=${!!this.selector.background?.original} | ||||
|                 .cropOptions=${this.selector.background?.crop} | ||||
|                 select-media | ||||
|                 @change=${this._pictureChanged} | ||||
|               ></ha-picture-upload> | ||||
|             `} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _pictureChanged(ev) { | ||||
|     const value = (ev.target as HaPictureUpload).value; | ||||
|  | ||||
|     fireEvent(this, "value-changed", { value: value ?? undefined }); | ||||
|   } | ||||
|  | ||||
|   private _clearValue() { | ||||
|     fireEvent(this, "value-changed", { value: undefined }); | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     :host { | ||||
|       display: block; | ||||
|       position: relative; | ||||
|     } | ||||
|     ha-picture-upload { | ||||
|       background-color: var(--primary-background-color); | ||||
|       border-radius: var(--file-upload-image-border-radius); | ||||
|     } | ||||
|     div { | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|     } | ||||
|     ha-button { | ||||
|       white-space: nowrap; | ||||
|       --mdc-theme-primary: var(--primary-color); | ||||
|     } | ||||
|     .value { | ||||
|       width: 100%; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       align-items: center; | ||||
|     } | ||||
|     img { | ||||
|       max-width: 100%; | ||||
|       max-height: 200px; | ||||
|       margin-bottom: 4px; | ||||
|       border-radius: var(--file-upload-image-border-radius); | ||||
|       transition: opacity 0.3s; | ||||
|       opacity: var(--picture-opacity, 1); | ||||
|     } | ||||
|     img:hover { | ||||
|       opacity: 1; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "ha-selector-background": HaBackgroundSelector; | ||||
|   } | ||||
| } | ||||
| @@ -34,7 +34,6 @@ const LOAD_ELEMENTS = { | ||||
|   file: () => import("./ha-selector-file"), | ||||
|   floor: () => import("./ha-selector-floor"), | ||||
|   label: () => import("./ha-selector-label"), | ||||
|   background: () => import("./ha-selector-background"), | ||||
|   language: () => import("./ha-selector-language"), | ||||
|   navigation: () => import("./ha-selector-navigation"), | ||||
|   number: () => import("./ha-selector-number"), | ||||
|   | ||||
| @@ -76,11 +76,16 @@ class HaServicePicker extends LitElement { | ||||
|     </ha-combo-box-item> | ||||
|   `; | ||||
|  | ||||
|   private _valueRenderer: PickerValueRenderer = (value) => { | ||||
|   private _valueRenderer = memoizeOne( | ||||
|     ( | ||||
|       localize: LocalizeFunc, | ||||
|       services: HomeAssistant["services"] | ||||
|     ): PickerValueRenderer => | ||||
|       (value) => { | ||||
|         const serviceId = value; | ||||
|         const [domain, service] = serviceId.split("."); | ||||
|  | ||||
|     if (!this.hass.services[domain]?.[service]) { | ||||
|         if (!services[domain]?.[service]) { | ||||
|           return html` | ||||
|             <ha-svg-icon slot="start" .path=${mdiRoomService}></ha-svg-icon> | ||||
|             <span slot="headline">${value}</span> | ||||
| @@ -88,8 +93,8 @@ class HaServicePicker extends LitElement { | ||||
|         } | ||||
|  | ||||
|         const serviceName = | ||||
|       this.hass.localize(`component.${domain}.services.${service}.name`) || | ||||
|       this.hass.services[domain][service].name || | ||||
|           localize(`component.${domain}.services.${service}.name`) || | ||||
|           services[domain][service].name || | ||||
|           service; | ||||
|  | ||||
|         return html` | ||||
| @@ -100,10 +105,13 @@ class HaServicePicker extends LitElement { | ||||
|           ></ha-service-icon> | ||||
|           <span slot="headline">${serviceName}</span> | ||||
|           ${this.showServiceId | ||||
|         ? html`<span slot="supporting-text" class="code">${serviceId}</span>` | ||||
|             ? html`<span slot="supporting-text" class="code" | ||||
|                 >${serviceId}</span | ||||
|               >` | ||||
|             : nothing} | ||||
|         `; | ||||
|   }; | ||||
|       } | ||||
|   ); | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const placeholder = | ||||
| @@ -123,7 +131,10 @@ class HaServicePicker extends LitElement { | ||||
|         .value=${this.value} | ||||
|         .getItems=${this._getItems} | ||||
|         .rowRenderer=${this._rowRenderer} | ||||
|         .valueRenderer=${this._valueRenderer} | ||||
|         .valueRenderer=${this._valueRenderer( | ||||
|           this.hass.localize, | ||||
|           this.hass.services | ||||
|         )} | ||||
|         @value-changed=${this._valueChanged} | ||||
|       > | ||||
|       </ha-generic-picker> | ||||
|   | ||||
| @@ -16,14 +16,10 @@ import memoizeOne from "memoize-one"; | ||||
| import { computeCssColor } from "../../common/color/compute-color"; | ||||
| import { hex2rgb } from "../../common/color/convert-color"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import { slugify } from "../../common/string/slugify"; | ||||
| import { | ||||
|   computeDeviceName, | ||||
|   computeDeviceNameDisplay, | ||||
| } from "../../common/entity/compute_device_name"; | ||||
| import { computeDeviceNameDisplay } from "../../common/entity/compute_device_name"; | ||||
| import { computeDomain } from "../../common/entity/compute_domain"; | ||||
| import { computeEntityName } from "../../common/entity/compute_entity_name"; | ||||
| import { getEntityContext } from "../../common/entity/context/get_entity_context"; | ||||
| import { computeStateName } from "../../common/entity/compute_state_name"; | ||||
| import { slugify } from "../../common/string/slugify"; | ||||
| import { getConfigEntry } from "../../data/config_entries"; | ||||
| import { labelsContext } from "../../data/context"; | ||||
| import { domainToName } from "../../data/integration"; | ||||
| @@ -172,23 +168,10 @@ export class HaTargetPickerValueChip extends LitElement { | ||||
|     if (type === "entity") { | ||||
|       this._setDomainName(computeDomain(itemId)); | ||||
|  | ||||
|       const stateObject = this.hass.states[itemId]; | ||||
|       const entityName = computeEntityName( | ||||
|         stateObject, | ||||
|         this.hass.entities, | ||||
|         this.hass.devices | ||||
|       ); | ||||
|       const { device } = getEntityContext( | ||||
|         stateObject, | ||||
|         this.hass.entities, | ||||
|         this.hass.devices, | ||||
|         this.hass.areas, | ||||
|         this.hass.floors | ||||
|       ); | ||||
|       const deviceName = device ? computeDeviceName(device) : undefined; | ||||
|       const stateObj = this.hass.states[itemId]; | ||||
|       return { | ||||
|         name: entityName || deviceName || itemId, | ||||
|         stateObject, | ||||
|         name: computeStateName(stateObj) || itemId, | ||||
|         stateObject: stateObj, | ||||
|       }; | ||||
|     } | ||||
|  | ||||
|   | ||||
| @@ -214,8 +214,6 @@ export interface PipelineRun { | ||||
|   stage: "ready" | "wake_word" | "stt" | "intent" | "tts" | "done" | "error"; | ||||
|   run: PipelineRunStartEvent["data"]; | ||||
|   error?: PipelineErrorEvent["data"]; | ||||
|   started: Date; | ||||
|   finished?: Date; | ||||
|   wake_word?: PipelineWakeWordStartEvent["data"] & | ||||
|     Partial<PipelineWakeWordEndEvent["data"]> & { done: boolean }; | ||||
|   stt?: PipelineSTTStartEvent["data"] & | ||||
| @@ -237,7 +235,6 @@ export const processEvent = ( | ||||
|       stage: "ready", | ||||
|       run: event.data, | ||||
|       events: [event], | ||||
|       started: new Date(event.timestamp), | ||||
|     }; | ||||
|     return run; | ||||
|   } | ||||
| @@ -293,14 +290,9 @@ export const processEvent = ( | ||||
|       tts: { ...run.tts!, ...event.data, done: true }, | ||||
|     }; | ||||
|   } else if (event.type === "run-end") { | ||||
|     run = { ...run, finished: new Date(event.timestamp), stage: "done" }; | ||||
|     run = { ...run, stage: "done" }; | ||||
|   } else if (event.type === "error") { | ||||
|     run = { | ||||
|       ...run, | ||||
|       finished: new Date(event.timestamp), | ||||
|       stage: "error", | ||||
|       error: event.data, | ||||
|     }; | ||||
|     run = { ...run, stage: "error", error: event.data }; | ||||
|   } else { | ||||
|     run = { ...run }; | ||||
|   } | ||||
|   | ||||
| @@ -1,228 +0,0 @@ | ||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
|  | ||||
| export const enum ChatLogEventType { | ||||
|   INITIAL_STATE = "initial_state", | ||||
|   CREATED = "created", | ||||
|   UPDATED = "updated", | ||||
|   DELETED = "deleted", | ||||
|   CONTENT_ADDED = "content_added", | ||||
| } | ||||
|  | ||||
| export interface ChatLogAttachment { | ||||
|   media_content_id: string; | ||||
|   mime_type: string; | ||||
|   path: string; | ||||
| } | ||||
|  | ||||
| export interface ChatLogSystemContent { | ||||
|   role: "system"; | ||||
|   content: string; | ||||
|   created: Date; | ||||
| } | ||||
|  | ||||
| export interface ChatLogUserContent { | ||||
|   role: "user"; | ||||
|   content: string; | ||||
|   created: Date; | ||||
|   attachments?: ChatLogAttachment[]; | ||||
| } | ||||
|  | ||||
| export interface ChatLogAssistantContent { | ||||
|   role: "assistant"; | ||||
|   agent_id: string; | ||||
|   created: Date; | ||||
|   content?: string; | ||||
|   thinking_content?: string; | ||||
|   tool_calls?: any[]; | ||||
| } | ||||
|  | ||||
| export interface ChatLogToolResultContent { | ||||
|   role: "tool_result"; | ||||
|   agent_id: string; | ||||
|   tool_call_id: string; | ||||
|   tool_name: string; | ||||
|   tool_result: any; | ||||
|   created: Date; | ||||
| } | ||||
|  | ||||
| export type ChatLogContent = | ||||
|   | ChatLogSystemContent | ||||
|   | ChatLogUserContent | ||||
|   | ChatLogAssistantContent | ||||
|   | ChatLogToolResultContent; | ||||
|  | ||||
| export interface ChatLog { | ||||
|   conversation_id: string; | ||||
|   continue_conversation: boolean; | ||||
|   content: ChatLogContent[]; | ||||
|   created: Date; | ||||
| } | ||||
|  | ||||
| // Internal wire format types (not exported) | ||||
| interface ChatLogSystemContentWire { | ||||
|   role: "system"; | ||||
|   content: string; | ||||
|   created: string; | ||||
| } | ||||
|  | ||||
| interface ChatLogUserContentWire { | ||||
|   role: "user"; | ||||
|   content: string; | ||||
|   created: string; | ||||
|   attachments?: ChatLogAttachment[]; | ||||
| } | ||||
|  | ||||
| interface ChatLogAssistantContentWire { | ||||
|   role: "assistant"; | ||||
|   agent_id: string; | ||||
|   created: string; | ||||
|   content?: string; | ||||
|   thinking_content?: string; | ||||
|   tool_calls?: { | ||||
|     tool_name: string; | ||||
|     tool_args: Record<string, any>; | ||||
|     id: string; | ||||
|     external: boolean; | ||||
|   }[]; | ||||
| } | ||||
|  | ||||
| interface ChatLogToolResultContentWire { | ||||
|   role: "tool_result"; | ||||
|   agent_id: string; | ||||
|   tool_call_id: string; | ||||
|   tool_name: string; | ||||
|   tool_result: any; | ||||
|   created: string; | ||||
| } | ||||
|  | ||||
| type ChatLogContentWire = | ||||
|   | ChatLogSystemContentWire | ||||
|   | ChatLogUserContentWire | ||||
|   | ChatLogAssistantContentWire | ||||
|   | ChatLogToolResultContentWire; | ||||
|  | ||||
| interface ChatLogWire { | ||||
|   conversation_id: string; | ||||
|   continue_conversation: boolean; | ||||
|   content: ChatLogContentWire[]; | ||||
|   created: string; | ||||
| } | ||||
|  | ||||
| const processContent = (content: ChatLogContentWire): ChatLogContent => ({ | ||||
|   ...content, | ||||
|   created: new Date(content.created), | ||||
| }); | ||||
|  | ||||
| const processChatLog = (chatLog: ChatLogWire): ChatLog => ({ | ||||
|   ...chatLog, | ||||
|   created: new Date(chatLog.created), | ||||
|   content: chatLog.content.map(processContent), | ||||
| }); | ||||
|  | ||||
| interface ChatLogInitialStateEvent { | ||||
|   event_type: ChatLogEventType.INITIAL_STATE; | ||||
|   data: ChatLogWire; | ||||
| } | ||||
|  | ||||
| interface ChatLogIndexInitialStateEvent { | ||||
|   event_type: ChatLogEventType.INITIAL_STATE; | ||||
|   data: ChatLogWire[]; | ||||
| } | ||||
|  | ||||
| interface ChatLogCreatedEvent { | ||||
|   conversation_id: string; | ||||
|   event_type: ChatLogEventType.CREATED; | ||||
|   data: ChatLogWire; | ||||
| } | ||||
|  | ||||
| interface ChatLogUpdatedEvent { | ||||
|   conversation_id: string; | ||||
|   event_type: ChatLogEventType.UPDATED; | ||||
|   data: { chat_log: ChatLogWire }; | ||||
| } | ||||
|  | ||||
| interface ChatLogDeletedEvent { | ||||
|   conversation_id: string; | ||||
|   event_type: ChatLogEventType.DELETED; | ||||
|   data: ChatLogWire; | ||||
| } | ||||
|  | ||||
| interface ChatLogContentAddedEvent { | ||||
|   conversation_id: string; | ||||
|   event_type: ChatLogEventType.CONTENT_ADDED; | ||||
|   data: { content: ChatLogContentWire }; | ||||
| } | ||||
|  | ||||
| type ChatLogSubscriptionEvent = | ||||
|   | ChatLogInitialStateEvent | ||||
|   | ChatLogUpdatedEvent | ||||
|   | ChatLogDeletedEvent | ||||
|   | ChatLogContentAddedEvent; | ||||
|  | ||||
| type ChatLogIndexSubscriptionEvent = | ||||
|   | ChatLogIndexInitialStateEvent | ||||
|   | ChatLogCreatedEvent | ||||
|   | ChatLogDeletedEvent; | ||||
|  | ||||
| export const subscribeChatLog = ( | ||||
|   hass: HomeAssistant, | ||||
|   conversationId: string, | ||||
|   callback: (chatLog: ChatLog | null) => void | ||||
| ): Promise<UnsubscribeFunc> => { | ||||
|   let chatLog: ChatLog | null = null; | ||||
|  | ||||
|   return hass.connection.subscribeMessage<ChatLogSubscriptionEvent>( | ||||
|     (event) => { | ||||
|       if (event.event_type === ChatLogEventType.INITIAL_STATE) { | ||||
|         chatLog = processChatLog(event.data); | ||||
|         callback(chatLog); | ||||
|       } else if (event.event_type === ChatLogEventType.CONTENT_ADDED) { | ||||
|         if (chatLog) { | ||||
|           chatLog = { | ||||
|             ...chatLog, | ||||
|             content: [...chatLog.content, processContent(event.data.content)], | ||||
|           }; | ||||
|           callback(chatLog); | ||||
|         } | ||||
|       } else if (event.event_type === ChatLogEventType.UPDATED) { | ||||
|         chatLog = processChatLog(event.data.chat_log); | ||||
|         callback(chatLog); | ||||
|       } else if (event.event_type === ChatLogEventType.DELETED) { | ||||
|         chatLog = null; | ||||
|         callback(null); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       type: "conversation/chat_log/subscribe", | ||||
|       conversation_id: conversationId, | ||||
|     } | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| export const subscribeChatLogIndex = ( | ||||
|   hass: HomeAssistant, | ||||
|   callback: (chatLogs: ChatLog[]) => void | ||||
| ): Promise<UnsubscribeFunc> => { | ||||
|   let chatLogs: ChatLog[] = []; | ||||
|  | ||||
|   return hass.connection.subscribeMessage<ChatLogIndexSubscriptionEvent>( | ||||
|     (event) => { | ||||
|       if (event.event_type === ChatLogEventType.INITIAL_STATE) { | ||||
|         chatLogs = event.data.map(processChatLog); | ||||
|         callback(chatLogs); | ||||
|       } else if (event.event_type === ChatLogEventType.CREATED) { | ||||
|         chatLogs = [...chatLogs, processChatLog(event.data)]; | ||||
|         callback(chatLogs); | ||||
|       } else if (event.event_type === ChatLogEventType.DELETED) { | ||||
|         chatLogs = chatLogs.filter( | ||||
|           (chatLog) => chatLog.conversation_id !== event.conversation_id | ||||
|         ); | ||||
|         callback(chatLogs); | ||||
|       } | ||||
|     }, | ||||
|     { | ||||
|       type: "conversation/chat_log/subscribe_index", | ||||
|     } | ||||
|   ); | ||||
| }; | ||||
| @@ -435,9 +435,9 @@ export const convertStatisticsToHistory = ( | ||||
|   Object.entries(orderedStatistics).forEach(([key, value]) => { | ||||
|     const entityHistoryStates: EntityHistoryState[] = value.map((e) => ({ | ||||
|       s: e.mean != null ? e.mean.toString() : e.state!.toString(), | ||||
|       lc: e.start / 1000, | ||||
|       lc: e.end / 1000, | ||||
|       a: {}, | ||||
|       lu: e.start / 1000, | ||||
|       lu: e.end / 1000, | ||||
|     })); | ||||
|     statsHistoryStates[key] = entityHistoryStates; | ||||
|   }); | ||||
|   | ||||
| @@ -264,6 +264,7 @@ export const getLabels = ( | ||||
|   const items = outputLabels.map<PickerComboBoxItem>((label) => ({ | ||||
|     id: label.label_id, | ||||
|     primary: label.name, | ||||
|     secondary: label.description ?? "", | ||||
|     icon: label.icon || undefined, | ||||
|     icon_path: label.icon ? undefined : mdiLabel, | ||||
|     sorting_label: label.name, | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import type { MediaSelectorValue } from "../../selector"; | ||||
| import type { LovelaceBadgeConfig } from "./badge"; | ||||
| import type { LovelaceCardConfig } from "./card"; | ||||
| import type { LovelaceSectionRawConfig } from "./section"; | ||||
| @@ -8,7 +9,7 @@ export interface ShowViewConfig { | ||||
| } | ||||
|  | ||||
| export interface LovelaceViewBackgroundConfig { | ||||
|   image?: string; | ||||
|   image?: string | MediaSelectorValue; | ||||
|   opacity?: number; | ||||
|   size?: "auto" | "cover" | "contain"; | ||||
|   alignment?: | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import type { | ||||
| import { ensureArray } from "../common/array/ensure-array"; | ||||
| import { computeStateDomain } from "../common/entity/compute_state_domain"; | ||||
| import { supportsFeature } from "../common/entity/supports-feature"; | ||||
| import type { CropOptions } from "../dialogs/image-cropper-dialog/show-image-cropper-dialog"; | ||||
| import { isHelperDomain } from "../panels/config/helpers/const"; | ||||
| import type { UiAction } from "../panels/lovelace/components/hui-action-editor"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| @@ -47,8 +46,6 @@ export type Selector = | ||||
|   | FileSelector | ||||
|   | IconSelector | ||||
|   | LabelSelector | ||||
|   | ImageSelector | ||||
|   | BackgroundSelector | ||||
|   | LanguageSelector | ||||
|   | LocationSelector | ||||
|   | MediaSelector | ||||
| @@ -273,14 +270,6 @@ export interface IconSelector { | ||||
|   } | null; | ||||
| } | ||||
|  | ||||
| export interface ImageSelector { | ||||
|   image: { original?: boolean; crop?: CropOptions } | null; | ||||
| } | ||||
|  | ||||
| export interface BackgroundSelector { | ||||
|   background: { original?: boolean; crop?: CropOptions } | null; | ||||
| } | ||||
|  | ||||
| export interface LabelSelector { | ||||
|   label: { | ||||
|     multiple?: boolean; | ||||
|   | ||||
| @@ -484,7 +484,7 @@ class DataEntryFlowDialog extends LitElement { | ||||
|         this._unsubDataEntryFlowProgress = undefined; | ||||
|       } | ||||
|       if (_step.next_flow[0] === "config_flow") { | ||||
|         showConfigFlowDialog(this._params!.dialogParentElement!, { | ||||
|         showConfigFlowDialog(this, { | ||||
|           continueFlowId: _step.next_flow[1], | ||||
|           carryOverDevices: this._devices( | ||||
|             this._params!.flowConfig.showDevices, | ||||
| @@ -496,32 +496,23 @@ class DataEntryFlowDialog extends LitElement { | ||||
|         }); | ||||
|       } else if (_step.next_flow[0] === "options_flow") { | ||||
|         if (_step.type === "create_entry") { | ||||
|           showOptionsFlowDialog( | ||||
|             this._params!.dialogParentElement!, | ||||
|             _step.result!, | ||||
|             { | ||||
|           showOptionsFlowDialog(this, _step.result!, { | ||||
|             continueFlowId: _step.next_flow[1], | ||||
|             navigateToResult: this._params!.navigateToResult, | ||||
|             dialogClosedCallback: this._params!.dialogClosedCallback, | ||||
|             } | ||||
|           ); | ||||
|           }); | ||||
|         } | ||||
|       } else if (_step.next_flow[0] === "config_subentries_flow") { | ||||
|         if (_step.type === "create_entry") { | ||||
|           showSubConfigFlowDialog( | ||||
|             this._params!.dialogParentElement!, | ||||
|             _step.result!, | ||||
|             _step.next_flow[0], | ||||
|             { | ||||
|           showSubConfigFlowDialog(this, _step.result!, _step.next_flow[0], { | ||||
|             continueFlowId: _step.next_flow[1], | ||||
|             navigateToResult: this._params!.navigateToResult, | ||||
|             dialogClosedCallback: this._params!.dialogClosedCallback, | ||||
|             } | ||||
|           ); | ||||
|           }); | ||||
|         } | ||||
|       } else { | ||||
|         this.closeDialog(); | ||||
|         showAlertDialog(this._params!.dialogParentElement!, { | ||||
|         showAlertDialog(this, { | ||||
|           text: this.hass.localize( | ||||
|             "ui.panel.config.integrations.config_flow.error", | ||||
|             { error: `Unsupported next flow type: ${_step.next_flow[0]}` } | ||||
|   | ||||
| @@ -678,8 +678,8 @@ export class MoreInfoDialog extends LitElement { | ||||
|           /* Set the top top of the dialog to a fixed position, so it doesnt jump when the content changes size */ | ||||
|           --vertical-align-dialog: flex-start; | ||||
|           --dialog-surface-margin-top: max( | ||||
|             40px, | ||||
|             var(--safe-area-inset-top, 0px) | ||||
|             var(--ha-space-10), | ||||
|             var(--safe-area-inset-top, var(--ha-space-0)) | ||||
|           ); | ||||
|           --dialog-content-padding: 0; | ||||
|         } | ||||
| @@ -698,14 +698,15 @@ export class MoreInfoDialog extends LitElement { | ||||
|         } | ||||
|  | ||||
|         ha-more-info-history-and-logbook { | ||||
|           padding: 8px 24px 24px 24px; | ||||
|           padding: var(--ha-space-2) var(--ha-space-6) var(--ha-space-6) | ||||
|             var(--ha-space-6); | ||||
|           display: block; | ||||
|         } | ||||
|  | ||||
|         @media all and (max-width: 450px), all and (max-height: 500px) { | ||||
|           /* When in fullscreen dialog should be attached to top */ | ||||
|           ha-dialog { | ||||
|             --dialog-surface-margin-top: 0px; | ||||
|             --dialog-surface-margin-top: var(--ha-space-0); | ||||
|           } | ||||
|         } | ||||
|  | ||||
| @@ -730,7 +731,8 @@ export class MoreInfoDialog extends LitElement { | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|           align-items: flex-start; | ||||
|           margin: 0 0 -10px 0; | ||||
|           margin: var(--ha-space-0) var(--ha-space-0) | ||||
|             calc(var(--ha-space-2) * -1) var(--ha-space-0); | ||||
|         } | ||||
|  | ||||
|         .title p { | ||||
| @@ -752,9 +754,9 @@ export class MoreInfoDialog extends LitElement { | ||||
|           font-size: var(--ha-font-size-m); | ||||
|           line-height: 16px; | ||||
|           --mdc-icon-size: 16px; | ||||
|           padding: 4px; | ||||
|           margin: -4px; | ||||
|           margin-top: -10px; | ||||
|           padding: var(--ha-space-1); | ||||
|           margin: calc(var(--ha-space-1) * -1); | ||||
|           margin-top: calc(var(--ha-space-2) * -1); | ||||
|           background: none; | ||||
|           border: none; | ||||
|           outline: none; | ||||
|   | ||||
| @@ -1011,8 +1011,8 @@ export class QuickBar extends LitElement { | ||||
|             --mdc-dialog-max-width: 800px; | ||||
|             --mdc-dialog-min-width: 500px; | ||||
|             --dialog-surface-position: fixed; | ||||
|             --dialog-surface-top: 40px; | ||||
|             --mdc-dialog-max-height: calc(100% - 72px); | ||||
|             --dialog-surface-top: var(--ha-space-10); | ||||
|             --mdc-dialog-max-height: calc(100% - var(--ha-space-18)); | ||||
|           } | ||||
|         } | ||||
|  | ||||
| @@ -1055,8 +1055,8 @@ export class QuickBar extends LitElement { | ||||
|         } | ||||
|  | ||||
|         span.command-text { | ||||
|           margin-left: 8px; | ||||
|           margin-inline-start: 8px; | ||||
|           margin-left: var(--ha-space-2); | ||||
|           margin-inline-start: var(--ha-space-2); | ||||
|           margin-inline-end: initial; | ||||
|           direction: var(--direction); | ||||
|         } | ||||
| @@ -1069,8 +1069,8 @@ export class QuickBar extends LitElement { | ||||
|         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; | ||||
|           --md-list-item-top-space: var(--ha-space-2); | ||||
|           --md-list-item-bottom-space: var(--ha-space-2); | ||||
|         } | ||||
|  | ||||
|         ha-md-list-item.three-line { | ||||
| @@ -1078,8 +1078,8 @@ export class QuickBar extends LitElement { | ||||
|           --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; | ||||
|           --md-list-item-top-space: var(--ha-space-2); | ||||
|           --md-list-item-bottom-space: var(--ha-space-2); | ||||
|         } | ||||
|  | ||||
|         ha-md-list-item .code { | ||||
| @@ -1104,11 +1104,11 @@ export class QuickBar extends LitElement { | ||||
|         } | ||||
|  | ||||
|         ha-tip { | ||||
|           padding: 20px; | ||||
|           padding: var(--ha-space-5); | ||||
|         } | ||||
|  | ||||
|         .nothing-found { | ||||
|           padding: 16px 0px; | ||||
|           padding: var(--ha-space-4) var(--ha-space-0); | ||||
|           text-align: center; | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -193,12 +193,12 @@ export class HaVoiceAssistantSetupDialog extends LitElement { | ||||
|                     ).map( | ||||
|                       (lang) => | ||||
|                         html`<ha-md-menu-item | ||||
|                           .value=${lang.value} | ||||
|                           .value=${lang.id} | ||||
|                           @click=${this._handlePickLanguage} | ||||
|                           @keydown=${this._handlePickLanguage} | ||||
|                           .selected=${this._language === lang.value} | ||||
|                           .selected=${this._language === lang.id} | ||||
|                         > | ||||
|                           ${lang.label} | ||||
|                           ${lang.primary} | ||||
|                         </ha-md-menu-item>` | ||||
|                     )} | ||||
|                   </ha-md-button-menu>` | ||||
|   | ||||
| @@ -143,9 +143,14 @@ class DialogCalendarEventDetail extends LitElement { | ||||
|       this.hass.locale.time_zone, | ||||
|       this.hass.config.time_zone | ||||
|     ); | ||||
|     const start = new TZDate(this._data!.dtstart, timeZone); | ||||
|     const endValue = new TZDate(this._data!.dtend, timeZone); | ||||
|     // All day events should be displayed as a day earlier | ||||
|     // For all-day events (date-only strings), parse without timezone to avoid offset issues | ||||
|     const start = isDate(this._data!.dtstart) | ||||
|       ? new Date(this._data!.dtstart + "T00:00:00") | ||||
|       : new TZDate(this._data!.dtstart, timeZone); | ||||
|     const endValue = isDate(this._data!.dtend) | ||||
|       ? new Date(this._data!.dtend + "T00:00:00") | ||||
|       : new TZDate(this._data!.dtend, timeZone); | ||||
|     // All day event end dates are exclusive in iCalendar format, subtract one day for display | ||||
|     const end = isDate(this._data.dtend) ? addDays(endValue, -1) : endValue; | ||||
|     // The range can be shortened when the start and end are on the same day. | ||||
|     if (isSameDay(start, end)) { | ||||
|   | ||||
| @@ -15,6 +15,7 @@ import { | ||||
|   getFloors, | ||||
| } from "../../lovelace/strategies/areas/helpers/areas-strategy-helper"; | ||||
| import { getHomeStructure } from "../../lovelace/strategies/home/helpers/home-structure"; | ||||
| import { floorDefaultIcon } from "../../../components/ha-floor-icon"; | ||||
|  | ||||
| export interface ClimateViewStrategyConfig { | ||||
|   type: "climate"; | ||||
| @@ -152,6 +153,7 @@ export class ClimateViewStrategy extends ReactiveElement { | ||||
|               floorCount > 1 | ||||
|                 ? floor.name | ||||
|                 : hass.localize("ui.panel.lovelace.strategy.home.areas"), | ||||
|             icon: floor.icon || floorDefaultIcon(floor), | ||||
|           }, | ||||
|         ], | ||||
|       }; | ||||
|   | ||||
| @@ -265,11 +265,8 @@ class DialogAreaDetail extends LitElement { | ||||
|               ${this.hass.localize("ui.common.delete")} | ||||
|             </ha-button>` | ||||
|           : nothing} | ||||
|         <div slot="primaryAction"> | ||||
|           <ha-button appearance="plain" @click=${this.closeDialog}> | ||||
|             ${this.hass.localize("ui.common.cancel")} | ||||
|           </ha-button> | ||||
|         <ha-button | ||||
|           slot="primaryAction" | ||||
|           @click=${this._updateEntry} | ||||
|           .disabled=${nameInvalid || !!this._submitting} | ||||
|         > | ||||
| @@ -277,7 +274,6 @@ class DialogAreaDetail extends LitElement { | ||||
|             ? this.hass.localize("ui.common.save") | ||||
|             : this.hass.localize("ui.common.create")} | ||||
|         </ha-button> | ||||
|         </div> | ||||
|       </ha-dialog> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -3,6 +3,7 @@ import { | ||||
|   mdiContentCopy, | ||||
|   mdiContentCut, | ||||
|   mdiDelete, | ||||
|   mdiIdentifier, | ||||
|   mdiPlayCircleOutline, | ||||
|   mdiPlaylistEdit, | ||||
|   mdiPlusCircleMultipleOutline, | ||||
| @@ -40,6 +41,8 @@ export default class HaAutomationSidebarTrigger extends LitElement { | ||||
|   @property({ type: Number, attribute: "sidebar-key" }) | ||||
|   public sidebarKey?: number; | ||||
|  | ||||
|   @state() private _requestShowId = false; | ||||
|  | ||||
|   @state() private _warnings?: string[]; | ||||
|  | ||||
|   @query(".sidebar-editor") | ||||
| @@ -47,6 +50,7 @@ export default class HaAutomationSidebarTrigger extends LitElement { | ||||
|  | ||||
|   protected willUpdate(changedProperties) { | ||||
|     if (changedProperties.has("config")) { | ||||
|       this._requestShowId = false; | ||||
|       this._warnings = undefined; | ||||
|       if (this.config) { | ||||
|         this.yamlMode = this.config.yamlMode; | ||||
| @@ -101,6 +105,24 @@ export default class HaAutomationSidebarTrigger extends LitElement { | ||||
|           </div> | ||||
|         </ha-md-menu-item> | ||||
|  | ||||
|         ${!this.yamlMode && | ||||
|         !("id" in this.config.config) && | ||||
|         !this._requestShowId | ||||
|           ? html`<ha-md-menu-item | ||||
|               slot="menu-items" | ||||
|               .clickAction=${this._showTriggerId} | ||||
|               .disabled=${this.disabled || type === "list"} | ||||
|             > | ||||
|               <ha-svg-icon slot="start" .path=${mdiIdentifier}></ha-svg-icon> | ||||
|               <div class="overflow-label"> | ||||
|                 ${this.hass.localize( | ||||
|                   "ui.panel.config.automation.editor.triggers.edit_id" | ||||
|                 )} | ||||
|                 <span class="shortcut-placeholder ${isMac ? "mac" : ""}"></span> | ||||
|               </div> | ||||
|             </ha-md-menu-item>` | ||||
|           : nothing} | ||||
|  | ||||
|         <ha-md-divider | ||||
|           slot="menu-items" | ||||
|           role="separator" | ||||
| @@ -250,6 +272,7 @@ export default class HaAutomationSidebarTrigger extends LitElement { | ||||
|             @value-changed=${this._valueChangedSidebar} | ||||
|             @yaml-changed=${this._yamlChangedSidebar} | ||||
|             .uiSupported=${this.config.uiSupported} | ||||
|             .showId=${this._requestShowId} | ||||
|             .yamlMode=${this.yamlMode} | ||||
|             .disabled=${this.disabled} | ||||
|             @ui-mode-not-available=${this._handleUiModeNotAvailable} | ||||
| @@ -292,6 +315,10 @@ export default class HaAutomationSidebarTrigger extends LitElement { | ||||
|     fireEvent(this, "toggle-yaml-mode"); | ||||
|   }; | ||||
|  | ||||
|   private _showTriggerId = () => { | ||||
|     this._requestShowId = true; | ||||
|   }; | ||||
|  | ||||
|   static styles = [sidebarEditorStyles, overflowStyles]; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -29,6 +29,8 @@ export default class HaAutomationTriggerEditor extends LitElement { | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "sidebar" }) public inSidebar = false; | ||||
|  | ||||
|   @property({ type: Boolean, attribute: "show-id" }) public showId = false; | ||||
|  | ||||
|   @query("ha-yaml-editor") public yamlEditor?: HaYamlEditor; | ||||
|  | ||||
|   protected render() { | ||||
| @@ -36,6 +38,8 @@ export default class HaAutomationTriggerEditor extends LitElement { | ||||
|  | ||||
|     const yamlMode = this.yamlMode || !this.uiSupported; | ||||
|  | ||||
|     const showId = "id" in this.trigger || this.showId; | ||||
|  | ||||
|     return html` | ||||
|       <div | ||||
|         class=${classMap({ | ||||
| @@ -70,20 +74,15 @@ export default class HaAutomationTriggerEditor extends LitElement { | ||||
|               ></ha-yaml-editor> | ||||
|             ` | ||||
|           : html` | ||||
|               ${!isTriggerList(this.trigger) | ||||
|               ${showId && !isTriggerList(this.trigger) | ||||
|                 ? html` | ||||
|                     <ha-textfield | ||||
|                       .label=${`${this.hass.localize( | ||||
|                       .label=${this.hass.localize( | ||||
|                         "ui.panel.config.automation.editor.triggers.id" | ||||
|                       )} (${this.hass.localize( | ||||
|                         "ui.panel.config.automation.editor.triggers.optional" | ||||
|                       )})`} | ||||
|                       )} | ||||
|                       .value=${this.trigger.id || ""} | ||||
|                       .disabled=${this.disabled} | ||||
|                       @change=${this._idChanged} | ||||
|                       .helper=${this.hass.localize( | ||||
|                         "ui.panel.config.automation.editor.triggers.id_helper" | ||||
|                       )} | ||||
|                     ></ha-textfield> | ||||
|                   ` | ||||
|                 : nothing} | ||||
|   | ||||
| @@ -19,6 +19,7 @@ import { ensureArray } from "../../../../../common/array/ensure-array"; | ||||
| import { fireEvent } from "../../../../../common/dom/fire_event"; | ||||
| import { hasTemplate } from "../../../../../common/string/has-template"; | ||||
| import type { StateTrigger } from "../../../../../data/automation"; | ||||
| import { ANY_STATE_VALUE } from "../../../../../components/entity/const"; | ||||
| import type { HomeAssistant } from "../../../../../types"; | ||||
| import { baseTriggerStruct, forDictStruct } from "../../structs"; | ||||
| import type { TriggerElement } from "../ha-automation-trigger-row"; | ||||
| @@ -36,14 +37,12 @@ const stateTriggerStruct = assign( | ||||
|     trigger: literal("state"), | ||||
|     entity_id: optional(union([string(), array(string())])), | ||||
|     attribute: optional(string()), | ||||
|     from: optional(nullable(string())), | ||||
|     to: optional(nullable(string())), | ||||
|     from: optional(union([nullable(string()), array(string())])), | ||||
|     to: optional(union([nullable(string()), array(string())])), | ||||
|     for: optional(union([number(), string(), forDictStruct])), | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const ANY_STATE_VALUE = "__ANY_STATE_IGNORE_ATTRIBUTES__"; | ||||
|  | ||||
| @customElement("ha-automation-trigger-state") | ||||
| export class HaStateTrigger extends LitElement implements TriggerElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -57,7 +56,12 @@ export class HaStateTrigger extends LitElement implements TriggerElement { | ||||
|   } | ||||
|  | ||||
|   private _schema = memoizeOne( | ||||
|     (localize: LocalizeFunc, attribute) => | ||||
|     ( | ||||
|       localize: LocalizeFunc, | ||||
|       attribute: string | undefined, | ||||
|       hideInFrom: string[], | ||||
|       hideInTo: string[] | ||||
|     ) => | ||||
|       [ | ||||
|         { | ||||
|           name: "entity_id", | ||||
| @@ -131,6 +135,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement { | ||||
|           }, | ||||
|           selector: { | ||||
|             state: { | ||||
|               multiple: true, | ||||
|               extra_options: (attribute | ||||
|                 ? [] | ||||
|                 : [ | ||||
| @@ -142,6 +147,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement { | ||||
|                     }, | ||||
|                   ]) as any, | ||||
|               attribute: attribute, | ||||
|               hide_states: hideInFrom, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
| @@ -152,6 +158,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement { | ||||
|           }, | ||||
|           selector: { | ||||
|             state: { | ||||
|               multiple: true, | ||||
|               extra_options: (attribute | ||||
|                 ? [] | ||||
|                 : [ | ||||
| @@ -163,6 +170,7 @@ export class HaStateTrigger extends LitElement implements TriggerElement { | ||||
|                     }, | ||||
|                   ]) as any, | ||||
|               attribute: attribute, | ||||
|               hide_states: hideInTo, | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
| @@ -207,13 +215,15 @@ export class HaStateTrigger extends LitElement implements TriggerElement { | ||||
|       entity_id: ensureArray(this.trigger.entity_id), | ||||
|       for: trgFor, | ||||
|     }; | ||||
|     if (!data.attribute && data.to === null) { | ||||
|       data.to = ANY_STATE_VALUE; | ||||
|     } | ||||
|     if (!data.attribute && data.from === null) { | ||||
|       data.from = ANY_STATE_VALUE; | ||||
|     } | ||||
|     const schema = this._schema(this.hass.localize, this.trigger.attribute); | ||||
|  | ||||
|     data.to = this._normalizeStates(this.trigger.to, data.attribute); | ||||
|     data.from = this._normalizeStates(this.trigger.from, data.attribute); | ||||
|     const schema = this._schema( | ||||
|       this.hass.localize, | ||||
|       this.trigger.attribute, | ||||
|       data.to, | ||||
|       data.from | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
| @@ -231,22 +241,60 @@ export class HaStateTrigger extends LitElement implements TriggerElement { | ||||
|     ev.stopPropagation(); | ||||
|     const newTrigger = ev.detail.value; | ||||
|  | ||||
|     if (newTrigger.to === ANY_STATE_VALUE) { | ||||
|       newTrigger.to = newTrigger.attribute ? undefined : null; | ||||
|     newTrigger.to = this._applyAnyStateExclusive( | ||||
|       newTrigger.to, | ||||
|       newTrigger.attribute | ||||
|     ); | ||||
|     if (Array.isArray(newTrigger.to) && newTrigger.to.length === 0) { | ||||
|       delete newTrigger.to; | ||||
|     } | ||||
|     if (newTrigger.from === ANY_STATE_VALUE) { | ||||
|       newTrigger.from = newTrigger.attribute ? undefined : null; | ||||
|     newTrigger.from = this._applyAnyStateExclusive( | ||||
|       newTrigger.from, | ||||
|       newTrigger.attribute | ||||
|     ); | ||||
|     if (Array.isArray(newTrigger.from) && newTrigger.from.length === 0) { | ||||
|       delete newTrigger.from; | ||||
|     } | ||||
|  | ||||
|     Object.keys(newTrigger).forEach((key) => | ||||
|       newTrigger[key] === undefined || newTrigger[key] === "" | ||||
|         ? delete newTrigger[key] | ||||
|         : {} | ||||
|     ); | ||||
|     Object.keys(newTrigger).forEach((key) => { | ||||
|       const val = newTrigger[key]; | ||||
|       if (val === undefined || val === "") { | ||||
|         delete newTrigger[key]; | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     fireEvent(this, "value-changed", { value: newTrigger }); | ||||
|   } | ||||
|  | ||||
|   private _applyAnyStateExclusive( | ||||
|     val: string | string[] | null | undefined, | ||||
|     attribute?: string | ||||
|   ): string | string[] | null | undefined { | ||||
|     const anyStateSelected = Array.isArray(val) | ||||
|       ? val.includes(ANY_STATE_VALUE) | ||||
|       : val === ANY_STATE_VALUE; | ||||
|     if (anyStateSelected) { | ||||
|       // Any state is exclusive: null if no attribute, undefined if attribute | ||||
|       return attribute ? undefined : null; | ||||
|     } | ||||
|     return val; | ||||
|   } | ||||
|  | ||||
|   private _normalizeStates( | ||||
|     value: string | string[] | null | undefined, | ||||
|     attribute?: string | ||||
|   ): string[] { | ||||
|     // If no attribute is selected and backend value is null, | ||||
|     // expose it as the special ANY state option in the UI. | ||||
|     if (!attribute && value === null) { | ||||
|       return [ANY_STATE_VALUE]; | ||||
|     } | ||||
|     if (value === undefined || value === null) { | ||||
|       return []; | ||||
|     } | ||||
|     return ensureArray(value); | ||||
|   } | ||||
|  | ||||
|   private _computeLabelCallback = ( | ||||
|     schema: SchemaUnion<ReturnType<typeof this._schema>> | ||||
|   ): string => | ||||
|   | ||||
| @@ -98,7 +98,7 @@ class DialogCategoryDetail extends LitElement { | ||||
|         </div> | ||||
|         <ha-button | ||||
|           appearance="plain" | ||||
|           slot="primaryAction" | ||||
|           slot="secondaryAction" | ||||
|           @click=${this.closeDialog} | ||||
|         > | ||||
|           ${this.hass.localize("ui.common.cancel")} | ||||
|   | ||||
| @@ -228,7 +228,7 @@ export class HaDeviceEntitiesCard extends LitElement { | ||||
|     addEntitiesToLovelaceView( | ||||
|       this, | ||||
|       this.hass, | ||||
|       computeCards(this.hass.states, entities, { | ||||
|       computeCards(this.hass, entities, { | ||||
|         title: this.deviceName, | ||||
|       }), | ||||
|       computeSection(entities, { | ||||
|   | ||||
| @@ -153,6 +153,8 @@ class HaConfigHardware extends SubscribeMixin(LitElement) { | ||||
|         }, | ||||
|         yAxis: { | ||||
|           type: "value", | ||||
|           min: 0, | ||||
|           max: 100, | ||||
|           splitLine: { | ||||
|             show: true, | ||||
|           }, | ||||
|   | ||||
| @@ -6,7 +6,6 @@ import { | ||||
| import { LitElement, css, html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { repeat } from "lit/directives/repeat"; | ||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { formatDateTimeWithSeconds } from "../../../../common/datetime/format_date_time"; | ||||
| import type { | ||||
|   PipelineRunEvent, | ||||
| @@ -21,8 +20,6 @@ import "../../../../layouts/hass-subpage"; | ||||
| import { haStyle } from "../../../../resources/styles"; | ||||
| import type { HomeAssistant, Route } from "../../../../types"; | ||||
| import "./assist-render-pipeline-events"; | ||||
| import type { ChatLog } from "../../../../data/chat_log"; | ||||
| import { subscribeChatLog } from "../../../../data/chat_log"; | ||||
|  | ||||
| @customElement("assist-pipeline-debug") | ||||
| export class AssistPipelineDebug extends LitElement { | ||||
| @@ -40,12 +37,8 @@ export class AssistPipelineDebug extends LitElement { | ||||
|  | ||||
|   @state() private _events?: PipelineRunEvent[]; | ||||
|  | ||||
|   @state() private _chatLog?: ChatLog; | ||||
|  | ||||
|   private _unsubRefreshEventsID?: number; | ||||
|  | ||||
|   private _unsubChatLogUpdates?: Promise<UnsubscribeFunc>; | ||||
|  | ||||
|   protected render() { | ||||
|     return html`<hass-subpage | ||||
|       .narrow=${this.narrow} | ||||
| @@ -113,7 +106,6 @@ export class AssistPipelineDebug extends LitElement { | ||||
|           ? html`<assist-render-pipeline-events | ||||
|               .hass=${this.hass} | ||||
|               .events=${this._events} | ||||
|               .chatLog=${this._chatLog} | ||||
|             ></assist-render-pipeline-events>` | ||||
|           : ""} | ||||
|       </div> | ||||
| @@ -128,10 +120,6 @@ export class AssistPipelineDebug extends LitElement { | ||||
|       clearRefresh = true; | ||||
|     } | ||||
|     if (changedProperties.has("_runId")) { | ||||
|       if (this._unsubChatLogUpdates) { | ||||
|         this._unsubChatLogUpdates.then((unsub) => unsub()); | ||||
|         this._unsubChatLogUpdates = undefined; | ||||
|       } | ||||
|       this._fetchEvents(); | ||||
|       clearRefresh = true; | ||||
|     } | ||||
| @@ -147,10 +135,6 @@ export class AssistPipelineDebug extends LitElement { | ||||
|       clearTimeout(this._unsubRefreshEventsID); | ||||
|       this._unsubRefreshEventsID = undefined; | ||||
|     } | ||||
|     if (this._unsubChatLogUpdates) { | ||||
|       this._unsubChatLogUpdates.then((unsub) => unsub()); | ||||
|       this._unsubChatLogUpdates = undefined; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private async _fetchRuns() { | ||||
| @@ -197,27 +181,8 @@ export class AssistPipelineDebug extends LitElement { | ||||
|       }); | ||||
|       return; | ||||
|     } | ||||
|     if (!this._events!.length) { | ||||
|       return; | ||||
|     } | ||||
|     if (!this._unsubChatLogUpdates && this._events[0].type === "run-start") { | ||||
|       this._unsubChatLogUpdates = subscribeChatLog( | ||||
|         this.hass, | ||||
|         this._events[0].data.conversation_id, | ||||
|         (chatLog) => { | ||||
|           if (chatLog) { | ||||
|             this._chatLog = chatLog; | ||||
|           } else { | ||||
|             this._unsubChatLogUpdates?.then((unsub) => unsub()); | ||||
|             this._unsubChatLogUpdates = undefined; | ||||
|           } | ||||
|         } | ||||
|       ); | ||||
|       this._unsubChatLogUpdates.catch(() => { | ||||
|         this._unsubChatLogUpdates = undefined; | ||||
|       }); | ||||
|     } | ||||
|     if ( | ||||
|       this._events?.length && | ||||
|       // If the last event is not a finish run event, the run is still ongoing. | ||||
|       // Refresh events automatically. | ||||
|       !["run-end", "error"].includes(this._events[this._events.length - 1].type) | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import type { TemplateResult } from "lit"; | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { extractSearchParam } from "../../../../common/url/search-params"; | ||||
| import "../../../../components/ha-assist-pipeline-picker"; | ||||
| import "../../../../components/ha-button"; | ||||
| @@ -25,8 +24,6 @@ import type { HomeAssistant } from "../../../../types"; | ||||
| import { AudioRecorder } from "../../../../util/audio-recorder"; | ||||
| import { fileDownload } from "../../../../util/file_download"; | ||||
| import "./assist-render-pipeline-run"; | ||||
| import type { ChatLog } from "../../../../data/chat_log"; | ||||
| import { subscribeChatLog } from "../../../../data/chat_log"; | ||||
|  | ||||
| @customElement("assist-pipeline-run-debug") | ||||
| export class AssistPipelineRunDebug extends LitElement { | ||||
| @@ -49,13 +46,6 @@ export class AssistPipelineRunDebug extends LitElement { | ||||
|   @state() private _pipelineId?: string = | ||||
|     extractSearchParam("pipeline") || undefined; | ||||
|  | ||||
|   @state() private _chatLog?: ChatLog; | ||||
|  | ||||
|   private _chatLogSubscription: { | ||||
|     conversationId: string; | ||||
|     unsub: Promise<UnsubscribeFunc>; | ||||
|   } | null = null; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     return html` | ||||
|       <hass-subpage | ||||
| @@ -188,7 +178,6 @@ export class AssistPipelineRunDebug extends LitElement { | ||||
|                   <assist-render-pipeline-run | ||||
|                     .hass=${this.hass} | ||||
|                     .pipelineRun=${run} | ||||
|                     .chatLog=${this._chatLog} | ||||
|                   ></assist-render-pipeline-run> | ||||
|                 ` | ||||
|           )} | ||||
| @@ -197,14 +186,6 @@ export class AssistPipelineRunDebug extends LitElement { | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   public disconnectedCallback(): void { | ||||
|     super.disconnectedCallback(); | ||||
|     if (this._chatLogSubscription) { | ||||
|       this._chatLogSubscription.unsub.then((unsub) => unsub()); | ||||
|       this._chatLogSubscription = null; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   private get conversationId(): string | null { | ||||
|     return this._pipelineRuns.length === 0 | ||||
|       ? null | ||||
| @@ -427,32 +408,6 @@ export class AssistPipelineRunDebug extends LitElement { | ||||
|             added = true; | ||||
|           } | ||||
|           callback(updatedRun); | ||||
|  | ||||
|           const conversationId = this.conversationId; | ||||
|           if ( | ||||
|             !this._chatLog && | ||||
|             conversationId && | ||||
|             (!this._chatLogSubscription || | ||||
|               this._chatLogSubscription.conversationId !== conversationId) | ||||
|           ) { | ||||
|             if (this._chatLogSubscription) { | ||||
|               this._chatLogSubscription.unsub.then((unsub) => unsub()); | ||||
|             } | ||||
|             this._chatLogSubscription = { | ||||
|               conversationId, | ||||
|               unsub: subscribeChatLog(this.hass, conversationId, (chatLog) => { | ||||
|                 if (chatLog) { | ||||
|                   this._chatLog = chatLog; | ||||
|                 } else { | ||||
|                   this._chatLogSubscription?.unsub.then((unsub) => unsub()); | ||||
|                   this._chatLogSubscription = null; | ||||
|                 } | ||||
|               }), | ||||
|             }; | ||||
|             this._chatLogSubscription.unsub.catch(() => { | ||||
|               this._chatLogSubscription = null; | ||||
|             }); | ||||
|           } | ||||
|         }, | ||||
|         { | ||||
|           ...options, | ||||
|   | ||||
| @@ -9,7 +9,6 @@ import type { | ||||
| import { processEvent } from "../../../../data/assist_pipeline"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import "./assist-render-pipeline-run"; | ||||
| import type { ChatLog } from "../../../../data/chat_log"; | ||||
|  | ||||
| @customElement("assist-render-pipeline-events") | ||||
| export class AssistPipelineEvents extends LitElement { | ||||
| @@ -17,8 +16,6 @@ export class AssistPipelineEvents extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) public events!: PipelineRunEvent[]; | ||||
|  | ||||
|   @property({ attribute: false }) public chatLog?: ChatLog; | ||||
|  | ||||
|   private _processEvents = memoizeOne( | ||||
|     (events: PipelineRunEvent[]): PipelineRun | undefined => { | ||||
|       let run: PipelineRun | undefined; | ||||
| @@ -49,7 +46,6 @@ export class AssistPipelineEvents extends LitElement { | ||||
|       <assist-render-pipeline-run | ||||
|         .hass=${this.hass} | ||||
|         .pipelineRun=${run} | ||||
|         .chatLog=${this.chatLog} | ||||
|       ></assist-render-pipeline-run> | ||||
|     `; | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import type { TemplateResult } from "lit"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { customElement, property } from "lit/decorators"; | ||||
| import "../../../../components/ha-card"; | ||||
| import "../../../../components/ha-alert"; | ||||
| @@ -11,12 +11,6 @@ import type { HomeAssistant } from "../../../../types"; | ||||
| import { formatNumber } from "../../../../common/number/format_number"; | ||||
| import "../../../../components/ha-yaml-editor"; | ||||
| import { showAlertDialog } from "../../../../dialogs/generic/show-dialog-box"; | ||||
| import type { | ||||
|   ChatLogAssistantContent, | ||||
|   ChatLog, | ||||
|   ChatLogContent, | ||||
|   ChatLogUserContent, | ||||
| } from "../../../../data/chat_log"; | ||||
|  | ||||
| const RUN_DATA = { | ||||
|   pipeline: "Pipeline", | ||||
| @@ -132,7 +126,7 @@ const dataMinusKeysRender = ( | ||||
|     result[key] = data[key]; | ||||
|   } | ||||
|   return render | ||||
|     ? html`<ha-expansion-panel class="yaml-expansion"> | ||||
|     ? html`<ha-expansion-panel> | ||||
|         <span slot="header">Raw</span> | ||||
|         <ha-yaml-editor readOnly autoUpdate .value=${result}></ha-yaml-editor> | ||||
|       </ha-expansion-panel>` | ||||
| @@ -145,8 +139,6 @@ export class AssistPipelineDebug extends LitElement { | ||||
|  | ||||
|   @property({ attribute: false }) public pipelineRun!: PipelineRun; | ||||
|  | ||||
|   @property({ attribute: false }) public chatLog?: ChatLog; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const lastRunStage: string = this.pipelineRun | ||||
|       ? ["tts", "intent", "stt", "wake_word"].find( | ||||
| @@ -154,23 +146,8 @@ export class AssistPipelineDebug extends LitElement { | ||||
|         ) || "ready" | ||||
|       : "ready"; | ||||
|  | ||||
|     let messages: ChatLogContent[]; | ||||
|     const messages: { from: string; text: string }[] = []; | ||||
|  | ||||
|     if (this.chatLog) { | ||||
|       messages = this.chatLog.content.filter( | ||||
|         this.pipelineRun.finished | ||||
|           ? (content: ChatLogContent) => | ||||
|               content.role === "system" || | ||||
|               (content.created >= this.pipelineRun.started && | ||||
|                 content.created <= this.pipelineRun.finished!) | ||||
|           : (content: ChatLogContent) => | ||||
|               content.role === "system" || | ||||
|               content.created >= this.pipelineRun.started | ||||
|       ); | ||||
|     } else { | ||||
|       messages = []; | ||||
|  | ||||
|       // We don't have the chat log everywhere yet, just fallback for now. | ||||
|     const userMessage = | ||||
|       (this.pipelineRun.init_options && | ||||
|       "text" in this.pipelineRun.init_options.input | ||||
| @@ -181,20 +158,19 @@ export class AssistPipelineDebug extends LitElement { | ||||
|  | ||||
|     if (userMessage) { | ||||
|       messages.push({ | ||||
|           role: "user", | ||||
|           content: userMessage, | ||||
|         } as ChatLogUserContent); | ||||
|         from: "user", | ||||
|         text: userMessage, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     if ( | ||||
|       this.pipelineRun?.intent?.intent_output?.response?.speech?.plain?.speech | ||||
|     ) { | ||||
|       messages.push({ | ||||
|           role: "assistant", | ||||
|           content: | ||||
|             this.pipelineRun.intent.intent_output.response.speech.plain.speech, | ||||
|         } as ChatLogAssistantContent); | ||||
|       } | ||||
|         from: "hass", | ||||
|         text: this.pipelineRun.intent.intent_output.response.speech.plain | ||||
|           .speech, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
| @@ -209,57 +185,9 @@ export class AssistPipelineDebug extends LitElement { | ||||
|           ${messages.length > 0 | ||||
|             ? html` | ||||
|                 <div class="messages"> | ||||
|                   ${messages.map((content) => | ||||
|                     content.role === "system" || content.role === "tool_result" | ||||
|                       ? html` | ||||
|                           <ha-expansion-panel | ||||
|                             class="content-expansion ${content.role}" | ||||
|                           > | ||||
|                             <div slot="header"> | ||||
|                               ${content.role === "system" | ||||
|                                 ? "System" | ||||
|                                 : `Result for ${content.tool_name}`} | ||||
|                             </div> | ||||
|                             ${content.role === "system" | ||||
|                               ? html`<pre>${content.content}</pre>` | ||||
|                               : html` | ||||
|                                   <ha-yaml-editor | ||||
|                                     read-only | ||||
|                                     auto-update | ||||
|                                     .value=${content} | ||||
|                                   ></ha-yaml-editor> | ||||
|                                 `} | ||||
|                           </ha-expansion-panel> | ||||
|                         ` | ||||
|                       : html` | ||||
|                           ${content.content | ||||
|                             ? html` | ||||
|                                 <div class=${`message ${content.role}`}> | ||||
|                                   ${content.content} | ||||
|                                 </div> | ||||
|                               ` | ||||
|                             : nothing} | ||||
|                           ${content.role === "assistant" && | ||||
|                           content.tool_calls?.length | ||||
|                             ? html` | ||||
|                                 <ha-expansion-panel | ||||
|                                   class="content-expansion assistant" | ||||
|                                 > | ||||
|                                   <span slot="header"> | ||||
|                                     Call | ||||
|                                     ${content.tool_calls.length === 1 | ||||
|                                       ? content.tool_calls[0].tool_name | ||||
|                                       : `${content.tool_calls.length} tools`} | ||||
|                                   </span> | ||||
|  | ||||
|                                   <ha-yaml-editor | ||||
|                                     read-only | ||||
|                                     auto-update | ||||
|                                     .value=${content.tool_calls} | ||||
|                                   ></ha-yaml-editor> | ||||
|                                 </ha-expansion-panel> | ||||
|                               ` | ||||
|                             : nothing} | ||||
|                   ${messages.map( | ||||
|                     ({ from, text }) => html` | ||||
|                       <div class=${`message ${from}`}>${text}</div> | ||||
|                     ` | ||||
|                   )} | ||||
|                 </div> | ||||
| @@ -432,7 +360,7 @@ export class AssistPipelineDebug extends LitElement { | ||||
|         : ""} | ||||
|       ${maybeRenderError(this.pipelineRun, "tts", lastRunStage)} | ||||
|       <ha-card> | ||||
|         <ha-expansion-panel class="yaml-expansion"> | ||||
|         <ha-expansion-panel> | ||||
|           <span slot="header">Raw</span> | ||||
|           <ha-yaml-editor | ||||
|             read-only | ||||
| @@ -471,12 +399,12 @@ export class AssistPipelineDebug extends LitElement { | ||||
|     .row > div:last-child { | ||||
|       text-align: right; | ||||
|     } | ||||
|     .yaml-expansion { | ||||
|     ha-expansion-panel { | ||||
|       padding-left: 8px; | ||||
|       padding-inline-start: 8px; | ||||
|       padding-inline-end: initial; | ||||
|     } | ||||
|     .card-content .yaml-expansion { | ||||
|     .card-content ha-expansion-panel { | ||||
|       padding-left: 0px; | ||||
|       padding-inline-start: 0px; | ||||
|       padding-inline-end: initial; | ||||
| @@ -492,59 +420,27 @@ export class AssistPipelineDebug extends LitElement { | ||||
|       margin-top: 8px; | ||||
|     } | ||||
|  | ||||
|     .content-expansion { | ||||
|       margin: 8px 0; | ||||
|       border-radius: var(--ha-border-radius-xl); | ||||
|       clear: both; | ||||
|       padding: 0 8px; | ||||
|       --input-fill-color: none; | ||||
|       max-width: calc(100% - 24px); | ||||
|       --expansion-panel-summary-padding: 0px; | ||||
|       --expansion-panel-content-padding: 0px; | ||||
|     } | ||||
|  | ||||
|     .content-expansion *[slot="header"] { | ||||
|       font-weight: var(--ha-font-weight-normal); | ||||
|     } | ||||
|  | ||||
|     .system { | ||||
|       background-color: var(--success-color); | ||||
|     } | ||||
|  | ||||
|     .message { | ||||
|       padding: 8px; | ||||
|     } | ||||
|  | ||||
|     .message, | ||||
|     .content-expansion { | ||||
|       font-size: var(--ha-font-size-l); | ||||
|       margin: 8px 0; | ||||
|       padding: 8px; | ||||
|       border-radius: var(--ha-border-radius-xl); | ||||
|       clear: both; | ||||
|     } | ||||
|  | ||||
|     .messages pre { | ||||
|       white-space: pre-wrap; | ||||
|     } | ||||
|  | ||||
|     .user, | ||||
|     .tool_result { | ||||
|     .message.user { | ||||
|       margin-left: 24px; | ||||
|       margin-inline-start: 24px; | ||||
|       margin-inline-end: initial; | ||||
|       float: var(--float-end); | ||||
|       text-align: right; | ||||
|       border-bottom-right-radius: 0px; | ||||
|       background-color: var(--light-primary-color); | ||||
|       color: var(--text-light-primary-color, var(--primary-text-color)); | ||||
|       direction: var(--direction); | ||||
|     } | ||||
|  | ||||
|     .message.user, | ||||
|     .content-expansion div[slot="header"] { | ||||
|       text-align: right; | ||||
|     } | ||||
|  | ||||
|     .assistant { | ||||
|     .message.hass { | ||||
|       margin-right: 24px; | ||||
|       margin-inline-end: 24px; | ||||
|       margin-inline-start: initial; | ||||
|   | ||||
| @@ -280,10 +280,11 @@ ${type === "object" | ||||
|  | ||||
|         .content.horizontal { | ||||
|           --code-mirror-max-height: calc( | ||||
|             100vh - var(--header-height) - (var(--ha-line-height-normal) * 3) - | ||||
|               (1em * 2) - (max(16px, var(--safe-area-inset-top)) * 2) - | ||||
|             100vh - var(--header-height) - | ||||
|               (var(--ha-line-height-normal) * var(--ha-font-size-m) * 3) - | ||||
|               (max(16px, var(--safe-area-inset-top)) * 2) - | ||||
|               (max(16px, var(--safe-area-inset-bottom)) * 2) - | ||||
|               (var(--ha-card-border-width, 1px) * 2) - 179px | ||||
|               (var(--ha-card-border-width, 1px) * 3) - (1em * 2) - 192px | ||||
|           ); | ||||
|         } | ||||
|  | ||||
|   | ||||
| @@ -313,9 +313,14 @@ class HaPanelHistory extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const statsStartDate = new Date(this._startDate); | ||||
|     // History uses the end datapoint of the statistic, so if we want the | ||||
|     // graph to start at 7AM, need to fetch the statistic from 6AM. | ||||
|     statsStartDate.setHours(statsStartDate.getHours() - 1); | ||||
|  | ||||
|     const statistics = await fetchStatistics( | ||||
|       this.hass!, | ||||
|       this._startDate, | ||||
|       statsStartDate, | ||||
|       this._endDate, | ||||
|       statisticIds, | ||||
|       "hour", | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { | ||||
|   generateEntityFilter, | ||||
|   type EntityFilter, | ||||
| } from "../../../common/entity/entity_filter"; | ||||
| import { floorDefaultIcon } from "../../../components/ha-floor-icon"; | ||||
| import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import type { LovelaceSectionRawConfig } from "../../../data/lovelace/config/section"; | ||||
| import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; | ||||
| @@ -98,6 +99,7 @@ export class LightViewStrategy extends ReactiveElement { | ||||
|               floorCount > 1 | ||||
|                 ? floor.name | ||||
|                 : hass.localize("ui.panel.lovelace.strategy.home.areas"), | ||||
|             icon: floor.icon || floorDefaultIcon(floor), | ||||
|           }, | ||||
|         ], | ||||
|       }; | ||||
|   | ||||
| @@ -0,0 +1,126 @@ | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import "../../../components/ha-control-number-buttons"; | ||||
| import { isUnavailableState } from "../../../data/entity"; | ||||
| import { | ||||
|   MediaPlayerEntityFeature, | ||||
|   type MediaPlayerEntity, | ||||
| } from "../../../data/media-player"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import type { LovelaceCardFeature, LovelaceCardFeatureEditor } from "../types"; | ||||
| import { cardFeatureStyles } from "./common/card-feature-styles"; | ||||
| import type { | ||||
|   LovelaceCardFeatureContext, | ||||
|   MediaPlayerVolumeButtonsCardFeatureConfig, | ||||
| } from "./types"; | ||||
| import { clamp } from "../../../common/number/clamp"; | ||||
|  | ||||
| export const supportsMediaPlayerVolumeButtonsCardFeature = ( | ||||
|   hass: HomeAssistant, | ||||
|   context: LovelaceCardFeatureContext | ||||
| ) => { | ||||
|   const stateObj = context.entity_id | ||||
|     ? hass.states[context.entity_id] | ||||
|     : undefined; | ||||
|   if (!stateObj) return false; | ||||
|   const domain = computeDomain(stateObj.entity_id); | ||||
|   return ( | ||||
|     domain === "media_player" && | ||||
|     supportsFeature(stateObj, MediaPlayerEntityFeature.VOLUME_SET) | ||||
|   ); | ||||
| }; | ||||
|  | ||||
| @customElement("hui-media-player-volume-buttons-card-feature") | ||||
| class HuiMediaPlayerVolumeButtonsCardFeature | ||||
|   extends LitElement | ||||
|   implements LovelaceCardFeature | ||||
| { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public context?: LovelaceCardFeatureContext; | ||||
|  | ||||
|   @state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig; | ||||
|  | ||||
|   private get _stateObj() { | ||||
|     if (!this.hass || !this.context || !this.context.entity_id) { | ||||
|       return undefined; | ||||
|     } | ||||
|     return this.hass.states[this.context.entity_id] as | ||||
|       | MediaPlayerEntity | ||||
|       | undefined; | ||||
|   } | ||||
|  | ||||
|   static getStubConfig(): MediaPlayerVolumeButtonsCardFeatureConfig { | ||||
|     return { | ||||
|       type: "media-player-volume-buttons", | ||||
|       step: 5, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   public static async getConfigElement(): Promise<LovelaceCardFeatureEditor> { | ||||
|     await import( | ||||
|       "../editor/config-elements/hui-media-player-volume-buttons-card-feature-editor" | ||||
|     ); | ||||
|     return document.createElement( | ||||
|       "hui-media-player-volume-buttons-card-feature-editor" | ||||
|     ); | ||||
|   } | ||||
|  | ||||
|   public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void { | ||||
|     if (!config) { | ||||
|       throw new Error("Invalid configuration"); | ||||
|     } | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if ( | ||||
|       !this._config || | ||||
|       !this.hass || | ||||
|       !this.context || | ||||
|       !this._stateObj || | ||||
|       !supportsMediaPlayerVolumeButtonsCardFeature(this.hass, this.context) | ||||
|     ) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     const position = | ||||
|       this._stateObj.attributes.volume_level != null | ||||
|         ? Math.round(this._stateObj.attributes.volume_level * 100) | ||||
|         : undefined; | ||||
|  | ||||
|     return html` | ||||
|       <ha-control-number-buttons | ||||
|         .disabled=${!this._stateObj || isUnavailableState(this._stateObj.state)} | ||||
|         .locale=${this.hass.locale} | ||||
|         min="0" | ||||
|         max="100" | ||||
|         .step=${this._config.step ?? 5} | ||||
|         .value=${position} | ||||
|         unit="%" | ||||
|         @value-changed=${this._valueChanged} | ||||
|       ></ha-control-number-buttons> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent) { | ||||
|     ev.stopPropagation(); | ||||
|  | ||||
|     this.hass!.callService("media_player", "volume_set", { | ||||
|       entity_id: this._stateObj!.entity_id, | ||||
|       volume_level: clamp(ev.detail.value, 0, 100) / 100, | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   static get styles() { | ||||
|     return cardFeatureStyles; | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "hui-media-player-volume-buttons-card-feature": HuiMediaPlayerVolumeButtonsCardFeature; | ||||
|   } | ||||
| } | ||||
| @@ -94,7 +94,7 @@ class HuiHistoryChartCardFeature | ||||
|     } | ||||
|     if (!this._coordinates) { | ||||
|       return html` | ||||
|         <div class="container"> | ||||
|         <div class="container loading"> | ||||
|           <ha-spinner size="small"></ha-spinner> | ||||
|         </div> | ||||
|       `; | ||||
| @@ -153,6 +153,14 @@ class HuiHistoryChartCardFeature | ||||
|       align-items: flex-end; | ||||
|       pointer-events: none !important; | ||||
|     } | ||||
|  | ||||
|     .container.loading { | ||||
|       width: 100%; | ||||
|       display: flex; | ||||
|       justify-content: center; | ||||
|       align-items: center; | ||||
|     } | ||||
|  | ||||
|     hui-graph-base { | ||||
|       width: 100%; | ||||
|       --accent-color: var(--feature-color); | ||||
|   | ||||
| @@ -50,6 +50,11 @@ export interface MediaPlayerVolumeSliderCardFeatureConfig { | ||||
|   type: "media-player-volume-slider"; | ||||
| } | ||||
|  | ||||
| export interface MediaPlayerVolumeButtonsCardFeatureConfig { | ||||
|   type: "media-player-volume-buttons"; | ||||
|   step?: number; | ||||
| } | ||||
|  | ||||
| export interface FanDirectionCardFeatureConfig { | ||||
|   type: "fan-direction"; | ||||
| } | ||||
| @@ -252,6 +257,7 @@ export type LovelaceCardFeatureConfig = | ||||
|   | LockCommandsCardFeatureConfig | ||||
|   | LockOpenDoorCardFeatureConfig | ||||
|   | MediaPlayerPlaybackCardFeatureConfig | ||||
|   | MediaPlayerVolumeButtonsCardFeatureConfig | ||||
|   | MediaPlayerVolumeSliderCardFeatureConfig | ||||
|   | NumericInputCardFeatureConfig | ||||
|   | SelectOptionsCardFeatureConfig | ||||
|   | ||||
| @@ -35,6 +35,8 @@ import { measureTextWidth } from "../../../../util/text"; | ||||
| import "../../../../components/ha-icon-button"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
| import { listenMediaQuery } from "../../../../common/dom/media_query"; | ||||
| import { getEnergyColor } from "./common/color"; | ||||
| import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base"; | ||||
|  | ||||
| @customElement("hui-energy-devices-graph-card") | ||||
| export class HuiEnergyDevicesGraphCard | ||||
| @@ -49,6 +51,8 @@ export class HuiEnergyDevicesGraphCard | ||||
|  | ||||
|   @state() private _data?: EnergyData; | ||||
|  | ||||
|   @state() private _legendData: NonNullable<CustomLegendOption["data"]> = []; | ||||
|  | ||||
|   @state() | ||||
|   @storage({ | ||||
|     key: "energy-devices-graph-chart-type", | ||||
| @@ -57,6 +61,14 @@ export class HuiEnergyDevicesGraphCard | ||||
|   }) | ||||
|   private _chartType: "bar" | "pie" = "bar"; | ||||
|  | ||||
|   @state() | ||||
|   @storage({ | ||||
|     key: "energy-devices-pie-hidden-stats", | ||||
|     state: true, | ||||
|     subscribe: false, | ||||
|   }) | ||||
|   private _hiddenStats: string[] = []; | ||||
|  | ||||
|   @state() private _isMobile = false; | ||||
|  | ||||
|   private _compoundStats: string[] = []; | ||||
| @@ -121,10 +133,16 @@ export class HuiEnergyDevicesGraphCard | ||||
|           <ha-chart-base | ||||
|             .hass=${this.hass} | ||||
|             .data=${this._chartData} | ||||
|             .options=${this._createOptions(this._chartData, this._chartType)} | ||||
|             .height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`} | ||||
|             @chart-click=${this._handleChartClick} | ||||
|             .options=${this._createOptions( | ||||
|               this._chartData, | ||||
|               this._chartType, | ||||
|               this._legendData | ||||
|             )} | ||||
|             .height=${`${Math.max(300, (this._legendData?.length || 0) * 28 + 50)}px`} | ||||
|             .extraComponents=${[PieChart]} | ||||
|             @chart-click=${this._handleChartClick} | ||||
|             @dataset-hidden=${this._datasetHidden} | ||||
|             @dataset-unhidden=${this._datasetUnhidden} | ||||
|           ></ha-chart-base> | ||||
|         </div> | ||||
|       </ha-card> | ||||
| @@ -145,7 +163,8 @@ export class HuiEnergyDevicesGraphCard | ||||
|   private _createOptions = memoizeOne( | ||||
|     ( | ||||
|       data: (BarSeriesOption | PieSeriesOption)[], | ||||
|       chartType: "bar" | "pie" | ||||
|       chartType: "bar" | "pie", | ||||
|       legendData: typeof this._legendData | ||||
|     ): ECOption => { | ||||
|       const options: ECOption = { | ||||
|         grid: { | ||||
| @@ -161,6 +180,7 @@ export class HuiEnergyDevicesGraphCard | ||||
|         }, | ||||
|         xAxis: { show: false }, | ||||
|         yAxis: { show: false }, | ||||
|         legend: { type: "custom", show: false }, | ||||
|       }; | ||||
|       if (chartType === "bar") { | ||||
|         options.xAxis = { | ||||
| @@ -191,6 +211,18 @@ export class HuiEnergyDevicesGraphCard | ||||
|             ), | ||||
|           }, | ||||
|         }; | ||||
|       } else { | ||||
|         options.legend = { | ||||
|           type: "custom", | ||||
|           show: true, | ||||
|           data: legendData, | ||||
|           selected: legendData | ||||
|             .filter((d) => d.id && this._hiddenStats.includes(d.id)) | ||||
|             .reduce((acc, d) => { | ||||
|               acc[d.id!] = false; | ||||
|               return acc; | ||||
|             }, {}), | ||||
|         }; | ||||
|       } | ||||
|       return options; | ||||
|     } | ||||
| @@ -354,23 +386,12 @@ export class HuiEnergyDevicesGraphCard | ||||
|       } | ||||
|     }); | ||||
|  | ||||
|     chartData.sort((a: any, b: any) => b.value[0] - a.value[0]); | ||||
|     if (compareData) { | ||||
|       datasets[1].data = chartData.map((d) => | ||||
|         chartDataCompare.find((d2) => (d2 as any).id === d.id) | ||||
|       ) as typeof chartDataCompare; | ||||
|     } | ||||
|  | ||||
|     datasets.forEach((dataset) => { | ||||
|       dataset.data!.length = Math.min( | ||||
|         this._config?.max_devices || Infinity, | ||||
|         dataset.data!.length | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     if (this._chartType === "pie") { | ||||
|       const { summedData } = getSummedData(energyData); | ||||
|       const { consumption } = computeConsumptionData(summedData); | ||||
|       const { summedData, compareSummedData } = getSummedData(energyData); | ||||
|       const { consumption, compareConsumption } = computeConsumptionData( | ||||
|         summedData, | ||||
|         compareSummedData | ||||
|       ); | ||||
|       const totalUsed = consumption.total.used_total; | ||||
|       const showUntracked = | ||||
|         "from_grid" in summedData || | ||||
| @@ -380,6 +401,47 @@ export class HuiEnergyDevicesGraphCard | ||||
|         ? totalUsed - | ||||
|           chartData.reduce((acc: number, d: any) => acc + d.value[0], 0) | ||||
|         : 0; | ||||
|       if (untracked > 0) { | ||||
|         const color = getEnergyColor( | ||||
|           computedStyle, | ||||
|           this.hass.themes.darkMode, | ||||
|           false, | ||||
|           false, | ||||
|           "--history-unknown-color" | ||||
|         ); | ||||
|         chartData.push({ | ||||
|           id: "untracked", | ||||
|           value: [untracked, "untracked"] as any, | ||||
|           name: this.hass.localize( | ||||
|             "ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption" | ||||
|           ), | ||||
|           itemStyle: { | ||||
|             color: color + "7F", | ||||
|             borderColor: color, | ||||
|           }, | ||||
|         }); | ||||
|         if (compareData) { | ||||
|           const compareUntracked = | ||||
|             compareConsumption!.total.used_total - | ||||
|             chartDataCompare.reduce( | ||||
|               (acc: number, d: any) => acc + d.value[0], | ||||
|               0 | ||||
|             ); | ||||
|           if (compareUntracked > 0) { | ||||
|             chartDataCompare.push({ | ||||
|               id: "untracked", | ||||
|               value: [compareUntracked, "untracked"] as any, | ||||
|               name: this.hass.localize( | ||||
|                 "ui.panel.lovelace.cards.energy.energy_devices_graph.untracked_consumption" | ||||
|               ), | ||||
|               itemStyle: { | ||||
|                 color: color + "32", | ||||
|                 borderColor: color + "7F", | ||||
|               }, | ||||
|             }); | ||||
|           } | ||||
|         } | ||||
|       } | ||||
|       datasets.push({ | ||||
|         type: "pie", | ||||
|         radius: ["0%", compareData ? "30%" : "40%"], | ||||
| @@ -401,17 +463,36 @@ export class HuiEnergyDevicesGraphCard | ||||
|           color: "rgba(0, 0, 0, 0)", | ||||
|         }, | ||||
|         tooltip: { | ||||
|           formatter: () => | ||||
|             untracked > 0 | ||||
|               ? this.hass.localize( | ||||
|                   "ui.panel.lovelace.cards.energy.energy_devices_graph.includes_untracked", | ||||
|                   { num: formatNumber(untracked, this.hass.locale) } | ||||
|                 ) | ||||
|               : "", | ||||
|           show: false, | ||||
|         }, | ||||
|       }); | ||||
|     } | ||||
|  | ||||
|     chartData.sort((a: any, b: any) => b.value[0] - a.value[0]); | ||||
|     if ( | ||||
|       this._config?.max_devices && | ||||
|       chartData.length > this._config.max_devices | ||||
|     ) { | ||||
|       chartData.splice(this._config.max_devices); | ||||
|     } | ||||
|  | ||||
|     this._legendData = chartData.map((d) => ({ | ||||
|       ...d, | ||||
|       name: this._getDeviceName(d.name), | ||||
|     })); | ||||
|     // filter out hidden stats in place | ||||
|     for (let i = chartData.length - 1; i >= 0; i--) { | ||||
|       if (this._hiddenStats.includes((chartData[i] as any).id)) { | ||||
|         chartData.splice(i, 1); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (compareData) { | ||||
|       datasets[1].data = chartData.map((d) => | ||||
|         chartDataCompare.find((d2) => (d2 as any).id === d.id) | ||||
|       ) as typeof chartDataCompare; | ||||
|     } | ||||
|  | ||||
|     this._chartData = datasets; | ||||
|     await this.updateComplete; | ||||
|   } | ||||
| @@ -440,6 +521,18 @@ export class HuiEnergyDevicesGraphCard | ||||
|     this._getStatistics(this._data!); | ||||
|   } | ||||
|  | ||||
|   private _datasetHidden(ev: CustomEvent<{ id: string }>) { | ||||
|     this._hiddenStats = [...this._hiddenStats, ev.detail.id]; | ||||
|     this._getStatistics(this._data!); | ||||
|   } | ||||
|  | ||||
|   private _datasetUnhidden(ev: CustomEvent<{ id: string }>) { | ||||
|     this._hiddenStats = this._hiddenStats.filter( | ||||
|       (stat) => stat !== ev.detail.id | ||||
|     ); | ||||
|     this._getStatistics(this._data!); | ||||
|   } | ||||
|  | ||||
|   static styles = css` | ||||
|     .card-header { | ||||
|       display: flex; | ||||
|   | ||||
| @@ -137,6 +137,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { | ||||
|           class=${classMap({ | ||||
|             "is-grid": this.layout === "grid", | ||||
|             "is-panel": this.layout === "panel", | ||||
|             "has-title": !!this._config.title, | ||||
|           })} | ||||
|           .narrow=${this._narrow} | ||||
|           .events=${this._events} | ||||
| @@ -229,6 +230,7 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { | ||||
|       padding: 0 8px 8px; | ||||
|       box-sizing: border-box; | ||||
|       height: 100%; | ||||
|       overflow: hidden; | ||||
|     } | ||||
|  | ||||
|     .header { | ||||
| @@ -239,15 +241,25 @@ export class HuiCalendarCard extends LitElement implements LovelaceCard { | ||||
|       padding-left: 8px; | ||||
|       padding-inline-start: 8px; | ||||
|       direction: var(--direction); | ||||
|       white-space: nowrap; | ||||
|       text-overflow: ellipsis; | ||||
|     } | ||||
|  | ||||
|     ha-full-calendar { | ||||
|       --calendar-height: 400px; | ||||
|       height: var(--calendar-height); | ||||
|     } | ||||
|  | ||||
|     ha-full-calendar.is-grid, | ||||
|     ha-full-calendar.is-panel { | ||||
|       height: calc(100% - 16px); | ||||
|       --calendar-height: calc(100% - 16px); | ||||
|     } | ||||
|  | ||||
|     ha-full-calendar.is-grid.has-title, | ||||
|     ha-full-calendar.is-panel.has-title { | ||||
|       --calendar-height: calc( | ||||
|         100% - var(--ha-card-header-font-size, var(--ha-font-size-2xl)) - 22px | ||||
|       ); | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import { classMap } from "lit/directives/class-map"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import "../../../components/entity/state-badge"; | ||||
| import "../../../components/ha-card"; | ||||
| import "../../../components/ha-icon"; | ||||
| @@ -19,6 +18,7 @@ import type { | ||||
| import { SENSOR_DEVICE_CLASS_TIMESTAMP } from "../../../data/sensor"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { handleAction } from "../common/handle-action"; | ||||
| import { hasAction, hasAnyAction } from "../common/has-action"; | ||||
| @@ -252,7 +252,11 @@ export class HuiGlanceCard extends LitElement implements LovelaceCard { | ||||
|       </div>`; | ||||
|     } | ||||
|  | ||||
|     const name = entityConf.name ?? computeStateName(stateObj); | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass!, | ||||
|       stateObj, | ||||
|       entityConf.name | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <div | ||||
|   | ||||
| @@ -3,25 +3,27 @@ import { LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { isComponentLoaded } from "../../../common/config/is_component_loaded"; | ||||
| import { createSearchParam } from "../../../common/url/search-params"; | ||||
| import "../../../components/chart/state-history-charts"; | ||||
| import "../../../components/ha-alert"; | ||||
| import "../../../components/ha-card"; | ||||
| import "../../../components/ha-icon-next"; | ||||
| import { | ||||
|   computeHistory, | ||||
|   subscribeHistoryStatesTimeWindow, | ||||
|   type HistoryResult, | ||||
|   convertStatisticsToHistory, | ||||
|   mergeHistoryResults, | ||||
|   subscribeHistoryStatesTimeWindow, | ||||
|   type HistoryResult, | ||||
| } from "../../../data/history"; | ||||
| import { fetchStatistics } from "../../../data/recorder"; | ||||
| import { getSensorNumericDeviceClasses } from "../../../data/sensor"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { hasConfigOrEntitiesChanged } from "../common/has-changed"; | ||||
| import { processConfigEntities } from "../common/process-config-entities"; | ||||
| import type { EntityConfig } from "../entity-rows/types"; | ||||
| import type { LovelaceCard, LovelaceGridOptions } from "../types"; | ||||
| import type { HistoryGraphCardConfig } from "./types"; | ||||
| import { createSearchParam } from "../../../common/url/search-params"; | ||||
| import { fetchStatistics } from "../../../data/recorder"; | ||||
|  | ||||
| export const DEFAULT_HOURS_TO_SHOW = 24; | ||||
|  | ||||
| @@ -51,6 +53,8 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { | ||||
|  | ||||
|   private _entityIds: string[] = []; | ||||
|  | ||||
|   private _entities: EntityConfig[] = []; | ||||
|  | ||||
|   private _hoursToShow = DEFAULT_HOURS_TO_SHOW; | ||||
|  | ||||
|   private _interval?: number; | ||||
| @@ -80,21 +84,35 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { | ||||
|       throw new Error("You must include at least one entity"); | ||||
|     } | ||||
|  | ||||
|     const configEntities = config.entities | ||||
|     this._entities = config.entities | ||||
|       ? processConfigEntities(config.entities) | ||||
|       : []; | ||||
|  | ||||
|     this._entityIds = []; | ||||
|     configEntities.forEach((entity) => { | ||||
|       this._entityIds.push(entity.entity); | ||||
|       if (entity.name) { | ||||
|         this._names[entity.entity] = entity.name; | ||||
|       } | ||||
|     }); | ||||
|     this._entityIds = this._entities.map((entity) => entity.entity); | ||||
|  | ||||
|     this._hoursToShow = config.hours_to_show || DEFAULT_HOURS_TO_SHOW; | ||||
|  | ||||
|     this._config = config; | ||||
|     this._computeNames(); | ||||
|   } | ||||
|  | ||||
|   private _computeNames() { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return; | ||||
|     } | ||||
|     this._names = {}; | ||||
|     this._entities.forEach((entity) => { | ||||
|       const stateObj = this.hass!.states[entity.entity]; | ||||
|       this._names[entity.entity] = stateObj | ||||
|         ? computeLovelaceEntityName(this.hass!, stateObj, entity.name) | ||||
|         : entity.entity; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   public willUpdate(changedProps: PropertyValues) { | ||||
|     super.willUpdate(changedProps); | ||||
|     if (changedProps.has("hass")) { | ||||
|       this._computeNames(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   public connectedCallback() { | ||||
| @@ -162,7 +180,7 @@ export class HuiHistoryGraphCard extends LitElement implements LovelaceCard { | ||||
|   private async _fetchStatistics(sensorNumericDeviceClasses: string[]) { | ||||
|     const now = new Date(); | ||||
|     const start = new Date(); | ||||
|     start.setHours(start.getHours() - this._hoursToShow); | ||||
|     start.setHours(start.getHours() - this._hoursToShow - 1); | ||||
|  | ||||
|     const statistics = await fetchStatistics( | ||||
|       this.hass!, | ||||
|   | ||||
| @@ -14,7 +14,11 @@ import { findEntities } from "../common/find-entities"; | ||||
| import { processConfigEntities } from "../common/process-config-entities"; | ||||
| import "../components/hui-warning"; | ||||
| import type { EntityConfig } from "../entity-rows/types"; | ||||
| import type { LovelaceCard, LovelaceCardEditor } from "../types"; | ||||
| import type { | ||||
|   LovelaceCard, | ||||
|   LovelaceCardEditor, | ||||
|   LovelaceGridOptions, | ||||
| } from "../types"; | ||||
| import type { LogbookCardConfig } from "./types"; | ||||
| import { resolveEntityIDs } from "../../../data/selector"; | ||||
| import { ensureArray } from "../../../common/array/ensure-array"; | ||||
| @@ -64,6 +68,15 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { | ||||
|     return 9 + (this._config?.title ? 1 : 0); | ||||
|   } | ||||
|  | ||||
|   public getGridOptions(): LovelaceGridOptions { | ||||
|     return { | ||||
|       rows: 6, | ||||
|       columns: 12, | ||||
|       min_columns: 6, | ||||
|       min_rows: this._config?.title ? 4 : 3, | ||||
|     }; | ||||
|   } | ||||
|  | ||||
|   public validateTarget( | ||||
|     config: LogbookCardConfig | ||||
|   ): HassServiceTarget | undefined { | ||||
| @@ -189,6 +202,10 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { | ||||
|       > | ||||
|         <div class="content"> | ||||
|           <ha-logbook | ||||
|             class=${classMap({ | ||||
|               "is-grid": this.layout === "grid", | ||||
|               "is-panel": this.layout === "panel", | ||||
|             })} | ||||
|             .hass=${this.hass} | ||||
|             .time=${this._time} | ||||
|             .entityIds=${this._getEntityIds()} | ||||
| @@ -212,6 +229,7 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { | ||||
|         } | ||||
|  | ||||
|         .content { | ||||
|           height: 100%; | ||||
|           padding: 0 16px 16px; | ||||
|         } | ||||
|  | ||||
| @@ -224,6 +242,11 @@ export class HuiLogbookCard extends LitElement implements LovelaceCard { | ||||
|           display: block; | ||||
|         } | ||||
|  | ||||
|         ha-logbook.is-grid, | ||||
|         ha-logbook.is-panel { | ||||
|           height: 100%; | ||||
|         } | ||||
|  | ||||
|         :host([ispanel]) .content, | ||||
|         :host([ispanel]) ha-logbook { | ||||
|           height: 100%; | ||||
|   | ||||
| @@ -35,20 +35,12 @@ import { | ||||
|   hasConfigOrEntitiesChanged, | ||||
| } from "../common/has-changed"; | ||||
| import { processConfigEntities } from "../common/process-config-entities"; | ||||
| import type { EntityConfig } from "../entity-rows/types"; | ||||
| import type { LovelaceCard, LovelaceGridOptions } from "../types"; | ||||
| import type { MapCardConfig } from "./types"; | ||||
| import type { MapCardConfig, MapEntityConfig } from "./types"; | ||||
|  | ||||
| export const DEFAULT_HOURS_TO_SHOW = 0; | ||||
| export const DEFAULT_ZOOM = 14; | ||||
|  | ||||
| interface MapEntityConfig extends EntityConfig { | ||||
|   label_mode?: "state" | "attribute" | "name"; | ||||
|   attribute?: string; | ||||
|   unit?: string; | ||||
|   focus?: boolean; | ||||
| } | ||||
|  | ||||
| interface GeoEntity { | ||||
|   entity_id: string; | ||||
|   label_mode?: "state" | "attribute" | "name" | "icon"; | ||||
|   | ||||
| @@ -20,14 +20,15 @@ import { | ||||
| } from "../../../data/recorder"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { computeCardSize } from "../common/compute-card-size"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||
| import { createHeaderFooterElement } from "../create-element/create-header-footer-element"; | ||||
| import type { | ||||
|   LovelaceCard, | ||||
|   LovelaceCardEditor, | ||||
|   LovelaceHeaderFooter, | ||||
|   LovelaceGridOptions, | ||||
|   LovelaceHeaderFooter, | ||||
| } from "../types"; | ||||
| import type { HuiErrorCard } from "./hui-error-card"; | ||||
| import type { EntityCardConfig, StatisticCardConfig } from "./types"; | ||||
| @@ -180,7 +181,9 @@ export class HuiStatisticCard extends LitElement implements LovelaceCard { | ||||
|  | ||||
|     const stateObj = this.hass.states[this._config.entity]; | ||||
|     const name = | ||||
|       this._config.name || | ||||
|       (this._config.name | ||||
|         ? computeLovelaceEntityName(this.hass, stateObj, this._config.name) | ||||
|         : "") || | ||||
|       getStatisticLabel(this.hass, this._config.entity, this._metadata); | ||||
|  | ||||
|     return html` | ||||
|   | ||||
| @@ -1,15 +1,11 @@ | ||||
| import { differenceInDays, subHours } from "date-fns"; | ||||
| import type { HassEntity, UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import { subHours, differenceInDays } from "date-fns"; | ||||
| import type { PropertyValues } from "lit"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import "../../../components/ha-card"; | ||||
| import { getEnergyDataCollection } from "../../../data/energy"; | ||||
| import { | ||||
|   getSuggestedMax, | ||||
|   getSuggestedPeriod, | ||||
| } from "./energy/common/energy-chart-options"; | ||||
| import type { | ||||
|   Statistics, | ||||
|   StatisticsMetaData, | ||||
| @@ -21,10 +17,16 @@ import { | ||||
|   getStatisticMetadata, | ||||
| } from "../../../data/recorder"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { hasConfigOrEntitiesChanged } from "../common/has-changed"; | ||||
| import { processConfigEntities } from "../common/process-config-entities"; | ||||
| import type { EntityConfig } from "../entity-rows/types"; | ||||
| import type { LovelaceCard, LovelaceGridOptions } from "../types"; | ||||
| import { | ||||
|   getSuggestedMax, | ||||
|   getSuggestedPeriod, | ||||
| } from "./energy/common/energy-chart-options"; | ||||
| import type { StatisticsGraphCardConfig } from "./types"; | ||||
|  | ||||
| export const DEFAULT_DAYS_TO_SHOW = 30; | ||||
| @@ -67,7 +69,9 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | ||||
|  | ||||
|   @state() private _unit?: string; | ||||
|  | ||||
|   private _entities: string[] = []; | ||||
|   private _entities: EntityConfig[] = []; | ||||
|  | ||||
|   private _entityIds: string[] = []; | ||||
|  | ||||
|   private _names: Record<string, string> = {}; | ||||
|  | ||||
| @@ -148,17 +152,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | ||||
|       throw new Error("You must include at least one entity"); | ||||
|     } | ||||
|  | ||||
|     const configEntities = config.entities | ||||
|     this._entities = config.entities | ||||
|       ? processConfigEntities(config.entities, false) | ||||
|       : []; | ||||
|  | ||||
|     this._entities = []; | ||||
|     configEntities.forEach((entity) => { | ||||
|       this._entities.push(entity.entity); | ||||
|       if (entity.name) { | ||||
|         this._names[entity.entity] = entity.name; | ||||
|       } | ||||
|     }); | ||||
|     this._entityIds = this._entities.map((ent) => ent.entity); | ||||
|  | ||||
|     if (typeof config.stat_types === "string") { | ||||
|       this._statTypes = [config.stat_types]; | ||||
| @@ -168,6 +165,20 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | ||||
|       this._statTypes = config.stat_types; | ||||
|     } | ||||
|     this._config = config; | ||||
|     this._computeNames(); | ||||
|   } | ||||
|  | ||||
|   private _computeNames() { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return; | ||||
|     } | ||||
|     this._names = {}; | ||||
|     this._entities.forEach((config) => { | ||||
|       const stateObj = this.hass!.states[config.entity]; | ||||
|       this._names[config.entity] = stateObj | ||||
|         ? computeLovelaceEntityName(this.hass!, stateObj, config.name) | ||||
|         : config.entity; | ||||
|     }); | ||||
|   } | ||||
|  | ||||
|   protected shouldUpdate(changedProps: PropertyValues): boolean { | ||||
| @@ -209,6 +220,10 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (changedProps.has("hass")) { | ||||
|       this._computeNames(); | ||||
|     } | ||||
|  | ||||
|     if ( | ||||
|       changedProps.has("_config") && | ||||
|       oldConfig?.entities !== this._config.entities | ||||
| @@ -232,7 +247,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | ||||
|     clearInterval(this._interval); | ||||
|     this._interval = 0; // block concurrent calls | ||||
|     if (fetchMetadata) { | ||||
|       await this._getStatisticsMetaData(this._entities); | ||||
|       await this._getStatisticsMetaData(this._entityIds); | ||||
|     } | ||||
|     await this._getStatistics(); | ||||
|     // statistics are created every hour | ||||
| @@ -344,7 +359,7 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | ||||
|         } | ||||
|       } | ||||
|       if (!unitClass && this._metadata) { | ||||
|         const metadata = this._metadata[this._entities[0]]; | ||||
|         const metadata = this._metadata[this._entityIds[0]]; | ||||
|         unitClass = metadata?.unit_class; | ||||
|         this._unit = unitClass | ||||
|           ? getDisplayUnit(this.hass!, metadata.statistic_id, metadata) || | ||||
| @@ -356,14 +371,15 @@ export class HuiStatisticsGraphCard extends LitElement implements LovelaceCard { | ||||
|         this.hass!, | ||||
|         startDate, | ||||
|         endDate, | ||||
|         this._entities, | ||||
|         this._entityIds, | ||||
|         this._period, | ||||
|         unitconfig, | ||||
|         this._statTypes | ||||
|       ); | ||||
|  | ||||
|       this._statistics = {}; | ||||
|       this._entities.forEach((id) => { | ||||
|       this._entities.forEach((entity) => { | ||||
|         const id = entity.entity; | ||||
|         if (id in statistics) { | ||||
|           this._statistics![id] = statistics[id]; | ||||
|         } | ||||
|   | ||||
| @@ -563,7 +563,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { | ||||
|         .name, | ||||
|         .attribute { | ||||
|           font-size: var(--ha-font-size-m); | ||||
|           line-height: 1; | ||||
|           line-height: var(--ha-line-height-condensed); | ||||
|         } | ||||
|  | ||||
|         .name-state { | ||||
| @@ -729,7 +729,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { | ||||
|         .short .state, | ||||
|         .short .temp-attribute .temp { | ||||
|           font-size: 24px; | ||||
|           line-height: 1.25; | ||||
|           line-height: var(--ha-line-height-condensed); | ||||
|         } | ||||
|  | ||||
|         .short .content + .forecast { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import type { EnergySourceByType } from "../../../data/energy"; | ||||
| import type { ActionConfig } from "../../../data/lovelace/config/action"; | ||||
| import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import type { Statistic, StatisticType } from "../../../data/recorder"; | ||||
| import type { MediaSelectorValue } from "../../../data/selector"; | ||||
| import type { TimeFormat } from "../../../data/translation"; | ||||
| import type { ForecastType } from "../../../data/weather"; | ||||
| import type { | ||||
| @@ -29,7 +30,6 @@ import type { | ||||
| import type { LovelaceHeaderFooterConfig } from "../header-footer/types"; | ||||
| import type { LovelaceHeadingBadgeConfig } from "../heading-badges/types"; | ||||
| import type { HomeSummary } from "../strategies/home/helpers/home-summaries"; | ||||
| import type { MediaSelectorValue } from "../../../data/selector"; | ||||
|  | ||||
| export type AlarmPanelCardConfigState = | ||||
|   | "arm_away" | ||||
| @@ -347,7 +347,15 @@ export interface LogbookCardConfig extends LovelaceCardConfig { | ||||
|   theme?: string; | ||||
| } | ||||
|  | ||||
| interface GeoLocationSourceConfig { | ||||
| export interface MapEntityConfig extends EntityConfig { | ||||
|   label_mode?: "state" | "attribute" | "name"; | ||||
|   attribute?: string; | ||||
|   unit?: string; | ||||
|   focus?: boolean; | ||||
|   name?: string; | ||||
| } | ||||
|  | ||||
| export interface GeoLocationSourceConfig { | ||||
|   source: string; | ||||
|   label_mode?: "name" | "state" | "attribute" | "icon"; | ||||
|   attribute?: string; | ||||
| @@ -362,7 +370,7 @@ export interface MapCardConfig extends LovelaceCardConfig { | ||||
|   auto_fit?: boolean; | ||||
|   fit_zones?: boolean; | ||||
|   default_zoom?: number; | ||||
|   entities?: (EntityConfig | string)[]; | ||||
|   entities?: (MapEntityConfig | string)[]; | ||||
|   hours_to_show?: number; | ||||
|   geo_location_sources?: (GeoLocationSourceConfig | string)[]; | ||||
|   dark_mode?: boolean; | ||||
| @@ -434,7 +442,7 @@ export interface StatisticsGraphCardConfig extends EnergyCardBaseConfig { | ||||
| } | ||||
|  | ||||
| export interface StatisticCardConfig extends LovelaceCardConfig { | ||||
|   name?: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   entities: (EntityConfig | string)[]; | ||||
|   period: | ||||
|     | { | ||||
|   | ||||
| @@ -1,28 +1,29 @@ | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { | ||||
|   DEFAULT_ENTITY_NAME, | ||||
|   type EntityNameItem, | ||||
| } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { ensureArray } from "../../../../common/array/ensure-array"; | ||||
| import type { EntityNameItem } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import { computeStateName } from "../../../../common/entity/compute_state_name"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
|  | ||||
| /** | ||||
|  * Computes the display name for an entity in Lovelace (cards and badges). | ||||
|  * | ||||
|  * @param hass - The Home Assistant instance | ||||
|  * @param stateObj - The entity state object | ||||
|  * @param nameConfig - The name configuration (string for override, or EntityNameItem[] for structured naming) | ||||
|  * @param config - The name configuration (string for override, or EntityNameItem[] for structured naming) | ||||
|  * @returns The computed entity name | ||||
|  */ | ||||
| export const computeLovelaceEntityName = ( | ||||
|   hass: HomeAssistant, | ||||
|   stateObj: HassEntity | undefined, | ||||
|   nameConfig: string | EntityNameItem | EntityNameItem[] | undefined | ||||
|   config: string | EntityNameItem | EntityNameItem[] | undefined | ||||
| ): string => { | ||||
|   if (typeof nameConfig === "string") { | ||||
|     return nameConfig; | ||||
|   // If no config is provided, fall back to the default state name | ||||
|   if (!config) { | ||||
|     return stateObj ? computeStateName(stateObj) : ""; | ||||
|   } | ||||
|   if (typeof config === "string") { | ||||
|     return config; | ||||
|   } | ||||
|   const config = nameConfig || DEFAULT_ENTITY_NAME; | ||||
|   if (stateObj) { | ||||
|     return hass.formatEntityName(stateObj, config); | ||||
|   } | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import type { HassEntities, HassEntity } from "home-assistant-js-websocket"; | ||||
| import { SENSOR_ENTITIES, ASSIST_ENTITIES } from "../../../common/const"; | ||||
| import { ASSIST_ENTITIES, SENSOR_ENTITIES } from "../../../common/const"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| @@ -14,12 +14,14 @@ import type { | ||||
|   GridSourceTypeEnergyPreference, | ||||
| } from "../../../data/energy"; | ||||
| import { domainToName } from "../../../data/integration"; | ||||
| import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; | ||||
| import type { LovelaceCardConfig } from "../../../data/lovelace/config/card"; | ||||
| import type { LovelaceSectionConfig } from "../../../data/lovelace/config/section"; | ||||
| import type { LovelaceViewConfig } from "../../../data/lovelace/config/view"; | ||||
| import { computeUserInitials } from "../../../data/user"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { HELPER_DOMAINS } from "../../config/helpers/const"; | ||||
| import type { EntityBadgeConfig } from "../badges/types"; | ||||
| import type { | ||||
|   AlarmPanelCardConfig, | ||||
|   EntitiesCardConfig, | ||||
| @@ -31,8 +33,7 @@ import type { | ||||
| } from "../cards/types"; | ||||
| import type { EntityConfig } from "../entity-rows/types"; | ||||
| import type { ButtonsHeaderFooterConfig } from "../header-footer/types"; | ||||
| import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; | ||||
| import type { EntityBadgeConfig } from "../badges/types"; | ||||
| import { computeLovelaceEntityName } from "./entity/compute-lovelace-entity-name"; | ||||
|  | ||||
| const HIDE_DOMAIN = new Set([ | ||||
|   "ai_task", | ||||
| @@ -125,13 +126,13 @@ export const computeSection = ( | ||||
| }); | ||||
|  | ||||
| export const computeCards = ( | ||||
|   states: HassEntities, | ||||
|   hass: HomeAssistant, | ||||
|   entityIds: string[], | ||||
|   entityCardOptions: Partial<EntitiesCardConfig>, | ||||
|   renderFooterEntities = true | ||||
| ): LovelaceCardConfig[] => { | ||||
|   const cards: LovelaceCardConfig[] = []; | ||||
|  | ||||
|   const states = hass.states; | ||||
|   // For entity card | ||||
|   const entitiesConf: (string | EntityConfig)[] = []; | ||||
|  | ||||
| @@ -270,19 +271,23 @@ export const computeCards = ( | ||||
|         ? states[a] | ||||
|           ? computeStateName(states[a]) | ||||
|           : "" | ||||
|         : a.name || "", | ||||
|         : states[a.entity] | ||||
|           ? computeLovelaceEntityName(hass, states[a.entity], a.name) | ||||
|           : "", | ||||
|       typeof b === "string" | ||||
|         ? states[b] | ||||
|           ? computeStateName(states[b]) | ||||
|           : "" | ||||
|         : b.name || "" | ||||
|         : states[b.entity] | ||||
|           ? computeLovelaceEntityName(hass, states[b.entity], b.name) | ||||
|           : "" | ||||
|     ); | ||||
|   }); | ||||
|  | ||||
|   // If we ended up with footer entities but no normal entities, | ||||
|   // render the footer entities as normal entities. | ||||
|   if (entitiesConf.length === 0 && footerEntities.length > 0) { | ||||
|     return computeCards(states, entityIds, entityCardOptions, false); | ||||
|     return computeCards(hass, entityIds, entityCardOptions, false); | ||||
|   } | ||||
|  | ||||
|   if (entitiesConf.length > 0 || footerEntities.length > 0) { | ||||
| @@ -360,14 +365,14 @@ const computeDefaultViewStates = ( | ||||
| }; | ||||
|  | ||||
| export const generateViewConfig = ( | ||||
|   localize: LocalizeFunc, | ||||
|   hass: HomeAssistant, | ||||
|   path: string, | ||||
|   title: string | undefined, | ||||
|   icon: string | undefined, | ||||
|   entities: HassEntities | ||||
| ): LovelaceViewConfig => { | ||||
|   const ungroupedEntitites: Record<string, string[]> = {}; | ||||
|  | ||||
|   const { localize } = hass; | ||||
|   // Organize ungrouped entities in ungrouped things | ||||
|   for (const entityId of Object.keys(entities)) { | ||||
|     const state = entities[entityId]; | ||||
| @@ -470,7 +475,7 @@ export const generateViewConfig = ( | ||||
|     .forEach((domain) => { | ||||
|       cards.push( | ||||
|         ...computeCards( | ||||
|           entities, | ||||
|           hass, | ||||
|           ungroupedEntitites[domain].sort((a, b) => | ||||
|             stringCompare( | ||||
|               computeStateName(entities[a]), | ||||
| @@ -498,16 +503,17 @@ export const generateViewConfig = ( | ||||
| }; | ||||
|  | ||||
| export const generateDefaultViewConfig = ( | ||||
|   areaEntries: HomeAssistant["areas"], | ||||
|   deviceEntries: HomeAssistant["devices"], | ||||
|   entityEntries: HomeAssistant["entities"], | ||||
|   entities: HassEntities, | ||||
|   hass: HomeAssistant, | ||||
|   localize: LocalizeFunc, | ||||
|   energyPrefs?: EnergyPreferences, | ||||
|   areasPrefs?: AreasDisplayValue, | ||||
|   hideEntitiesWithoutAreas?: boolean, | ||||
|   hideEnergy?: boolean | ||||
| ): LovelaceViewConfig => { | ||||
|   const entities = hass.states; | ||||
|   const areaEntries = hass.areas; | ||||
|   const deviceEntries = hass.devices; | ||||
|   const entityEntries = hass.entities; | ||||
|   const states = computeDefaultViewStates(entities, entityEntries); | ||||
|   const path = "default_view"; | ||||
|   const title = "Home"; | ||||
| @@ -549,7 +555,7 @@ export const generateDefaultViewConfig = ( | ||||
|  | ||||
|   for (const groupEntity of splittedByGroups.groups) { | ||||
|     groupCards.push( | ||||
|       ...computeCards(entities, groupEntity.attributes.entity_id, { | ||||
|       ...computeCards(hass, groupEntity.attributes.entity_id, { | ||||
|         title: computeStateName(groupEntity), | ||||
|         show_header_toggle: groupEntity.attributes.control !== "hidden", | ||||
|       }) | ||||
| @@ -557,7 +563,7 @@ export const generateDefaultViewConfig = ( | ||||
|   } | ||||
|  | ||||
|   const config = generateViewConfig( | ||||
|     localize, | ||||
|     hass, | ||||
|     path, | ||||
|     title, | ||||
|     icon, | ||||
| @@ -575,7 +581,7 @@ export const generateDefaultViewConfig = ( | ||||
|     const area = areaEntries[areaId]; | ||||
|     areaCards.push( | ||||
|       ...computeCards( | ||||
|         entities, | ||||
|         hass, | ||||
|         areaEntities.map((entity) => entity.entity_id), | ||||
|         { | ||||
|           title: area.name, | ||||
| @@ -601,7 +607,7 @@ export const generateDefaultViewConfig = ( | ||||
|     const device = deviceEntries[deviceId]; | ||||
|     deviceCards.push( | ||||
|       ...computeCards( | ||||
|         entities, | ||||
|         hass, | ||||
|         deviceEntities.map((entity) => entity.entity_id), | ||||
|         { | ||||
|           title: | ||||
|   | ||||
| @@ -2,8 +2,12 @@ | ||||
| import { isValidEntityId } from "../../../common/entity/valid_entity_id"; | ||||
| import type { EntityConfig, LovelaceRowConfig } from "../entity-rows/types"; | ||||
|  | ||||
| interface BaseEntityConfig { | ||||
|   type: string; | ||||
|   entity: string; | ||||
| } | ||||
| export const processConfigEntities = < | ||||
|   T extends EntityConfig | LovelaceRowConfig, | ||||
|   T extends BaseEntityConfig | LovelaceRowConfig, | ||||
| >( | ||||
|   entities: (T | string)[], | ||||
|   checkEntityId = true | ||||
|   | ||||
| @@ -45,14 +45,13 @@ export class HuiEntityEditor extends LitElement { | ||||
|       this.hass.devices | ||||
|     ); | ||||
|  | ||||
|     const name = this.hass.formatEntityName( | ||||
|       stateObj, | ||||
|       useDeviceName ? { type: "device" } : { type: "entity" } | ||||
|     ); | ||||
|  | ||||
|     const isRTL = computeRTL(this.hass); | ||||
|  | ||||
|     const primary = item.name || name || item.entity; | ||||
|     const primary = | ||||
|       this.hass.formatEntityName( | ||||
|         stateObj, | ||||
|         useDeviceName ? { type: "device" } : { type: "entity" } | ||||
|       ) || item.entity; | ||||
|  | ||||
|     const secondary = this.hass.formatEntityName( | ||||
|       stateObj, | ||||
|   | ||||
| @@ -4,19 +4,19 @@ import { customElement, property } from "lit/decorators"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import { DOMAINS_INPUT_ROW } from "../../../common/const"; | ||||
| import { stopPropagation } from "../../../common/dom/stop_propagation"; | ||||
| import { toggleAttribute } from "../../../common/dom/toggle_attribute"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import "../../../components/entity/state-badge"; | ||||
| import "../../../components/ha-relative-time"; | ||||
| import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import type { EntitiesCardEntityConfig } from "../cards/types"; | ||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { handleAction } from "../common/handle-action"; | ||||
| import { hasAction, hasAnyAction } from "../common/has-action"; | ||||
| import { createEntityNotFoundWarning } from "./hui-warning"; | ||||
| import { stopPropagation } from "../../../common/dom/stop_propagation"; | ||||
|  | ||||
| @customElement("hui-generic-entity-row") | ||||
| export class HuiGenericEntityRow extends LitElement { | ||||
| @@ -59,7 +59,11 @@ export class HuiGenericEntityRow extends LitElement { | ||||
|     const pointer = hasAnyAction(this.config); | ||||
|  | ||||
|     const hasSecondary = this.secondaryText || this.config.secondary_info; | ||||
|     const name = this.config.name ?? computeStateName(stateObj); | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this.config.name | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <div | ||||
| @@ -87,7 +91,7 @@ export class HuiGenericEntityRow extends LitElement { | ||||
|               class="info ${classMap({ "text-content": !hasSecondary })}" | ||||
|               .title=${name} | ||||
|             > | ||||
|               ${this.config.name || computeStateName(stateObj)} | ||||
|               ${name} | ||||
|               ${hasSecondary | ||||
|                 ? html` | ||||
|                     <div class="secondary"> | ||||
|   | ||||
| @@ -23,6 +23,7 @@ import "../card-features/hui-light-color-temp-card-feature"; | ||||
| import "../card-features/hui-lock-commands-card-feature"; | ||||
| import "../card-features/hui-lock-open-door-card-feature"; | ||||
| import "../card-features/hui-media-player-playback-card-feature"; | ||||
| import "../card-features/hui-media-player-volume-buttons-card-feature"; | ||||
| import "../card-features/hui-media-player-volume-slider-card-feature"; | ||||
| import "../card-features/hui-numeric-input-card-feature"; | ||||
| import "../card-features/hui-select-options-card-feature"; | ||||
| @@ -72,6 +73,7 @@ const TYPES = new Set<LovelaceCardFeatureConfig["type"]>([ | ||||
|   "lock-commands", | ||||
|   "lock-open-door", | ||||
|   "media-player-playback", | ||||
|   "media-player-volume-buttons", | ||||
|   "media-player-volume-slider", | ||||
|   "numeric-input", | ||||
|   "select-options", | ||||
|   | ||||
| @@ -296,11 +296,7 @@ export class HuiCreateDialogCard | ||||
|   } | ||||
|  | ||||
|   private _suggestCards(): void { | ||||
|     const cardConfig = computeCards( | ||||
|       this.hass.states, | ||||
|       this._selectedEntities, | ||||
|       {} | ||||
|     ); | ||||
|     const cardConfig = computeCards(this.hass, this._selectedEntities, {}); | ||||
|  | ||||
|     let sectionOptions: Partial<LovelaceSectionConfig> = {}; | ||||
|  | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { array, assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import { supportsFeature } from "../../../../common/entity/supports-feature"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| @@ -65,9 +64,7 @@ export class HuiAlarmPanelCardEditor | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|             entity_name: {}, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|   | ||||
| @@ -5,7 +5,6 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { assert, assign, boolean, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { | ||||
|   HaFormSchema, | ||||
| @@ -73,7 +72,7 @@ export class HuiButtonCardEditor | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { default_name: DEFAULT_ENTITY_NAME }, | ||||
|             entity_name: {}, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|   | ||||
| @@ -48,6 +48,7 @@ import { supportsLightColorTempCardFeature } from "../../card-features/hui-light | ||||
| import { supportsLockCommandsCardFeature } from "../../card-features/hui-lock-commands-card-feature"; | ||||
| import { supportsLockOpenDoorCardFeature } from "../../card-features/hui-lock-open-door-card-feature"; | ||||
| import { supportsMediaPlayerPlaybackCardFeature } from "../../card-features/hui-media-player-playback-card-feature"; | ||||
| import { supportsMediaPlayerVolumeButtonsCardFeature } from "../../card-features/hui-media-player-volume-buttons-card-feature"; | ||||
| import { supportsMediaPlayerVolumeSliderCardFeature } from "../../card-features/hui-media-player-volume-slider-card-feature"; | ||||
| import { supportsNumericInputCardFeature } from "../../card-features/hui-numeric-input-card-feature"; | ||||
| import { supportsSelectOptionsCardFeature } from "../../card-features/hui-select-options-card-feature"; | ||||
| @@ -102,6 +103,7 @@ const UI_FEATURE_TYPES = [ | ||||
|   "lock-commands", | ||||
|   "lock-open-door", | ||||
|   "media-player-playback", | ||||
|   "media-player-volume-buttons", | ||||
|   "media-player-volume-slider", | ||||
|   "numeric-input", | ||||
|   "select-options", | ||||
| @@ -131,6 +133,7 @@ const EDITABLES_FEATURE_TYPES = new Set<UiFeatureTypes>([ | ||||
|   "fan-preset-modes", | ||||
|   "humidifier-modes", | ||||
|   "lawn-mower-commands", | ||||
|   "media-player-volume-buttons", | ||||
|   "numeric-input", | ||||
|   "select-options", | ||||
|   "trend-graph", | ||||
| @@ -171,6 +174,7 @@ const SUPPORTS_FEATURE_TYPES: Record< | ||||
|   "lock-commands": supportsLockCommandsCardFeature, | ||||
|   "lock-open-door": supportsLockOpenDoorCardFeature, | ||||
|   "media-player-playback": supportsMediaPlayerPlaybackCardFeature, | ||||
|   "media-player-volume-buttons": supportsMediaPlayerVolumeButtonsCardFeature, | ||||
|   "media-player-volume-slider": supportsMediaPlayerVolumeSliderCardFeature, | ||||
|   "numeric-input": supportsNumericInputCardFeature, | ||||
|   "select-options": supportsSelectOptionsCardFeature, | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import { | ||||
|   string, | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| @@ -86,9 +85,7 @@ export class HuiEntityBadgeEditor | ||||
|             { | ||||
|               name: "name", | ||||
|               selector: { | ||||
|                 entity_name: { | ||||
|                   default_name: DEFAULT_ENTITY_NAME, | ||||
|                 }, | ||||
|                 entity_name: {}, | ||||
|               }, | ||||
|               context: { entity: "entity" }, | ||||
|             }, | ||||
|   | ||||
| @@ -1,5 +1,4 @@ | ||||
| import { assert, assign, boolean, object, optional, string } from "superstruct"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import { headerFooterConfigStructs } from "../../header-footer/structs"; | ||||
| @@ -26,9 +25,7 @@ const SCHEMA = [ | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|       entity_name: {}, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   | ||||
| @@ -14,7 +14,6 @@ import { | ||||
|   string, | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity_attributes"; | ||||
| @@ -102,9 +101,7 @@ export class HuiGaugeCardEditor | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|             entity_name: {}, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|   | ||||
| @@ -46,10 +46,10 @@ export class HuiGenericEntityRowEditor | ||||
|     return [ | ||||
|       { name: "entity", required: true, selector: { entity: {} } }, | ||||
|       { | ||||
|         type: "grid", | ||||
|         name: "", | ||||
|         schema: [ | ||||
|           { name: "name", selector: { text: {} } }, | ||||
|         name: "name", | ||||
|         selector: { entity_name: {} }, | ||||
|         context: { entity: "entity" }, | ||||
|       }, | ||||
|       { | ||||
|         name: "icon", | ||||
|         selector: { | ||||
| @@ -59,8 +59,6 @@ export class HuiGenericEntityRowEditor | ||||
|           icon_entity: "entity", | ||||
|         }, | ||||
|       }, | ||||
|         ], | ||||
|       }, | ||||
|       { | ||||
|         name: "secondary_info", | ||||
|         selector: { | ||||
|   | ||||
| @@ -11,20 +11,20 @@ import { | ||||
|   string, | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import "../hui-sub-element-editor"; | ||||
| import type { EditDetailElementEvent, SubElementEditorConfig } from "../types"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { ConfigEntity, GlanceCardConfig } from "../../cards/types"; | ||||
| import "../../components/hui-entity-editor"; | ||||
| import type { EntityConfig } from "../../entity-rows/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import "../hui-sub-element-editor"; | ||||
| import { processEditorEntities } from "../process-editor-entities"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entitiesConfigStruct } from "../structs/entities-struct"; | ||||
| import type { EntityConfig } from "../../entity-rows/types"; | ||||
| import type { EditDetailElementEvent, SubElementEditorConfig } from "../types"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -42,11 +42,17 @@ const cardConfigStruct = assign( | ||||
|  | ||||
| const SUB_SCHEMA = [ | ||||
|   { name: "entity", selector: { entity: {} }, required: true }, | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { entity_name: {} }, | ||||
|     context: { | ||||
|       entity: "entity", | ||||
|     }, | ||||
|   }, | ||||
|   { | ||||
|     type: "grid", | ||||
|     name: "", | ||||
|     schema: [ | ||||
|       { name: "name", selector: { text: {} } }, | ||||
|       { | ||||
|         name: "icon", | ||||
|         selector: { | ||||
|   | ||||
| @@ -45,7 +45,13 @@ const cardConfigStruct = assign( | ||||
|  | ||||
| const SUB_SCHEMA = [ | ||||
|   { name: "entity", selector: { entity: {} }, required: true }, | ||||
|   { name: "name", selector: { text: {} } }, | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { entity_name: {} }, | ||||
|     context: { | ||||
|       entity: "entity", | ||||
|     }, | ||||
|   }, | ||||
| ] as const; | ||||
|  | ||||
| @customElement("hui-history-graph-card-editor") | ||||
|   | ||||
| @@ -14,7 +14,6 @@ import { | ||||
| } from "superstruct"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-expansion-panel"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { | ||||
| @@ -61,9 +60,7 @@ const SCHEMA = [ | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|       entity_name: {}, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   | ||||
| @@ -1,10 +1,9 @@ | ||||
| import { mdiGestureTap } from "@mdi/js"; | ||||
| import type { CSSResultGroup } from "lit"; | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { mdiGestureTap } from "@mdi/js"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| @@ -37,9 +36,7 @@ const SCHEMA = [ | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|       entity_name: {}, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   | ||||
| @@ -2,6 +2,7 @@ import { mdiPalette } from "@mdi/js"; | ||||
| import type { CSSResultGroup } from "lit"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { | ||||
|   array, | ||||
|   assert, | ||||
| @@ -13,19 +14,19 @@ import { | ||||
|   string, | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { hasLocation } from "../../../../common/entity/has_location"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
| import { hasLocation } from "../../../../common/entity/has_location"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import type { SelectSelector } from "../../../../data/selector"; | ||||
| import "../../../../components/ha-formfield"; | ||||
| import "../../../../components/ha-switch"; | ||||
| import "../../../../components/ha-selector/ha-selector-select"; | ||||
| import "../../../../components/ha-switch"; | ||||
| import type { SelectSelector } from "../../../../data/selector"; | ||||
| import type { HomeAssistant, ValueChangedEvent } from "../../../../types"; | ||||
| import { DEFAULT_HOURS_TO_SHOW, DEFAULT_ZOOM } from "../../cards/hui-map-card"; | ||||
| import type { MapCardConfig } from "../../cards/types"; | ||||
| import type { MapCardConfig, MapEntityConfig } from "../../cards/types"; | ||||
| import "../../components/hui-entity-editor"; | ||||
| import type { EntityConfig } from "../../entity-rows/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| @@ -33,7 +34,6 @@ import { processEditorEntities } from "../process-editor-entities"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import type { EntitiesEditorEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
|  | ||||
| export const mapEntitiesConfigStruct = union([ | ||||
|   object({ | ||||
| @@ -223,7 +223,9 @@ export class HuiMapCardEditor extends LitElement implements LovelaceCardEditor { | ||||
|     }) | ||||
|   ); | ||||
|  | ||||
|   private _entitiesValueChanged(ev: EntitiesEditorEvent): void { | ||||
|   private _entitiesValueChanged( | ||||
|     ev: EntitiesEditorEvent<MapEntityConfig> | ||||
|   ): void { | ||||
|     if (ev.detail && ev.detail.entities) { | ||||
|       this._config = { ...this._config!, entities: ev.detail.entities }; | ||||
|  | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { | ||||
|   HaFormSchema, | ||||
| @@ -33,9 +32,7 @@ const SCHEMA = [ | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|       entity_name: {}, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   | ||||
| @@ -0,0 +1,86 @@ | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { | ||||
|   LovelaceCardFeatureContext, | ||||
|   MediaPlayerVolumeButtonsCardFeatureConfig, | ||||
| } from "../../card-features/types"; | ||||
| import type { LovelaceCardFeatureEditor } from "../../types"; | ||||
|  | ||||
| @customElement("hui-media-player-volume-buttons-card-feature-editor") | ||||
| export class HuiMediaPlayerVolumeButtonsCardFeatureEditor | ||||
|   extends LitElement | ||||
|   implements LovelaceCardFeatureEditor | ||||
| { | ||||
|   @property({ attribute: false }) public hass?: HomeAssistant; | ||||
|  | ||||
|   @property({ attribute: false }) public context?: LovelaceCardFeatureContext; | ||||
|  | ||||
|   @state() private _config?: MediaPlayerVolumeButtonsCardFeatureConfig; | ||||
|  | ||||
|   public setConfig(config: MediaPlayerVolumeButtonsCardFeatureConfig): void { | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   private _schema = memoizeOne( | ||||
|     () => | ||||
|       [ | ||||
|         { | ||||
|           name: "step", | ||||
|           selector: { | ||||
|             number: { | ||||
|               mode: "slider", | ||||
|               step: 1, | ||||
|               min: 1, | ||||
|               max: 100, | ||||
|               unit_of_measurement: "%", | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|       ] as const | ||||
|   ); | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     const data: MediaPlayerVolumeButtonsCardFeatureConfig = { | ||||
|       type: "media-player-volume-buttons", | ||||
|       step: this._config.step ?? 5, | ||||
|     }; | ||||
|  | ||||
|     const schema = this._schema(); | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${data} | ||||
|         .schema=${schema} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _computeLabelCallback = ( | ||||
|     schema: SchemaUnion<ReturnType<typeof this._schema>> | ||||
|   ) => | ||||
|     this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.features.types.media-player-volume-buttons.${schema.name}` | ||||
|     ); | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   interface HTMLElementTagNameMap { | ||||
|     "hui-media-player-volume-buttons-card-feature-editor": HuiMediaPlayerVolumeButtonsCardFeatureEditor; | ||||
|   } | ||||
| } | ||||
| @@ -1,8 +1,8 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { mdiGestureTap } from "@mdi/js"; | ||||
| import type { CSSResultGroup } from "lit"; | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { | ||||
|   assert, | ||||
|   assign, | ||||
| @@ -15,7 +15,6 @@ import { | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { | ||||
| @@ -71,9 +70,7 @@ export class HuiPictureEntityCardEditor | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|             entity_name: {}, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| @@ -25,9 +24,7 @@ const SCHEMA = [ | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|       entity_name: {}, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   | ||||
| @@ -1,7 +1,7 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import type { CSSResultGroup } from "lit"; | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { | ||||
|   assert, | ||||
|   assign, | ||||
| @@ -12,18 +12,17 @@ import { | ||||
|   string, | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card"; | ||||
| import type { SensorCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-sensor-card"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
| @@ -71,9 +70,7 @@ export class HuiSensorCardEditor | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|             entity_name: {}, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|   | ||||
| @@ -21,12 +21,13 @@ import type { StatisticCardConfig } from "../../cards/types"; | ||||
| import { headerFooterConfigStructs } from "../../header-footer/structs"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     entity: optional(string()), | ||||
|     name: optional(string()), | ||||
|     name: optional(entityNameStruct), | ||||
|     icon: optional(string()), | ||||
|     unit: optional(string()), | ||||
|     stat_type: optional(string()), | ||||
| @@ -144,11 +145,15 @@ export class HuiStatisticCardEditor | ||||
|                 } | ||||
|               : { object: {} }, | ||||
|         }, | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { entity_name: {} }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|         { | ||||
|           type: "grid", | ||||
|           name: "", | ||||
|           schema: [ | ||||
|             { name: "name", selector: { text: {} } }, | ||||
|             { | ||||
|               name: "icon", | ||||
|               selector: { | ||||
|   | ||||
| @@ -14,7 +14,7 @@ import { | ||||
| } from "superstruct"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
| import "../../../../components/ha-expansion-panel"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { | ||||
| @@ -35,7 +35,6 @@ import type { EditDetailElementEvent, EditSubElementEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import "./hui-card-features-editor"; | ||||
| import type { FeatureType } from "./hui-card-features-editor"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
|  | ||||
| const COMPATIBLE_FEATURES_TYPES: Record<string, FeatureType[]> = { | ||||
|   climate: [ | ||||
| @@ -89,9 +88,7 @@ export class HuiThermostatCardEditor | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|             entity_name: {}, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|   | ||||
| @@ -16,7 +16,6 @@ import { | ||||
| } from "superstruct"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import { orderProperties } from "../../../../common/util/order-properties"; | ||||
| import "../../../../components/ha-expansion-panel"; | ||||
| @@ -102,9 +101,7 @@ export class HuiTileCardEditor | ||||
|             { | ||||
|               name: "name", | ||||
|               selector: { | ||||
|                 entity_name: { | ||||
|                   default_name: DEFAULT_ENTITY_NAME, | ||||
|                 }, | ||||
|                 entity_name: {}, | ||||
|               }, | ||||
|               context: { entity: "entity" }, | ||||
|             }, | ||||
|   | ||||
| @@ -12,7 +12,6 @@ import { | ||||
|   string, | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import { supportsFeature } from "../../../../common/entity/supports-feature"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| @@ -153,9 +152,7 @@ export class HuiWeatherForecastCardEditor | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|             entity_name: {}, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import { | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-expansion-panel"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| @@ -94,9 +93,7 @@ export class HuiHeadingEntityEditor | ||||
|                 { | ||||
|                   name: "name", | ||||
|                   selector: { | ||||
|                     entity_name: { | ||||
|                       default_name: DEFAULT_ENTITY_NAME, | ||||
|                     }, | ||||
|                     entity_name: {}, | ||||
|                   }, | ||||
|                   context: { entity: "entity" }, | ||||
|                 }, | ||||
|   | ||||
| @@ -4,11 +4,12 @@ import { | ||||
|   actionConfigStruct, | ||||
|   actionConfigStructConfirmation, | ||||
| } from "./action-struct"; | ||||
| import { entityNameStruct } from "./entity-name-struct"; | ||||
|  | ||||
| export const entitiesConfigStruct = union([ | ||||
|   object({ | ||||
|     entity: string(), | ||||
|     name: optional(string()), | ||||
|     name: optional(entityNameStruct), | ||||
|     icon: optional(string()), | ||||
|     image: optional(string()), | ||||
|     secondary_info: optional(string()), | ||||
|   | ||||
| @@ -43,9 +43,10 @@ export interface ConfigError { | ||||
|   message: string; | ||||
| } | ||||
|  | ||||
| export interface EntitiesEditorEvent extends CustomEvent { | ||||
| export interface EntitiesEditorEvent<T extends EntityConfig = EntityConfig> | ||||
|   extends CustomEvent { | ||||
|   detail: { | ||||
|     entities?: EntityConfig[]; | ||||
|     entities?: T[]; | ||||
|     item?: any; | ||||
|   }; | ||||
|   target: EventTarget | null; | ||||
|   | ||||
| @@ -111,11 +111,7 @@ export class HuiUnusedEntities extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _addToLovelaceView(): void { | ||||
|     const cardConfig = computeCards( | ||||
|       this.hass.states, | ||||
|       this._selectedEntities, | ||||
|       {} | ||||
|     ); | ||||
|     const cardConfig = computeCards(this.hass, this._selectedEntities, {}); | ||||
|     const sectionConfig = computeSection(this._selectedEntities, {}); | ||||
|  | ||||
|     if (this.lovelace.config.views.length === 1) { | ||||
|   | ||||
| @@ -1,11 +1,18 @@ | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { LitElement, css, html, nothing } from "lit"; | ||||
| import type { PropertyValues } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import type { LovelaceViewConfig } from "../../../../data/lovelace/config/view"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
|  | ||||
| import { | ||||
|   isMediaSourceContentId, | ||||
|   resolveMediaSource, | ||||
| } from "../../../../data/media_source"; | ||||
|  | ||||
| @customElement("hui-view-background-editor") | ||||
| export class HuiViewBackgroundEditor extends LitElement { | ||||
| @@ -13,6 +20,8 @@ export class HuiViewBackgroundEditor extends LitElement { | ||||
|  | ||||
|   @state() private _config!: LovelaceViewConfig; | ||||
|  | ||||
|   @state({ attribute: false }) private _resolvedImage?: string; | ||||
|  | ||||
|   set config(config: LovelaceViewConfig) { | ||||
|     this._config = config; | ||||
|   } | ||||
| @@ -20,10 +29,22 @@ export class HuiViewBackgroundEditor extends LitElement { | ||||
|   private _localizeValueCallback = (key: string) => | ||||
|     this.hass.localize(key as any); | ||||
|  | ||||
|   private _schema = memoizeOne((showSettings: boolean) => [ | ||||
|   private _schema = memoizeOne( | ||||
|     (localize: LocalizeFunc, showSettings: boolean) => | ||||
|       [ | ||||
|         { | ||||
|           name: "image", | ||||
|       selector: { background: { original: true } }, | ||||
|           selector: { | ||||
|             media: { | ||||
|               accept: ["image/*"] as string[], | ||||
|               clearable: true, | ||||
|               image_upload: true, | ||||
|               hide_content_type: true, | ||||
|               content_id_helper: localize( | ||||
|                 "ui.panel.lovelace.editor.card.picture.content_id_helper" | ||||
|               ), | ||||
|             }, | ||||
|           }, | ||||
|         }, | ||||
|         ...(showSettings | ||||
|           ? ([ | ||||
| @@ -99,16 +120,73 @@ export class HuiViewBackgroundEditor extends LitElement { | ||||
|               }, | ||||
|             ] as const) | ||||
|           : []), | ||||
|   ]); | ||||
|       ] as const | ||||
|   ); | ||||
|  | ||||
|   protected updated(changedProps: PropertyValues) { | ||||
|     if ( | ||||
|       this._config && | ||||
|       this.hass && | ||||
|       (changedProps.has("_config") || | ||||
|         (changedProps.has("hass") && !changedProps.get("hass"))) | ||||
|     ) { | ||||
|       const background = this._backgroundData(this._config); | ||||
|       this.style.setProperty( | ||||
|         "--picture-opacity", | ||||
|         `${(background.opacity ?? 100) / 100}` | ||||
|       ); | ||||
|  | ||||
|       const backgroundImage = | ||||
|         typeof background.image === "object" | ||||
|           ? background.image.media_content_id | ||||
|           : background.image; | ||||
|  | ||||
|       if (backgroundImage && isMediaSourceContentId(backgroundImage)) { | ||||
|         resolveMediaSource(this.hass, backgroundImage).then((result) => { | ||||
|           this._resolvedImage = result.url; | ||||
|         }); | ||||
|       } else { | ||||
|         this._resolvedImage = backgroundImage; | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this.hass) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     let background = this._config?.background; | ||||
|     const background = this._backgroundData(this._config); | ||||
|  | ||||
|     return html` | ||||
|       ${this._resolvedImage | ||||
|         ? html`<div class="previewContainer"> | ||||
|             <img | ||||
|               src=${this._resolvedImage} | ||||
|               alt=${this.hass.localize( | ||||
|                 "ui.components.picture-upload.current_image_alt" | ||||
|               )} | ||||
|             /> | ||||
|           </div>` | ||||
|         : nothing} | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${background} | ||||
|         .schema=${this._schema(this.hass.localize, true)} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         .localizeValue=${this._localizeValueCallback} | ||||
|       ></ha-form> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _backgroundData = memoizeOne( | ||||
|     (backgroundConfig?: LovelaceViewConfig) => { | ||||
|       let background = backgroundConfig?.background; | ||||
|       if (typeof background === "string") { | ||||
|       const backgroundUrl = background.match(/url\(['"]?([^'"]+)['"]?\)/)?.[1]; | ||||
|         const backgroundUrl = background.match( | ||||
|           /url\(['"]?([^'"]+)['"]?\)/ | ||||
|         )?.[1]; | ||||
|  | ||||
|         background = { | ||||
|           image: backgroundUrl, | ||||
| @@ -131,21 +209,14 @@ export class HuiViewBackgroundEditor extends LitElement { | ||||
|           repeat: "no-repeat", | ||||
|           attachment: "scroll", | ||||
|           ...background, | ||||
|           ...(typeof background.image === "string" | ||||
|             ? { image: { media_content_id: background.image } } | ||||
|             : {}), | ||||
|         }; | ||||
|       } | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${background} | ||||
|         .schema=${this._schema(true)} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|         .localizeValue=${this._localizeValueCallback} | ||||
|         style=${`--picture-opacity: ${(background.opacity ?? 100) / 100};`} | ||||
|       ></ha-form> | ||||
|     `; | ||||
|       return background; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     const config = { | ||||
| @@ -195,6 +266,23 @@ export class HuiViewBackgroundEditor extends LitElement { | ||||
|       display: block; | ||||
|       --file-upload-image-border-radius: var(--ha-border-radius-sm); | ||||
|     } | ||||
|     .previewContainer { | ||||
|       width: 100%; | ||||
|       display: flex; | ||||
|       flex-direction: column; | ||||
|       align-items: center; | ||||
|     } | ||||
|     img { | ||||
|       max-width: 100%; | ||||
|       max-height: 200px; | ||||
|       margin-bottom: 4px; | ||||
|       border-radius: var(--file-upload-image-border-radius); | ||||
|       transition: opacity 0.3s; | ||||
|       opacity: var(--picture-opacity, 1); | ||||
|     } | ||||
|     img:hover { | ||||
|       opacity: 1; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,17 @@ | ||||
| import { format } from "date-fns"; | ||||
| import type { PropertyValues, TemplateResult } from "lit"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import "../../../components/ha-date-input"; | ||||
| import { format } from "date-fns"; | ||||
| import { isUnavailableState, UNAVAILABLE } from "../../../data/entity"; | ||||
| import "../../../components/ha-time-input"; | ||||
| import { setDateTimeValue } from "../../../data/datetime"; | ||||
| import { isUnavailableState, UNAVAILABLE } from "../../../data/entity"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||
| import "../components/hui-generic-entity-row"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| import type { EntityConfig, LovelaceRow } from "./types"; | ||||
| import "../../../components/ha-time-input"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
|  | ||||
| @customElement("hui-datetime-entity-row") | ||||
| class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { | ||||
| @@ -53,6 +53,12 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { | ||||
|     const time = dateObj ? format(dateObj, "HH:mm:ss") : undefined; | ||||
|     const date = dateObj ? format(dateObj, "yyyy-MM-dd") : undefined; | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass!, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <hui-generic-entity-row | ||||
|         .hass=${this.hass} | ||||
| @@ -61,7 +67,7 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { | ||||
|       > | ||||
|         <div> | ||||
|           <ha-date-input | ||||
|             .label=${this._config.name || computeStateName(stateObj)} | ||||
|             .label=${name} | ||||
|             .locale=${this.hass.locale} | ||||
|             .value=${date} | ||||
|             .disabled=${unavailable} | ||||
|   | ||||
| @@ -1,7 +1,6 @@ | ||||
| import type { PropertyValues } from "lit"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import "../../../components/ha-date-input"; | ||||
| import "../../../components/ha-time-input"; | ||||
| import { isUnavailableState, UNKNOWN } from "../../../data/entity"; | ||||
| @@ -10,6 +9,7 @@ import { | ||||
|   stateToIsoDateString, | ||||
| } from "../../../data/input_datetime"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||
| import "../components/hui-generic-entity-row"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| @@ -47,7 +47,11 @@ class HuiInputDatetimeEntityRow extends LitElement implements LovelaceRow { | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     const name = this._config.name || computeStateName(stateObj); | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass!, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <hui-generic-entity-row | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user