mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-31 06:29:43 +00:00 
			
		
		
		
	Compare commits
	
		
			28 Commits
		
	
	
		
			power-char
			...
			section-vi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 9cd74fbff8 | ||
|   | 33a7aacd83 | ||
|   | 39546615bb | ||
|   | be51cbc944 | ||
|   | 77874aa2d7 | ||
|   | 4808463d5f | ||
|   | 5fb3cab247 | ||
|   | d1093b187f | ||
|   | fd7f0d3841 | ||
|   | 36aa74e4a5 | ||
|   | 938128d1c3 | ||
|   | 2a5d4ac578 | ||
|   | be63ff7702 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 132c68bf20 | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | 16499bbd6b | ||
| ![renovate[bot]](/assets/img/avatar_default.png)  | c7eddfed8f | ||
|   | 150842e431 | ||
|   | 9eb5360a68 | ||
|   | e9e32c7d91 | ||
|   | c83d760e82 | ||
|   | 489b7f9227 | ||
|   | ad2ba63155 | ||
|   | 29bc894dbd | ||
|   | faf6cb6333 | ||
|   | a2e1e6362b | ||
|   | 3212ab6f3b | ||
|   | 3d27daad80 | ||
|   | b679f1ce60 | 
| @@ -157,7 +157,7 @@ | |||||||
|     "@octokit/auth-oauth-device": "8.0.2", |     "@octokit/auth-oauth-device": "8.0.2", | ||||||
|     "@octokit/plugin-retry": "8.0.2", |     "@octokit/plugin-retry": "8.0.2", | ||||||
|     "@octokit/rest": "22.0.0", |     "@octokit/rest": "22.0.0", | ||||||
|     "@rsdoctor/rspack-plugin": "1.3.1", |     "@rsdoctor/rspack-plugin": "1.3.2", | ||||||
|     "@rspack/core": "1.5.8", |     "@rspack/core": "1.5.8", | ||||||
|     "@rspack/dev-server": "1.1.4", |     "@rspack/dev-server": "1.1.4", | ||||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", |     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||||
| @@ -167,7 +167,7 @@ | |||||||
|     "@types/culori": "4.0.1", |     "@types/culori": "4.0.1", | ||||||
|     "@types/html-minifier-terser": "7.0.2", |     "@types/html-minifier-terser": "7.0.2", | ||||||
|     "@types/js-yaml": "4.0.9", |     "@types/js-yaml": "4.0.9", | ||||||
|     "@types/leaflet": "1.9.20", |     "@types/leaflet": "1.9.21", | ||||||
|     "@types/leaflet-draw": "1.0.13", |     "@types/leaflet-draw": "1.0.13", | ||||||
|     "@types/leaflet.markercluster": "1.5.6", |     "@types/leaflet.markercluster": "1.5.6", | ||||||
|     "@types/lodash.merge": "4.6.9", |     "@types/lodash.merge": "4.6.9", | ||||||
| @@ -203,7 +203,7 @@ | |||||||
|     "husky": "9.1.7", |     "husky": "9.1.7", | ||||||
|     "jsdom": "27.0.0", |     "jsdom": "27.0.0", | ||||||
|     "jszip": "3.10.1", |     "jszip": "3.10.1", | ||||||
|     "lint-staged": "16.2.3", |     "lint-staged": "16.2.4", | ||||||
|     "lit-analyzer": "2.0.3", |     "lit-analyzer": "2.0.3", | ||||||
|     "lodash.merge": "4.6.2", |     "lodash.merge": "4.6.2", | ||||||
|     "lodash.template": "4.5.0", |     "lodash.template": "4.5.0", | ||||||
|   | |||||||
| @@ -9,6 +9,11 @@ import { getEntityContext } from "./context/get_entity_context"; | |||||||
|  |  | ||||||
| const DEFAULT_SEPARATOR = " "; | const DEFAULT_SEPARATOR = " "; | ||||||
|  |  | ||||||
|  | export const DEFAULT_ENTITY_NAME = [ | ||||||
|  |   { type: "device" }, | ||||||
|  |   { type: "entity" }, | ||||||
|  | ] satisfies EntityNameItem[]; | ||||||
|  |  | ||||||
| export type EntityNameItem = | export type EntityNameItem = | ||||||
|   | { |   | { | ||||||
|       type: "entity" | "device" | "area" | "floor"; |       type: "entity" | "device" | "area" | "floor"; | ||||||
| @@ -24,14 +29,14 @@ export interface EntityNameOptions { | |||||||
|  |  | ||||||
| export const computeEntityNameDisplay = ( | export const computeEntityNameDisplay = ( | ||||||
|   stateObj: HassEntity, |   stateObj: HassEntity, | ||||||
|   name: EntityNameItem | EntityNameItem[], |   name: EntityNameItem | EntityNameItem[] | undefined, | ||||||
|   entities: HomeAssistant["entities"], |   entities: HomeAssistant["entities"], | ||||||
|   devices: HomeAssistant["devices"], |   devices: HomeAssistant["devices"], | ||||||
|   areas: HomeAssistant["areas"], |   areas: HomeAssistant["areas"], | ||||||
|   floors: HomeAssistant["floors"], |   floors: HomeAssistant["floors"], | ||||||
|   options?: EntityNameOptions |   options?: EntityNameOptions | ||||||
| ) => { | ) => { | ||||||
|   let items = ensureArray(name); |   let items = ensureArray(name || DEFAULT_ENTITY_NAME); | ||||||
|  |  | ||||||
|   const separator = options?.separator ?? DEFAULT_SEPARATOR; |   const separator = options?.separator ?? DEFAULT_SEPARATOR; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -8,10 +8,10 @@ interface AreaContext { | |||||||
| } | } | ||||||
| export const getAreaContext = ( | export const getAreaContext = ( | ||||||
|   area: AreaRegistryEntry, |   area: AreaRegistryEntry, | ||||||
|   hass: HomeAssistant |   hassFloors: HomeAssistant["floors"] | ||||||
| ): AreaContext => { | ): AreaContext => { | ||||||
|   const floorId = area.floor_id; |   const floorId = area.floor_id; | ||||||
|   const floor = floorId ? hass.floors[floorId] : undefined; |   const floor = floorId ? hassFloors[floorId] : undefined; | ||||||
|  |  | ||||||
|   return { |   return { | ||||||
|     area: area, |     area: area, | ||||||
|   | |||||||
| @@ -1,21 +1,22 @@ | |||||||
| import type { LineSeriesOption } from "echarts"; | import type { LineSeriesOption } from "echarts"; | ||||||
|  |  | ||||||
| export function downSampleLineData( | export function downSampleLineData< | ||||||
|   data: LineSeriesOption["data"], |   T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number], | ||||||
|   chartWidth: number, | >( | ||||||
|  |   data: T[] | undefined, | ||||||
|  |   maxDetails: number, | ||||||
|   minX?: number, |   minX?: number, | ||||||
|   maxX?: number |   maxX?: number | ||||||
| ) { | ): T[] { | ||||||
|   if (!data || data.length < 10) { |   if (!data) { | ||||||
|     return data; |     return []; | ||||||
|   } |   } | ||||||
|   const width = chartWidth * window.devicePixelRatio; |   if (data.length <= maxDetails) { | ||||||
|   if (data.length <= width) { |  | ||||||
|     return data; |     return data; | ||||||
|   } |   } | ||||||
|   const min = minX ?? getPointData(data[0]!)[0]; |   const min = minX ?? getPointData(data[0]!)[0]; | ||||||
|   const max = maxX ?? getPointData(data[data.length - 1]!)[0]; |   const max = maxX ?? getPointData(data[data.length - 1]!)[0]; | ||||||
|   const step = Math.floor((max - min) / width); |   const step = Math.ceil((max - min) / Math.floor(maxDetails)); | ||||||
|   const frames = new Map< |   const frames = new Map< | ||||||
|     number, |     number, | ||||||
|     { |     { | ||||||
| @@ -47,7 +48,7 @@ export function downSampleLineData( | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Convert frames back to points |   // Convert frames back to points | ||||||
|   const result: typeof data = []; |   const result: T[] = []; | ||||||
|   for (const [_i, frame] of frames) { |   for (const [_i, frame] of frames) { | ||||||
|     // Use min/max points to preserve visual accuracy |     // Use min/max points to preserve visual accuracy | ||||||
|     // The order of the data must be preserved so max may be before min |     // The order of the data must be preserved so max may be before min | ||||||
|   | |||||||
| @@ -22,7 +22,7 @@ import { fireEvent } from "../../common/dom/fire_event"; | |||||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | import { listenMediaQuery } from "../../common/dom/media_query"; | ||||||
| import { themesContext } from "../../data/context"; | import { themesContext } from "../../data/context"; | ||||||
| import type { Themes } from "../../data/ws-themes"; | import type { Themes } from "../../data/ws-themes"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { isMac } from "../../util/is_mac"; | import { isMac } from "../../util/is_mac"; | ||||||
| import "../chips/ha-assist-chip"; | import "../chips/ha-assist-chip"; | ||||||
| @@ -346,7 +346,7 @@ export class HaChartBase extends LitElement { | |||||||
|       if (this.chart) { |       if (this.chart) { | ||||||
|         this.chart.dispose(); |         this.chart.dispose(); | ||||||
|       } |       } | ||||||
|       const echarts = (await import("../../resources/echarts")).default; |       const echarts = (await import("../../resources/echarts/echarts")).default; | ||||||
|  |  | ||||||
|       if (this.extraComponents?.length) { |       if (this.extraComponents?.length) { | ||||||
|         echarts.use(this.extraComponents); |         echarts.use(this.extraComponents); | ||||||
| @@ -805,7 +805,7 @@ export class HaChartBase extends LitElement { | |||||||
|             sampling: undefined, |             sampling: undefined, | ||||||
|             data: downSampleLineData( |             data: downSampleLineData( | ||||||
|               data as LineSeriesOption["data"], |               data as LineSeriesOption["data"], | ||||||
|               this.clientWidth, |               this.clientWidth * window.devicePixelRatio, | ||||||
|               minX, |               minX, | ||||||
|               maxX |               maxX | ||||||
|             ), |             ), | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ import type { TopLevelFormatterParams } from "echarts/types/dist/shared"; | |||||||
| import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js"; | import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | import { listenMediaQuery } from "../../common/dom/media_query"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
| import type { HaChartBase } from "./ha-chart-base"; | import type { HaChartBase } from "./ha-chart-base"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
|   | |||||||
| @@ -1,13 +1,13 @@ | |||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { LitElement, html, css } from "lit"; | import { LitElement, html, css } from "lit"; | ||||||
| import type { EChartsType } from "echarts/core"; | import type { EChartsType } from "echarts/core"; | ||||||
| import type { CallbackDataParams } from "echarts/types/dist/shared"; |  | ||||||
| import type { SankeySeriesOption } from "echarts/types/dist/echarts"; | import type { SankeySeriesOption } from "echarts/types/dist/echarts"; | ||||||
| import { SankeyChart } from "echarts/charts"; | import type { CallbackDataParams } from "echarts/types/src/util/types"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||||
|  | import SankeyChart from "../../resources/echarts/components/sankey/install"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import { measureTextWidth } from "../../util/text"; | import { measureTextWidth } from "../../util/text"; | ||||||
| import { filterXSS } from "../../common/util/xss"; | import { filterXSS } from "../../common/util/xss"; | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
| @@ -39,7 +39,7 @@ type ProcessedLink = Link & { | |||||||
|  |  | ||||||
| const OVERFLOW_MARGIN = 5; | const OVERFLOW_MARGIN = 5; | ||||||
| const FONT_SIZE = 12; | const FONT_SIZE = 12; | ||||||
| const NODE_GAP = 8; | const NODE_GAP = 6; | ||||||
| const LABEL_DISTANCE = 5; | const LABEL_DISTANCE = 5; | ||||||
|  |  | ||||||
| @customElement("ha-sankey-chart") | @customElement("ha-sankey-chart") | ||||||
| @@ -164,6 +164,7 @@ export class HaSankeyChart extends LitElement { | |||||||
|       lineStyle: { |       lineStyle: { | ||||||
|         color: "gradient", |         color: "gradient", | ||||||
|         opacity: 0.4, |         opacity: 0.4, | ||||||
|  |         curveness: 0.5, | ||||||
|       }, |       }, | ||||||
|       layoutIterations: 0, |       layoutIterations: 0, | ||||||
|       label: { |       label: { | ||||||
|   | |||||||
| @@ -11,7 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl"; | |||||||
| import type { LineChartEntity, LineChartState } from "../../data/history"; | import type { LineChartEntity, LineChartState } from "../../data/history"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; | import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; | ||||||
| import { | import { | ||||||
|   getNumberFormatOptions, |   getNumberFormatOptions, | ||||||
|   | |||||||
| @@ -15,8 +15,8 @@ import type { TimelineEntity } from "../../data/history"; | |||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | ||||||
| import { computeTimelineColor } from "./timeline-color"; | import { computeTimelineColor } from "./timeline-color"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import echarts from "../../resources/echarts"; | import echarts from "../../resources/echarts/echarts"; | ||||||
| import { luminosity } from "../../common/color/rgb"; | import { luminosity } from "../../common/color/rgb"; | ||||||
| import { hex2rgb } from "../../common/color/convert-color"; | import { hex2rgb } from "../../common/color/convert-color"; | ||||||
| import { measureTextWidth } from "../../util/text"; | import { measureTextWidth } from "../../util/text"; | ||||||
|   | |||||||
| @@ -29,7 +29,7 @@ import { | |||||||
|   getStatisticMetadata, |   getStatisticMetadata, | ||||||
|   statisticsHaveType, |   statisticsHaveType, | ||||||
| } from "../../data/recorder"; | } from "../../data/recorder"; | ||||||
| import type { ECOption } from "../../resources/echarts"; | import type { ECOption } from "../../resources/echarts/echarts"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import type { CustomLegendOption } from "./ha-chart-base"; | import type { CustomLegendOption } from "./ha-chart-base"; | ||||||
| import "./ha-chart-base"; | import "./ha-chart-base"; | ||||||
|   | |||||||
| @@ -5,24 +5,18 @@ import { customElement, property, query, state } from "lit/decorators"; | |||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { computeAreaName } from "../../common/entity/compute_area_name"; | import { computeAreaName } from "../../common/entity/compute_area_name"; | ||||||
| import { | import { computeDeviceName } from "../../common/entity/compute_device_name"; | ||||||
|   computeDeviceName, |  | ||||||
|   computeDeviceNameDisplay, |  | ||||||
| } from "../../common/entity/compute_device_name"; |  | ||||||
| import { computeDomain } from "../../common/entity/compute_domain"; |  | ||||||
| import { getDeviceContext } from "../../common/entity/context/get_device_context"; | import { getDeviceContext } from "../../common/entity/context/get_device_context"; | ||||||
| import { getConfigEntries, type ConfigEntry } from "../../data/config_entries"; | import { getConfigEntries, type ConfigEntry } from "../../data/config_entries"; | ||||||
| import { | import { | ||||||
|   getDeviceEntityDisplayLookup, |   getDevices, | ||||||
|   type DeviceEntityDisplayLookup, |   type DevicePickerItem, | ||||||
|   type DeviceRegistryEntry, |   type DeviceRegistryEntry, | ||||||
| } from "../../data/device_registry"; | } from "../../data/device_registry"; | ||||||
| import { domainToName } from "../../data/integration"; |  | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import { brandsUrl } from "../../util/brands-url"; | import { brandsUrl } from "../../util/brands-url"; | ||||||
| import "../ha-generic-picker"; | import "../ha-generic-picker"; | ||||||
| import type { HaGenericPicker } from "../ha-generic-picker"; | import type { HaGenericPicker } from "../ha-generic-picker"; | ||||||
| import type { PickerComboBoxItem } from "../ha-picker-combo-box"; |  | ||||||
|  |  | ||||||
| export type HaDevicePickerDeviceFilterFunc = ( | export type HaDevicePickerDeviceFilterFunc = ( | ||||||
|   device: DeviceRegistryEntry |   device: DeviceRegistryEntry | ||||||
| @@ -30,11 +24,6 @@ export type HaDevicePickerDeviceFilterFunc = ( | |||||||
|  |  | ||||||
| export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; | export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; | ||||||
|  |  | ||||||
| interface DevicePickerItem extends PickerComboBoxItem { |  | ||||||
|   domain?: string; |  | ||||||
|   domain_name?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement("ha-device-picker") | @customElement("ha-device-picker") | ||||||
| export class HaDevicePicker extends LitElement { | export class HaDevicePicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -104,6 +93,8 @@ export class HaDevicePicker extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _configEntryLookup: Record<string, ConfigEntry> = {}; |   @state() private _configEntryLookup: Record<string, ConfigEntry> = {}; | ||||||
|  |  | ||||||
|  |   private _getDevicesMemoized = memoizeOne(getDevices); | ||||||
|  |  | ||||||
|   protected firstUpdated(_changedProperties: PropertyValues): void { |   protected firstUpdated(_changedProperties: PropertyValues): void { | ||||||
|     super.firstUpdated(_changedProperties); |     super.firstUpdated(_changedProperties); | ||||||
|     this._loadConfigEntries(); |     this._loadConfigEntries(); | ||||||
| @@ -117,162 +108,18 @@ export class HaDevicePicker extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _getItems = () => |   private _getItems = () => | ||||||
|     this._getDevices( |     this._getDevicesMemoized( | ||||||
|       this.hass.devices, |       this.hass, | ||||||
|       this.hass.entities, |  | ||||||
|       this._configEntryLookup, |       this._configEntryLookup, | ||||||
|       this.includeDomains, |       this.includeDomains, | ||||||
|       this.excludeDomains, |       this.excludeDomains, | ||||||
|       this.includeDeviceClasses, |       this.includeDeviceClasses, | ||||||
|       this.deviceFilter, |       this.deviceFilter, | ||||||
|       this.entityFilter, |       this.entityFilter, | ||||||
|       this.excludeDevices |       this.excludeDevices, | ||||||
|  |       this.value | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   private _getDevices = memoizeOne( |  | ||||||
|     ( |  | ||||||
|       haDevices: HomeAssistant["devices"], |  | ||||||
|       haEntities: HomeAssistant["entities"], |  | ||||||
|       configEntryLookup: Record<string, ConfigEntry>, |  | ||||||
|       includeDomains: this["includeDomains"], |  | ||||||
|       excludeDomains: this["excludeDomains"], |  | ||||||
|       includeDeviceClasses: this["includeDeviceClasses"], |  | ||||||
|       deviceFilter: this["deviceFilter"], |  | ||||||
|       entityFilter: this["entityFilter"], |  | ||||||
|       excludeDevices: this["excludeDevices"] |  | ||||||
|     ): DevicePickerItem[] => { |  | ||||||
|       const devices = Object.values(haDevices); |  | ||||||
|       const entities = Object.values(haEntities); |  | ||||||
|  |  | ||||||
|       let deviceEntityLookup: DeviceEntityDisplayLookup = {}; |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         includeDomains || |  | ||||||
|         excludeDomains || |  | ||||||
|         includeDeviceClasses || |  | ||||||
|         entityFilter |  | ||||||
|       ) { |  | ||||||
|         deviceEntityLookup = getDeviceEntityDisplayLookup(entities); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let inputDevices = devices.filter( |  | ||||||
|         (device) => device.id === this.value || !device.disabled_by |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       if (includeDomains) { |  | ||||||
|         inputDevices = inputDevices.filter((device) => { |  | ||||||
|           const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|           if (!devEntities || !devEntities.length) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|           return deviceEntityLookup[device.id].some((entity) => |  | ||||||
|             includeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeDomains) { |  | ||||||
|         inputDevices = inputDevices.filter((device) => { |  | ||||||
|           const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|           if (!devEntities || !devEntities.length) { |  | ||||||
|             return true; |  | ||||||
|           } |  | ||||||
|           return entities.every( |  | ||||||
|             (entity) => |  | ||||||
|               !excludeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeDevices) { |  | ||||||
|         inputDevices = inputDevices.filter( |  | ||||||
|           (device) => !excludeDevices!.includes(device.id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (includeDeviceClasses) { |  | ||||||
|         inputDevices = inputDevices.filter((device) => { |  | ||||||
|           const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|           if (!devEntities || !devEntities.length) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|           return deviceEntityLookup[device.id].some((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             if (!stateObj) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return ( |  | ||||||
|               stateObj.attributes.device_class && |  | ||||||
|               includeDeviceClasses.includes(stateObj.attributes.device_class) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (entityFilter) { |  | ||||||
|         inputDevices = inputDevices.filter((device) => { |  | ||||||
|           const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|           if (!devEntities || !devEntities.length) { |  | ||||||
|             return false; |  | ||||||
|           } |  | ||||||
|           return devEntities.some((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             if (!stateObj) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return entityFilter(stateObj); |  | ||||||
|           }); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (deviceFilter) { |  | ||||||
|         inputDevices = inputDevices.filter( |  | ||||||
|           (device) => |  | ||||||
|             // We always want to include the device of the current value |  | ||||||
|             device.id === this.value || deviceFilter!(device) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const outputDevices = inputDevices.map<DevicePickerItem>((device) => { |  | ||||||
|         const deviceName = computeDeviceNameDisplay( |  | ||||||
|           device, |  | ||||||
|           this.hass, |  | ||||||
|           deviceEntityLookup[device.id] |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         const { area } = getDeviceContext(device, this.hass); |  | ||||||
|  |  | ||||||
|         const areaName = area ? computeAreaName(area) : undefined; |  | ||||||
|  |  | ||||||
|         const configEntry = device.primary_config_entry |  | ||||||
|           ? configEntryLookup?.[device.primary_config_entry] |  | ||||||
|           : undefined; |  | ||||||
|  |  | ||||||
|         const domain = configEntry?.domain; |  | ||||||
|         const domainName = domain |  | ||||||
|           ? domainToName(this.hass.localize, domain) |  | ||||||
|           : undefined; |  | ||||||
|  |  | ||||||
|         return { |  | ||||||
|           id: device.id, |  | ||||||
|           label: "", |  | ||||||
|           primary: |  | ||||||
|             deviceName || |  | ||||||
|             this.hass.localize("ui.components.device-picker.unnamed_device"), |  | ||||||
|           secondary: areaName, |  | ||||||
|           domain: configEntry?.domain, |  | ||||||
|           domain_name: domainName, |  | ||||||
|           search_labels: [deviceName, areaName, domain, domainName].filter( |  | ||||||
|             Boolean |  | ||||||
|           ) as string[], |  | ||||||
|           sorting_label: deviceName || "zzz", |  | ||||||
|         }; |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       return outputDevices; |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   private _valueRenderer = memoizeOne( |   private _valueRenderer = memoizeOne( | ||||||
|     (configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => { |     (configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => { | ||||||
|       const deviceId = value; |       const deviceId = value; | ||||||
|   | |||||||
| @@ -7,7 +7,7 @@ import { isValidEntityId } from "../../common/entity/valid_entity_id"; | |||||||
| import type { HomeAssistant, ValueChangedEvent } from "../../types"; | import type { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||||
| import "../ha-sortable"; | import "../ha-sortable"; | ||||||
| import "./ha-entity-picker"; | import "./ha-entity-picker"; | ||||||
| import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker"; | import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; | ||||||
|  |  | ||||||
| @customElement("ha-entities-picker") | @customElement("ha-entities-picker") | ||||||
| class HaEntitiesPicker extends LitElement { | class HaEntitiesPicker extends LitElement { | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import type { HassEntity } from "home-assistant-js-websocket"; |  | ||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { LitElement, html, nothing } from "lit"; | import { LitElement, html, nothing } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| @@ -8,8 +7,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types"; | |||||||
| import "../ha-combo-box"; | import "../ha-combo-box"; | ||||||
| import type { HaComboBox } from "../ha-combo-box"; | import type { HaComboBox } from "../ha-combo-box"; | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; |  | ||||||
|  |  | ||||||
| interface AttributeOption { | interface AttributeOption { | ||||||
|   value: string; |   value: string; | ||||||
|   label: string; |   label: string; | ||||||
|   | |||||||
| @@ -25,6 +25,7 @@ import "../ha-sortable"; | |||||||
| interface EntityNameOption { | interface EntityNameOption { | ||||||
|   primary: string; |   primary: string; | ||||||
|   secondary?: string; |   secondary?: string; | ||||||
|  |   field_label: string; | ||||||
|   value: string; |   value: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -41,6 +42,23 @@ const KNOWN_TYPES = new Set(["entity", "device", "area", "floor"]); | |||||||
|  |  | ||||||
| const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]); | const UNIQUE_TYPES = new Set(["entity", "device", "area", "floor"]); | ||||||
|  |  | ||||||
|  | const formatOptionValue = (item: EntityNameItem) => { | ||||||
|  |   if (item.type === "text" && item.text) { | ||||||
|  |     return item.text; | ||||||
|  |   } | ||||||
|  |   return `___${item.type}___`; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | const parseOptionValue = (value: string): EntityNameItem => { | ||||||
|  |   if (value.startsWith("___") && value.endsWith("___")) { | ||||||
|  |     const type = value.slice(3, -3); | ||||||
|  |     if (KNOWN_TYPES.has(type)) { | ||||||
|  |       return { type: type as EntityNameType }; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   return { type: "text", text: value }; | ||||||
|  | }; | ||||||
|  |  | ||||||
| @customElement("ha-entity-name-picker") | @customElement("ha-entity-name-picker") | ||||||
| export class HaEntityNamePicker extends LitElement { | export class HaEntityNamePicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -121,13 +139,23 @@ export class HaEntityNamePicker extends LitElement { | |||||||
|       return { |       return { | ||||||
|         primary, |         primary, | ||||||
|         secondary, |         secondary, | ||||||
|         value: name, |         field_label: primary, | ||||||
|  |         value: formatOptionValue({ type: name }), | ||||||
|       }; |       }; | ||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     return items; |     return items; | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  |   private _customNameOption = memoizeOne((text: string) => ({ | ||||||
|  |     primary: this.hass.localize( | ||||||
|  |       "ui.components.entity.entity-name-picker.custom_name" | ||||||
|  |     ), | ||||||
|  |     secondary: `"${text}"`, | ||||||
|  |     field_label: text, | ||||||
|  |     value: formatOptionValue({ type: "text", text }), | ||||||
|  |   })); | ||||||
|  |  | ||||||
|   private _formatItem = (item: EntityNameItem) => { |   private _formatItem = (item: EntityNameItem) => { | ||||||
|     if (item.type === "text") { |     if (item.type === "text") { | ||||||
|       return `"${item.text}"`; |       return `"${item.text}"`; | ||||||
| @@ -214,7 +242,7 @@ export class HaEntityNamePicker extends LitElement { | |||||||
|             allow-custom-value |             allow-custom-value | ||||||
|             item-id-path="value" |             item-id-path="value" | ||||||
|             item-value-path="value" |             item-value-path="value" | ||||||
|             item-label-path="primary" |             item-label-path="field_label" | ||||||
|             .renderer=${rowRenderer} |             .renderer=${rowRenderer} | ||||||
|             @opened-changed=${this._openedChanged} |             @opened-changed=${this._openedChanged} | ||||||
|             @value-changed=${this._comboBoxValueChanged} |             @value-changed=${this._comboBoxValueChanged} | ||||||
| @@ -286,14 +314,13 @@ export class HaEntityNamePicker extends LitElement { | |||||||
|       const initialItem = |       const initialItem = | ||||||
|         this._editIndex != null ? this._value[this._editIndex] : undefined; |         this._editIndex != null ? this._value[this._editIndex] : undefined; | ||||||
|  |  | ||||||
|       const initialValue = initialItem |       const initialValue = initialItem ? formatOptionValue(initialItem) : ""; | ||||||
|         ? initialItem.type === "text" |  | ||||||
|           ? initialItem.text |  | ||||||
|           : initialItem.type |  | ||||||
|         : ""; |  | ||||||
|  |  | ||||||
|       const filteredItems = this._filterSelectedOptions(options, initialValue); |       const filteredItems = this._filterSelectedOptions(options, initialValue); | ||||||
|  |  | ||||||
|  |       if (initialItem && initialItem.type === "text" && initialItem.text) { | ||||||
|  |         filteredItems.push(this._customNameOption(initialItem.text)); | ||||||
|  |       } | ||||||
|       this._comboBox.filteredItems = filteredItems; |       this._comboBox.filteredItems = filteredItems; | ||||||
|       this._comboBox.setInputValue(initialValue); |       this._comboBox.setInputValue(initialValue); | ||||||
|     } else { |     } else { | ||||||
| @@ -326,11 +353,7 @@ export class HaEntityNamePicker extends LitElement { | |||||||
|     const currentItem = |     const currentItem = | ||||||
|       this._editIndex != null ? this._value[this._editIndex] : undefined; |       this._editIndex != null ? this._value[this._editIndex] : undefined; | ||||||
|  |  | ||||||
|     const currentValue = currentItem |     const currentValue = currentItem ? formatOptionValue(currentItem) : ""; | ||||||
|       ? currentItem.type === "text" |  | ||||||
|         ? currentItem.text |  | ||||||
|         : currentItem.type |  | ||||||
|       : ""; |  | ||||||
|  |  | ||||||
|     this._comboBox.filteredItems = this._filterSelectedOptions( |     this._comboBox.filteredItems = this._filterSelectedOptions( | ||||||
|       options, |       options, | ||||||
| @@ -352,6 +375,7 @@ export class HaEntityNamePicker extends LitElement { | |||||||
|     const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions); |     const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions); | ||||||
|     const filteredItems = fuse.search(filter).map((result) => result.item); |     const filteredItems = fuse.search(filter).map((result) => result.item); | ||||||
|  |  | ||||||
|  |     filteredItems.push(this._customNameOption(input)); | ||||||
|     this._comboBox.filteredItems = filteredItems; |     this._comboBox.filteredItems = filteredItems; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -385,9 +409,7 @@ export class HaEntityNamePicker extends LitElement { | |||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const item: EntityNameItem = KNOWN_TYPES.has(value as any) |     const item: EntityNameItem = parseOptionValue(value); | ||||||
|       ? { type: value as EntityNameType } |  | ||||||
|       : { type: "text", text: value }; |  | ||||||
|  |  | ||||||
|     const newValue = [...this._value]; |     const newValue = [...this._value]; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,15 +1,17 @@ | |||||||
| import { mdiPlus, mdiShape } from "@mdi/js"; | import { mdiPlus, mdiShape } from "@mdi/js"; | ||||||
| import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | import type { ComboBoxLitRenderer } from "@vaadin/combo-box/lit"; | ||||||
| import type { HassEntity } from "home-assistant-js-websocket"; |  | ||||||
| import { html, LitElement, nothing, type PropertyValues } from "lit"; | import { html, LitElement, nothing, type PropertyValues } from "lit"; | ||||||
| import { customElement, property, query } from "lit/decorators"; | import { customElement, property, query } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import { computeDomain } from "../../common/entity/compute_domain"; |  | ||||||
| import { computeEntityNameList } from "../../common/entity/compute_entity_name_display"; | import { computeEntityNameList } from "../../common/entity/compute_entity_name_display"; | ||||||
| import { computeStateName } from "../../common/entity/compute_state_name"; |  | ||||||
| import { isValidEntityId } from "../../common/entity/valid_entity_id"; | import { isValidEntityId } from "../../common/entity/valid_entity_id"; | ||||||
| import { computeRTL } from "../../common/util/compute_rtl"; | import { computeRTL } from "../../common/util/compute_rtl"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; | ||||||
|  | import { | ||||||
|  |   getEntities, | ||||||
|  |   type EntityComboBoxItem, | ||||||
|  | } from "../../data/entity_registry"; | ||||||
| import { domainToName } from "../../data/integration"; | import { domainToName } from "../../data/integration"; | ||||||
| import { | import { | ||||||
|   isHelperDomain, |   isHelperDomain, | ||||||
| @@ -20,21 +22,11 @@ import type { HomeAssistant } from "../../types"; | |||||||
| import "../ha-combo-box-item"; | import "../ha-combo-box-item"; | ||||||
| import "../ha-generic-picker"; | import "../ha-generic-picker"; | ||||||
| import type { HaGenericPicker } from "../ha-generic-picker"; | import type { HaGenericPicker } from "../ha-generic-picker"; | ||||||
| import type { | import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box"; | ||||||
|   PickerComboBoxItem, |  | ||||||
|   PickerComboBoxSearchFn, |  | ||||||
| } from "../ha-picker-combo-box"; |  | ||||||
| import type { PickerValueRenderer } from "../ha-picker-field"; | import type { PickerValueRenderer } from "../ha-picker-field"; | ||||||
| import "../ha-svg-icon"; | import "../ha-svg-icon"; | ||||||
| import "./state-badge"; | import "./state-badge"; | ||||||
|  |  | ||||||
| interface EntityComboBoxItem extends PickerComboBoxItem { |  | ||||||
|   domain_name?: string; |  | ||||||
|   stateObj?: HassEntity; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; |  | ||||||
|  |  | ||||||
| const CREATE_ID = "___create-new-entity___"; | const CREATE_ID = "___create-new-entity___"; | ||||||
|  |  | ||||||
| @customElement("ha-entity-picker") | @customElement("ha-entity-picker") | ||||||
| @@ -255,8 +247,10 @@ export class HaEntityPicker extends LitElement { | |||||||
|     } |     } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|  |   private _getEntitiesMemoized = memoizeOne(getEntities); | ||||||
|  |  | ||||||
|   private _getItems = () => |   private _getItems = () => | ||||||
|     this._getEntities( |     this._getEntitiesMemoized( | ||||||
|       this.hass, |       this.hass, | ||||||
|       this.includeDomains, |       this.includeDomains, | ||||||
|       this.excludeDomains, |       this.excludeDomains, | ||||||
| @@ -264,128 +258,10 @@ export class HaEntityPicker extends LitElement { | |||||||
|       this.includeDeviceClasses, |       this.includeDeviceClasses, | ||||||
|       this.includeUnitOfMeasurement, |       this.includeUnitOfMeasurement, | ||||||
|       this.includeEntities, |       this.includeEntities, | ||||||
|       this.excludeEntities |       this.excludeEntities, | ||||||
|  |       this.value | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|   private _getEntities = memoizeOne( |  | ||||||
|     ( |  | ||||||
|       hass: this["hass"], |  | ||||||
|       includeDomains: this["includeDomains"], |  | ||||||
|       excludeDomains: this["excludeDomains"], |  | ||||||
|       entityFilter: this["entityFilter"], |  | ||||||
|       includeDeviceClasses: this["includeDeviceClasses"], |  | ||||||
|       includeUnitOfMeasurement: this["includeUnitOfMeasurement"], |  | ||||||
|       includeEntities: this["includeEntities"], |  | ||||||
|       excludeEntities: this["excludeEntities"] |  | ||||||
|     ): EntityComboBoxItem[] => { |  | ||||||
|       let items: EntityComboBoxItem[] = []; |  | ||||||
|  |  | ||||||
|       let entityIds = Object.keys(hass.states); |  | ||||||
|  |  | ||||||
|       if (includeEntities) { |  | ||||||
|         entityIds = entityIds.filter((entityId) => |  | ||||||
|           includeEntities.includes(entityId) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeEntities) { |  | ||||||
|         entityIds = entityIds.filter( |  | ||||||
|           (entityId) => !excludeEntities.includes(entityId) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (includeDomains) { |  | ||||||
|         entityIds = entityIds.filter((eid) => |  | ||||||
|           includeDomains.includes(computeDomain(eid)) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeDomains) { |  | ||||||
|         entityIds = entityIds.filter( |  | ||||||
|           (eid) => !excludeDomains.includes(computeDomain(eid)) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const isRTL = computeRTL(hass); |  | ||||||
|  |  | ||||||
|       items = entityIds.map<EntityComboBoxItem>((entityId) => { |  | ||||||
|         const stateObj = hass.states[entityId]; |  | ||||||
|  |  | ||||||
|         const friendlyName = computeStateName(stateObj); // Keep this for search |  | ||||||
|  |  | ||||||
|         const [entityName, deviceName, areaName] = computeEntityNameList( |  | ||||||
|           stateObj, |  | ||||||
|           [{ type: "entity" }, { type: "device" }, { type: "area" }], |  | ||||||
|           hass.entities, |  | ||||||
|           hass.devices, |  | ||||||
|           hass.areas, |  | ||||||
|           hass.floors |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         const domainName = domainToName(hass.localize, computeDomain(entityId)); |  | ||||||
|  |  | ||||||
|         const primary = entityName || deviceName || entityId; |  | ||||||
|         const secondary = [areaName, entityName ? deviceName : undefined] |  | ||||||
|           .filter(Boolean) |  | ||||||
|           .join(isRTL ? " ◂ " : " ▸ "); |  | ||||||
|         const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - "); |  | ||||||
|  |  | ||||||
|         return { |  | ||||||
|           id: entityId, |  | ||||||
|           primary: primary, |  | ||||||
|           secondary: secondary, |  | ||||||
|           domain_name: domainName, |  | ||||||
|           sorting_label: [deviceName, entityName].filter(Boolean).join("_"), |  | ||||||
|           search_labels: [ |  | ||||||
|             entityName, |  | ||||||
|             deviceName, |  | ||||||
|             areaName, |  | ||||||
|             domainName, |  | ||||||
|             friendlyName, |  | ||||||
|             entityId, |  | ||||||
|           ].filter(Boolean) as string[], |  | ||||||
|           a11y_label: a11yLabel, |  | ||||||
|           stateObj: stateObj, |  | ||||||
|         }; |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       if (includeDeviceClasses) { |  | ||||||
|         items = items.filter( |  | ||||||
|           (item) => |  | ||||||
|             // We always want to include the entity of the current value |  | ||||||
|             item.id === this.value || |  | ||||||
|             (item.stateObj?.attributes.device_class && |  | ||||||
|               includeDeviceClasses.includes( |  | ||||||
|                 item.stateObj.attributes.device_class |  | ||||||
|               )) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (includeUnitOfMeasurement) { |  | ||||||
|         items = items.filter( |  | ||||||
|           (item) => |  | ||||||
|             // We always want to include the entity of the current value |  | ||||||
|             item.id === this.value || |  | ||||||
|             (item.stateObj?.attributes.unit_of_measurement && |  | ||||||
|               includeUnitOfMeasurement.includes( |  | ||||||
|                 item.stateObj.attributes.unit_of_measurement |  | ||||||
|               )) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (entityFilter) { |  | ||||||
|         items = items.filter( |  | ||||||
|           (item) => |  | ||||||
|             // We always want to include the entity of the current value |  | ||||||
|             item.id === this.value || |  | ||||||
|             (item.stateObj && entityFilter!(item.stateObj)) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       return items; |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     const placeholder = |     const placeholder = | ||||||
|       this.placeholder ?? |       this.placeholder ?? | ||||||
|   | |||||||
| @@ -1,4 +1,3 @@ | |||||||
| import type { HassEntity } from "home-assistant-js-websocket"; |  | ||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { LitElement, html, nothing } from "lit"; | import { LitElement, html, nothing } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| @@ -9,8 +8,6 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types"; | |||||||
| import "../ha-combo-box"; | import "../ha-combo-box"; | ||||||
| import type { HaComboBox } from "../ha-combo-box"; | import type { HaComboBox } from "../ha-combo-box"; | ||||||
|  |  | ||||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; |  | ||||||
|  |  | ||||||
| interface StateOption { | interface StateOption { | ||||||
|   value: string; |   value: string; | ||||||
|   label: string; |   label: string; | ||||||
|   | |||||||
| @@ -21,6 +21,7 @@ import "../ha-combo-box-item"; | |||||||
| import "../ha-generic-picker"; | import "../ha-generic-picker"; | ||||||
| import type { HaGenericPicker } from "../ha-generic-picker"; | import type { HaGenericPicker } from "../ha-generic-picker"; | ||||||
| import "../ha-icon-button"; | import "../ha-icon-button"; | ||||||
|  | import "../ha-input-helper-text"; | ||||||
| import type { | import type { | ||||||
|   PickerComboBoxItem, |   PickerComboBoxItem, | ||||||
|   PickerComboBoxSearchFn, |   PickerComboBoxSearchFn, | ||||||
| @@ -476,7 +477,6 @@ export class HaStatisticPicker extends LitElement { | |||||||
|         .hideClearIcon=${this.hideClearIcon} |         .hideClearIcon=${this.hideClearIcon} | ||||||
|         .searchFn=${this._searchFn} |         .searchFn=${this._searchFn} | ||||||
|         .valueRenderer=${this._valueRenderer} |         .valueRenderer=${this._valueRenderer} | ||||||
|         .helper=${this.helper} |  | ||||||
|         @value-changed=${this._valueChanged} |         @value-changed=${this._valueChanged} | ||||||
|       > |       > | ||||||
|       </ha-generic-picker> |       </ha-generic-picker> | ||||||
|   | |||||||
| @@ -8,21 +8,13 @@ import { styleMap } from "lit/directives/style-map"; | |||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { computeAreaName } from "../common/entity/compute_area_name"; | import { computeAreaName } from "../common/entity/compute_area_name"; | ||||||
| import { computeDomain } from "../common/entity/compute_domain"; |  | ||||||
| import { computeFloorName } from "../common/entity/compute_floor_name"; | import { computeFloorName } from "../common/entity/compute_floor_name"; | ||||||
| import { stringCompare } from "../common/string/compare"; |  | ||||||
| import { computeRTL } from "../common/util/compute_rtl"; | import { computeRTL } from "../common/util/compute_rtl"; | ||||||
| import type { AreaRegistryEntry } from "../data/area_registry"; |  | ||||||
| import type { |  | ||||||
|   DeviceEntityDisplayLookup, |  | ||||||
|   DeviceRegistryEntry, |  | ||||||
| } from "../data/device_registry"; |  | ||||||
| import { getDeviceEntityDisplayLookup } from "../data/device_registry"; |  | ||||||
| import type { EntityRegistryDisplayEntry } from "../data/entity_registry"; |  | ||||||
| import { | import { | ||||||
|   getFloorAreaLookup, |   getAreasAndFloors, | ||||||
|   type FloorRegistryEntry, |   type AreaFloorValue, | ||||||
| } from "../data/floor_registry"; |   type FloorComboBoxItem, | ||||||
|  | } from "../data/area_floor"; | ||||||
| import type { HomeAssistant, ValueChangedEvent } from "../types"; | import type { HomeAssistant, ValueChangedEvent } from "../types"; | ||||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||||
| import "./ha-combo-box-item"; | import "./ha-combo-box-item"; | ||||||
| @@ -30,24 +22,12 @@ import "./ha-floor-icon"; | |||||||
| import "./ha-generic-picker"; | import "./ha-generic-picker"; | ||||||
| import type { HaGenericPicker } from "./ha-generic-picker"; | import type { HaGenericPicker } from "./ha-generic-picker"; | ||||||
| import "./ha-icon-button"; | import "./ha-icon-button"; | ||||||
| import type { PickerComboBoxItem } from "./ha-picker-combo-box"; |  | ||||||
| import type { PickerValueRenderer } from "./ha-picker-field"; | import type { PickerValueRenderer } from "./ha-picker-field"; | ||||||
| import "./ha-svg-icon"; | import "./ha-svg-icon"; | ||||||
| import "./ha-tree-indicator"; | import "./ha-tree-indicator"; | ||||||
|  |  | ||||||
| const SEPARATOR = "________"; | const SEPARATOR = "________"; | ||||||
|  |  | ||||||
| interface FloorComboBoxItem extends PickerComboBoxItem { |  | ||||||
|   type: "floor" | "area"; |  | ||||||
|   floor?: FloorRegistryEntry; |  | ||||||
|   area?: AreaRegistryEntry; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| interface AreaFloorValue { |  | ||||||
|   id: string; |  | ||||||
|   type: "floor" | "area"; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| @customElement("ha-area-floor-picker") | @customElement("ha-area-floor-picker") | ||||||
| export class HaAreaFloorPicker extends LitElement { | export class HaAreaFloorPicker extends LitElement { | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
| @@ -154,243 +134,6 @@ export class HaAreaFloorPicker extends LitElement { | |||||||
|     `; |     `; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|   private _getAreasAndFloors = memoizeOne( |  | ||||||
|     ( |  | ||||||
|       haFloors: HomeAssistant["floors"], |  | ||||||
|       haAreas: HomeAssistant["areas"], |  | ||||||
|       haDevices: HomeAssistant["devices"], |  | ||||||
|       haEntities: HomeAssistant["entities"], |  | ||||||
|       includeDomains: this["includeDomains"], |  | ||||||
|       excludeDomains: this["excludeDomains"], |  | ||||||
|       includeDeviceClasses: this["includeDeviceClasses"], |  | ||||||
|       deviceFilter: this["deviceFilter"], |  | ||||||
|       entityFilter: this["entityFilter"], |  | ||||||
|       excludeAreas: this["excludeAreas"], |  | ||||||
|       excludeFloors: this["excludeFloors"] |  | ||||||
|     ): FloorComboBoxItem[] => { |  | ||||||
|       const floors = Object.values(haFloors); |  | ||||||
|       const areas = Object.values(haAreas); |  | ||||||
|       const devices = Object.values(haDevices); |  | ||||||
|       const entities = Object.values(haEntities); |  | ||||||
|  |  | ||||||
|       let deviceEntityLookup: DeviceEntityDisplayLookup = {}; |  | ||||||
|       let inputDevices: DeviceRegistryEntry[] | undefined; |  | ||||||
|       let inputEntities: EntityRegistryDisplayEntry[] | undefined; |  | ||||||
|  |  | ||||||
|       if ( |  | ||||||
|         includeDomains || |  | ||||||
|         excludeDomains || |  | ||||||
|         includeDeviceClasses || |  | ||||||
|         deviceFilter || |  | ||||||
|         entityFilter |  | ||||||
|       ) { |  | ||||||
|         deviceEntityLookup = getDeviceEntityDisplayLookup(entities); |  | ||||||
|         inputDevices = devices; |  | ||||||
|         inputEntities = entities.filter((entity) => entity.area_id); |  | ||||||
|  |  | ||||||
|         if (includeDomains) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => |  | ||||||
|               includeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => |  | ||||||
|             includeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (excludeDomains) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return true; |  | ||||||
|             } |  | ||||||
|             return entities.every( |  | ||||||
|               (entity) => |  | ||||||
|                 !excludeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter( |  | ||||||
|             (entity) => |  | ||||||
|               !excludeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (includeDeviceClasses) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => { |  | ||||||
|               const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|               if (!stateObj) { |  | ||||||
|                 return false; |  | ||||||
|               } |  | ||||||
|               return ( |  | ||||||
|                 stateObj.attributes.device_class && |  | ||||||
|                 includeDeviceClasses.includes(stateObj.attributes.device_class) |  | ||||||
|               ); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             return ( |  | ||||||
|               stateObj.attributes.device_class && |  | ||||||
|               includeDeviceClasses.includes(stateObj.attributes.device_class) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (deviceFilter) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => |  | ||||||
|             deviceFilter!(device) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (entityFilter) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => { |  | ||||||
|               const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|               if (!stateObj) { |  | ||||||
|                 return false; |  | ||||||
|               } |  | ||||||
|               return entityFilter(stateObj); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             if (!stateObj) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return entityFilter!(stateObj); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let outputAreas = areas; |  | ||||||
|  |  | ||||||
|       let areaIds: string[] | undefined; |  | ||||||
|  |  | ||||||
|       if (inputDevices) { |  | ||||||
|         areaIds = inputDevices |  | ||||||
|           .filter((device) => device.area_id) |  | ||||||
|           .map((device) => device.area_id!); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (inputEntities) { |  | ||||||
|         areaIds = (areaIds ?? []).concat( |  | ||||||
|           inputEntities |  | ||||||
|             .filter((entity) => entity.area_id) |  | ||||||
|             .map((entity) => entity.area_id!) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (areaIds) { |  | ||||||
|         outputAreas = outputAreas.filter((area) => |  | ||||||
|           areaIds!.includes(area.area_id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeAreas) { |  | ||||||
|         outputAreas = outputAreas.filter( |  | ||||||
|           (area) => !excludeAreas!.includes(area.area_id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeFloors) { |  | ||||||
|         outputAreas = outputAreas.filter( |  | ||||||
|           (area) => !area.floor_id || !excludeFloors!.includes(area.floor_id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const floorAreaLookup = getFloorAreaLookup(outputAreas); |  | ||||||
|       const unassisgnedAreas = Object.values(outputAreas).filter( |  | ||||||
|         (area) => !area.floor_id || !floorAreaLookup[area.floor_id] |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       // @ts-ignore |  | ||||||
|       const floorAreaEntries: [ |  | ||||||
|         FloorRegistryEntry | undefined, |  | ||||||
|         AreaRegistryEntry[], |  | ||||||
|       ][] = Object.entries(floorAreaLookup) |  | ||||||
|         .map(([floorId, floorAreas]) => { |  | ||||||
|           const floor = floors.find((fl) => fl.floor_id === floorId)!; |  | ||||||
|           return [floor, floorAreas] as const; |  | ||||||
|         }) |  | ||||||
|         .sort(([floorA], [floorB]) => { |  | ||||||
|           if (floorA.level !== floorB.level) { |  | ||||||
|             return (floorA.level ?? 0) - (floorB.level ?? 0); |  | ||||||
|           } |  | ||||||
|           return stringCompare(floorA.name, floorB.name); |  | ||||||
|         }); |  | ||||||
|  |  | ||||||
|       const items: FloorComboBoxItem[] = []; |  | ||||||
|  |  | ||||||
|       floorAreaEntries.forEach(([floor, floorAreas]) => { |  | ||||||
|         if (floor) { |  | ||||||
|           const floorName = computeFloorName(floor); |  | ||||||
|  |  | ||||||
|           const areaSearchLabels = floorAreas |  | ||||||
|             .map((area) => { |  | ||||||
|               const areaName = computeAreaName(area) || area.area_id; |  | ||||||
|               return [area.area_id, areaName, ...area.aliases]; |  | ||||||
|             }) |  | ||||||
|             .flat(); |  | ||||||
|  |  | ||||||
|           items.push({ |  | ||||||
|             id: this._formatValue({ id: floor.floor_id, type: "floor" }), |  | ||||||
|             type: "floor", |  | ||||||
|             primary: floorName, |  | ||||||
|             floor: floor, |  | ||||||
|             search_labels: [ |  | ||||||
|               floor.floor_id, |  | ||||||
|               floorName, |  | ||||||
|               ...floor.aliases, |  | ||||||
|               ...areaSearchLabels, |  | ||||||
|             ], |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|         items.push( |  | ||||||
|           ...floorAreas.map((area) => { |  | ||||||
|             const areaName = computeAreaName(area) || area.area_id; |  | ||||||
|             return { |  | ||||||
|               id: this._formatValue({ id: area.area_id, type: "area" }), |  | ||||||
|               type: "area" as const, |  | ||||||
|               primary: areaName, |  | ||||||
|               area: area, |  | ||||||
|               icon: area.icon || undefined, |  | ||||||
|               search_labels: [area.area_id, areaName, ...area.aliases], |  | ||||||
|             }; |  | ||||||
|           }) |  | ||||||
|         ); |  | ||||||
|       }); |  | ||||||
|  |  | ||||||
|       items.push( |  | ||||||
|         ...unassisgnedAreas.map((area) => { |  | ||||||
|           const areaName = computeAreaName(area) || area.area_id; |  | ||||||
|           return { |  | ||||||
|             id: this._formatValue({ id: area.area_id, type: "area" }), |  | ||||||
|             type: "area" as const, |  | ||||||
|             primary: areaName, |  | ||||||
|             icon: area.icon || undefined, |  | ||||||
|             search_labels: [area.area_id, areaName, ...area.aliases], |  | ||||||
|           }; |  | ||||||
|         }) |  | ||||||
|       ); |  | ||||||
|  |  | ||||||
|       return items; |  | ||||||
|     } |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = ( |   private _rowRenderer: ComboBoxLitRenderer<FloorComboBoxItem> = ( | ||||||
|     item, |     item, | ||||||
|     { index }, |     { index }, | ||||||
| @@ -445,12 +188,16 @@ export class HaAreaFloorPicker extends LitElement { | |||||||
|     `; |     `; | ||||||
|   }; |   }; | ||||||
|  |  | ||||||
|  |   private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors); | ||||||
|  |  | ||||||
|   private _getItems = () => |   private _getItems = () => | ||||||
|     this._getAreasAndFloors( |     this._getAreasAndFloorsMemoized( | ||||||
|  |       this.hass.states, | ||||||
|       this.hass.floors, |       this.hass.floors, | ||||||
|       this.hass.areas, |       this.hass.areas, | ||||||
|       this.hass.devices, |       this.hass.devices, | ||||||
|       this.hass.entities, |       this.hass.entities, | ||||||
|  |       this._formatValue, | ||||||
|       this.includeDomains, |       this.includeDomains, | ||||||
|       this.excludeDomains, |       this.excludeDomains, | ||||||
|       this.includeDeviceClasses, |       this.includeDeviceClasses, | ||||||
|   | |||||||
| @@ -107,7 +107,7 @@ export class HaAreaPicker extends LitElement { | |||||||
|           `; |           `; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         const { floor } = getAreaContext(area, this.hass); |         const { floor } = getAreaContext(area, this.hass.floors); | ||||||
|  |  | ||||||
|         const areaName = area ? computeAreaName(area) : undefined; |         const areaName = area ? computeAreaName(area) : undefined; | ||||||
|         const floorName = floor ? computeFloorName(floor) : undefined; |         const floorName = floor ? computeFloorName(floor) : undefined; | ||||||
| @@ -279,7 +279,7 @@ export class HaAreaPicker extends LitElement { | |||||||
|       } |       } | ||||||
|  |  | ||||||
|       const items = outputAreas.map<PickerComboBoxItem>((area) => { |       const items = outputAreas.map<PickerComboBoxItem>((area) => { | ||||||
|         const { floor } = getAreaContext(area, this.hass); |         const { floor } = getAreaContext(area, this.hass.floors); | ||||||
|         const floorName = floor ? computeFloorName(floor) : undefined; |         const floorName = floor ? computeFloorName(floor) : undefined; | ||||||
|         const areaName = computeAreaName(area); |         const areaName = computeAreaName(area); | ||||||
|         return { |         return { | ||||||
|   | |||||||
| @@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement { | |||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const items: DisplayItem[] = areas.map((area) => { |     const items: DisplayItem[] = areas.map((area) => { | ||||||
|       const { floor } = getAreaContext(area, this.hass!); |       const { floor } = getAreaContext(area, this.hass.floors); | ||||||
|       return { |       return { | ||||||
|         value: area.area_id, |         value: area.area_id, | ||||||
|         label: area.name, |         label: area.name, | ||||||
|   | |||||||
| @@ -138,7 +138,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement { | |||||||
|       ); |       ); | ||||||
|       const groupedItems: Record<string, DisplayItem[]> = areas.reduce( |       const groupedItems: Record<string, DisplayItem[]> = areas.reduce( | ||||||
|         (acc, area) => { |         (acc, area) => { | ||||||
|           const { floor } = getAreaContext(area, this.hass!); |           const { floor } = getAreaContext(area, this.hass.floors); | ||||||
|           const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR; |           const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR; | ||||||
|  |  | ||||||
|           if (!acc[floorId]) { |           if (!acc[floorId]) { | ||||||
|   | |||||||
| @@ -1,5 +1,5 @@ | |||||||
| import { css, html, LitElement, type PropertyValues } from "lit"; |  | ||||||
| import "@home-assistant/webawesome/dist/components/drawer/drawer"; | 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, state } from "lit/decorators"; | ||||||
|  |  | ||||||
| export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; | export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; | ||||||
| @@ -8,6 +8,9 @@ export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; | |||||||
| export class HaBottomSheet extends LitElement { | export class HaBottomSheet extends LitElement { | ||||||
|   @property({ type: Boolean }) public open = false; |   @property({ type: Boolean }) public open = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, reflect: true, attribute: "flexcontent" }) | ||||||
|  |   public flexContent = false; | ||||||
|  |  | ||||||
|   @state() private _drawerOpen = false; |   @state() private _drawerOpen = false; | ||||||
|  |  | ||||||
|   private _handleAfterHide() { |   private _handleAfterHide() { | ||||||
| @@ -41,16 +44,19 @@ export class HaBottomSheet extends LitElement { | |||||||
|  |  | ||||||
|   static styles = css` |   static styles = css` | ||||||
|     wa-drawer { |     wa-drawer { | ||||||
|       --wa-color-surface-raised: var( |       --wa-color-surface-raised: transparent; | ||||||
|         --ha-bottom-sheet-surface-background, |  | ||||||
|         var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)), |  | ||||||
|       ); |  | ||||||
|       --spacing: 0; |       --spacing: 0; | ||||||
|       --size: auto; |       --size: var(--ha-bottom-sheet-height, auto); | ||||||
|       --show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms; |       --show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms; | ||||||
|       --hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms; |       --hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms; | ||||||
|     } |     } | ||||||
|     wa-drawer::part(dialog) { |     wa-drawer::part(dialog) { | ||||||
|  |       max-height: var(--ha-bottom-sheet-max-height, 90vh); | ||||||
|  |       align-items: center; | ||||||
|  |     } | ||||||
|  |     wa-drawer::part(body) { | ||||||
|  |       max-width: var(--ha-bottom-sheet-max-width); | ||||||
|  |       width: 100%; | ||||||
|       border-top-left-radius: var( |       border-top-left-radius: var( | ||||||
|         --ha-bottom-sheet-border-radius, |         --ha-bottom-sheet-border-radius, | ||||||
|         var(--ha-dialog-border-radius, var(--ha-border-radius-2xl)) |         var(--ha-dialog-border-radius, var(--ha-border-radius-2xl)) | ||||||
| @@ -59,10 +65,19 @@ export class HaBottomSheet extends LitElement { | |||||||
|         --ha-bottom-sheet-border-radius, |         --ha-bottom-sheet-border-radius, | ||||||
|         var(--ha-dialog-border-radius, var(--ha-border-radius-2xl)) |         var(--ha-dialog-border-radius, var(--ha-border-radius-2xl)) | ||||||
|       ); |       ); | ||||||
|       max-height: 90vh; |       background-color: var( | ||||||
|       padding-bottom: var(--safe-area-inset-bottom); |         --ha-bottom-sheet-surface-background, | ||||||
|       padding-left: var(--safe-area-inset-left); |         var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)), | ||||||
|       padding-right: var(--safe-area-inset-right); |       ); | ||||||
|  |       padding: var( | ||||||
|  |         --ha-bottom-sheet-padding, | ||||||
|  |         0 var(--safe-area-inset-right) var(--safe-area-inset-bottom) | ||||||
|  |           var(--safe-area-inset-left) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     :host([flexcontent]) wa-drawer::part(body) { | ||||||
|  |       display: flex; | ||||||
|     } |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|   | |||||||
| @@ -86,7 +86,8 @@ export class HaCameraStream extends LitElement { | |||||||
|     const streams = this._streams( |     const streams = this._streams( | ||||||
|       this._capabilities?.frontend_stream_types, |       this._capabilities?.frontend_stream_types, | ||||||
|       this._hlsStreams, |       this._hlsStreams, | ||||||
|       this._webRtcStreams |       this._webRtcStreams, | ||||||
|  |       this.muted | ||||||
|     ); |     ); | ||||||
|     return html`${repeat( |     return html`${repeat( | ||||||
|       streams, |       streams, | ||||||
| @@ -190,7 +191,8 @@ export class HaCameraStream extends LitElement { | |||||||
|     ( |     ( | ||||||
|       supportedTypes?: StreamType[], |       supportedTypes?: StreamType[], | ||||||
|       hlsStreams?: { hasAudio: boolean; hasVideo: boolean }, |       hlsStreams?: { hasAudio: boolean; hasVideo: boolean }, | ||||||
|       webRtcStreams?: { hasAudio: boolean; hasVideo: boolean } |       webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }, | ||||||
|  |       muted?: boolean | ||||||
|     ): Stream[] => { |     ): Stream[] => { | ||||||
|       if (__DEMO__) { |       if (__DEMO__) { | ||||||
|         return [{ type: MJPEG_STREAM, visible: true }]; |         return [{ type: MJPEG_STREAM, visible: true }]; | ||||||
| @@ -220,9 +222,10 @@ export class HaCameraStream extends LitElement { | |||||||
|         if ( |         if ( | ||||||
|           hlsStreams.hasVideo && |           hlsStreams.hasVideo && | ||||||
|           hlsStreams.hasAudio && |           hlsStreams.hasAudio && | ||||||
|           !webRtcStreams.hasAudio |           !webRtcStreams.hasAudio && | ||||||
|  |           !muted | ||||||
|         ) { |         ) { | ||||||
|           // webRTC stream is missing audio, use HLS |           // webRTC stream is missing audio and audio is not muted, use HLS | ||||||
|           return [{ type: STREAM_TYPE_HLS, visible: true }]; |           return [{ type: STREAM_TYPE_HLS, visible: true }]; | ||||||
|         } |         } | ||||||
|         if (webRtcStreams.hasVideo) { |         if (webRtcStreams.hasVideo) { | ||||||
|   | |||||||
| @@ -49,6 +49,7 @@ export class HaExpansionPanel extends LitElement { | |||||||
|           tabindex=${this.noCollapse ? -1 : 0} |           tabindex=${this.noCollapse ? -1 : 0} | ||||||
|           aria-expanded=${this.expanded} |           aria-expanded=${this.expanded} | ||||||
|           aria-controls="sect1" |           aria-controls="sect1" | ||||||
|  |           part="summary" | ||||||
|         > |         > | ||||||
|           ${this.leftChevron ? chevronIcon : nothing} |           ${this.leftChevron ? chevronIcon : nothing} | ||||||
|           <slot name="leading-icon"></slot> |           <slot name="leading-icon"></slot> | ||||||
| @@ -170,6 +171,11 @@ export class HaExpansionPanel extends LitElement { | |||||||
|       margin-left: 8px; |       margin-left: 8px; | ||||||
|       margin-inline-start: 8px; |       margin-inline-start: 8px; | ||||||
|       margin-inline-end: initial; |       margin-inline-end: initial; | ||||||
|  |       border-radius: var(--ha-border-radius-circle); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     #summary:focus-visible ha-svg-icon.summary-icon { | ||||||
|  |       background-color: var(--ha-color-fill-neutral-normal-active); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     :host([left-chevron]) .summary-icon, |     :host([left-chevron]) .summary-icon, | ||||||
|   | |||||||
| @@ -79,6 +79,7 @@ export class HaGenericPicker extends LitElement { | |||||||
|         ${!this._opened |         ${!this._opened | ||||||
|           ? html` |           ? html` | ||||||
|               <ha-picker-field |               <ha-picker-field | ||||||
|  |                 id="picker" | ||||||
|                 type="button" |                 type="button" | ||||||
|                 compact |                 compact | ||||||
|                 aria-label=${ifDefined(this.label)} |                 aria-label=${ifDefined(this.label)} | ||||||
|   | |||||||
| @@ -5,16 +5,10 @@ import { LitElement, html } from "lit"; | |||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { fireEvent } from "../common/dom/fire_event"; | import { fireEvent } from "../common/dom/fire_event"; | ||||||
| import { computeDomain } from "../common/entity/compute_domain"; |  | ||||||
| import type { |  | ||||||
|   DeviceEntityDisplayLookup, |  | ||||||
|   DeviceRegistryEntry, |  | ||||||
| } from "../data/device_registry"; |  | ||||||
| import { getDeviceEntityDisplayLookup } from "../data/device_registry"; |  | ||||||
| import type { EntityRegistryDisplayEntry } from "../data/entity_registry"; |  | ||||||
| import type { LabelRegistryEntry } from "../data/label_registry"; | import type { LabelRegistryEntry } from "../data/label_registry"; | ||||||
| import { | import { | ||||||
|   createLabelRegistryEntry, |   createLabelRegistryEntry, | ||||||
|  |   getLabels, | ||||||
|   subscribeLabelRegistry, |   subscribeLabelRegistry, | ||||||
| } from "../data/label_registry"; | } from "../data/label_registry"; | ||||||
| import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||||
| @@ -137,201 +131,22 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { | |||||||
|       } |       } | ||||||
|   ); |   ); | ||||||
|  |  | ||||||
|   private _getLabels = memoizeOne( |   private _getLabelsMemoized = memoizeOne(getLabels); | ||||||
|     ( |  | ||||||
|       labels: LabelRegistryEntry[] | undefined, |  | ||||||
|       haAreas: HomeAssistant["areas"], |  | ||||||
|       haDevices: HomeAssistant["devices"], |  | ||||||
|       haEntities: HomeAssistant["entities"], |  | ||||||
|       includeDomains: this["includeDomains"], |  | ||||||
|       excludeDomains: this["excludeDomains"], |  | ||||||
|       includeDeviceClasses: this["includeDeviceClasses"], |  | ||||||
|       deviceFilter: this["deviceFilter"], |  | ||||||
|       entityFilter: this["entityFilter"], |  | ||||||
|       excludeLabels: this["excludeLabels"] |  | ||||||
|     ): PickerComboBoxItem[] => { |  | ||||||
|       if (!labels || labels.length === 0) { |  | ||||||
|         return [ |  | ||||||
|           { |  | ||||||
|             id: NO_LABELS, |  | ||||||
|             primary: this.hass.localize("ui.components.label-picker.no_labels"), |  | ||||||
|             icon_path: mdiLabel, |  | ||||||
|           }, |  | ||||||
|         ]; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const devices = Object.values(haDevices); |   private _getItems = () => { | ||||||
|       const entities = Object.values(haEntities); |     if (!this._labels || this._labels.length === 0) { | ||||||
|  |       return [ | ||||||
|       let deviceEntityLookup: DeviceEntityDisplayLookup = {}; |         { | ||||||
|       let inputDevices: DeviceRegistryEntry[] | undefined; |           id: NO_LABELS, | ||||||
|       let inputEntities: EntityRegistryDisplayEntry[] | undefined; |           primary: this.hass.localize("ui.components.label-picker.no_labels"), | ||||||
|  |           icon_path: mdiLabel, | ||||||
|       if ( |         }, | ||||||
|         includeDomains || |       ]; | ||||||
|         excludeDomains || |  | ||||||
|         includeDeviceClasses || |  | ||||||
|         deviceFilter || |  | ||||||
|         entityFilter |  | ||||||
|       ) { |  | ||||||
|         deviceEntityLookup = getDeviceEntityDisplayLookup(entities); |  | ||||||
|         inputDevices = devices; |  | ||||||
|         inputEntities = entities.filter((entity) => entity.labels.length > 0); |  | ||||||
|  |  | ||||||
|         if (includeDomains) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => |  | ||||||
|               includeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => |  | ||||||
|             includeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (excludeDomains) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return true; |  | ||||||
|             } |  | ||||||
|             return entities.every( |  | ||||||
|               (entity) => |  | ||||||
|                 !excludeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter( |  | ||||||
|             (entity) => |  | ||||||
|               !excludeDomains.includes(computeDomain(entity.entity_id)) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (includeDeviceClasses) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => { |  | ||||||
|               const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|               if (!stateObj) { |  | ||||||
|                 return false; |  | ||||||
|               } |  | ||||||
|               return ( |  | ||||||
|                 stateObj.attributes.device_class && |  | ||||||
|                 includeDeviceClasses.includes(stateObj.attributes.device_class) |  | ||||||
|               ); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             return ( |  | ||||||
|               stateObj.attributes.device_class && |  | ||||||
|               includeDeviceClasses.includes(stateObj.attributes.device_class) |  | ||||||
|             ); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (deviceFilter) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => |  | ||||||
|             deviceFilter!(device) |  | ||||||
|           ); |  | ||||||
|         } |  | ||||||
|  |  | ||||||
|         if (entityFilter) { |  | ||||||
|           inputDevices = inputDevices!.filter((device) => { |  | ||||||
|             const devEntities = deviceEntityLookup[device.id]; |  | ||||||
|             if (!devEntities || !devEntities.length) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return deviceEntityLookup[device.id].some((entity) => { |  | ||||||
|               const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|               if (!stateObj) { |  | ||||||
|                 return false; |  | ||||||
|               } |  | ||||||
|               return entityFilter(stateObj); |  | ||||||
|             }); |  | ||||||
|           }); |  | ||||||
|           inputEntities = inputEntities!.filter((entity) => { |  | ||||||
|             const stateObj = this.hass.states[entity.entity_id]; |  | ||||||
|             if (!stateObj) { |  | ||||||
|               return false; |  | ||||||
|             } |  | ||||||
|             return entityFilter!(stateObj); |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       let outputLabels = labels; |  | ||||||
|       const usedLabels = new Set<string>(); |  | ||||||
|  |  | ||||||
|       let areaIds: string[] | undefined; |  | ||||||
|  |  | ||||||
|       if (inputDevices) { |  | ||||||
|         areaIds = inputDevices |  | ||||||
|           .filter((device) => device.area_id) |  | ||||||
|           .map((device) => device.area_id!); |  | ||||||
|  |  | ||||||
|         inputDevices.forEach((device) => { |  | ||||||
|           device.labels.forEach((label) => usedLabels.add(label)); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (inputEntities) { |  | ||||||
|         areaIds = (areaIds ?? []).concat( |  | ||||||
|           inputEntities |  | ||||||
|             .filter((entity) => entity.area_id) |  | ||||||
|             .map((entity) => entity.area_id!) |  | ||||||
|         ); |  | ||||||
|         inputEntities.forEach((entity) => { |  | ||||||
|           entity.labels.forEach((label) => usedLabels.add(label)); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (areaIds) { |  | ||||||
|         areaIds.forEach((areaId) => { |  | ||||||
|           const area = haAreas[areaId]; |  | ||||||
|           area.labels.forEach((label) => usedLabels.add(label)); |  | ||||||
|         }); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (excludeLabels) { |  | ||||||
|         outputLabels = outputLabels.filter( |  | ||||||
|           (label) => !excludeLabels!.includes(label.label_id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (inputDevices || inputEntities) { |  | ||||||
|         outputLabels = outputLabels.filter((label) => |  | ||||||
|           usedLabels.has(label.label_id) |  | ||||||
|         ); |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       const items = outputLabels.map<PickerComboBoxItem>((label) => ({ |  | ||||||
|         id: label.label_id, |  | ||||||
|         primary: label.name, |  | ||||||
|         icon: label.icon || undefined, |  | ||||||
|         icon_path: label.icon ? undefined : mdiLabel, |  | ||||||
|         sorting_label: label.name, |  | ||||||
|         search_labels: [label.name, label.label_id, label.description].filter( |  | ||||||
|           (v): v is string => Boolean(v) |  | ||||||
|         ), |  | ||||||
|       })); |  | ||||||
|  |  | ||||||
|       return items; |  | ||||||
|     } |     } | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   private _getItems = () => |     return this._getLabelsMemoized( | ||||||
|     this._getLabels( |       this.hass, | ||||||
|       this._labels, |       this._labels, | ||||||
|       this.hass.areas, |  | ||||||
|       this.hass.devices, |  | ||||||
|       this.hass.entities, |  | ||||||
|       this.includeDomains, |       this.includeDomains, | ||||||
|       this.excludeDomains, |       this.excludeDomains, | ||||||
|       this.includeDeviceClasses, |       this.includeDeviceClasses, | ||||||
| @@ -339,6 +154,7 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { | |||||||
|       this.entityFilter, |       this.entityFilter, | ||||||
|       this.excludeLabels |       this.excludeLabels | ||||||
|     ); |     ); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|   private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => { |   private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => { | ||||||
|     if (!labels) { |     if (!labels) { | ||||||
|   | |||||||
| @@ -107,14 +107,15 @@ export class HaMediaSelector extends LitElement { | |||||||
|         supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); |         supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); | ||||||
|  |  | ||||||
|     if (this.selector.media?.image_upload && !this.value) { |     if (this.selector.media?.image_upload && !this.value) { | ||||||
|       return html`<ha-picture-upload |       return html`${this.label ? html`<label>${this.label}</label>` : nothing} | ||||||
|         .hass=${this.hass} |         <ha-picture-upload | ||||||
|         .value=${null} |           .hass=${this.hass} | ||||||
|         .contentIdHelper=${this.selector.media?.content_id_helper} |           .value=${null} | ||||||
|         select-media |           .contentIdHelper=${this.selector.media?.content_id_helper} | ||||||
|         full-media |           select-media | ||||||
|         @media-picked=${this._pictureUploadMediaPicked} |           full-media | ||||||
|       ></ha-picture-upload>`; |           @media-picked=${this._pictureUploadMediaPicked} | ||||||
|  |         ></ha-picture-upload>`; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
| @@ -141,6 +142,7 @@ export class HaMediaSelector extends LitElement { | |||||||
|           `} |           `} | ||||||
|       ${!supportsBrowse |       ${!supportsBrowse | ||||||
|         ? html` |         ? html` | ||||||
|  |             ${this.label ? html`<label>${this.label}</label>` : nothing} | ||||||
|             <ha-alert> |             <ha-alert> | ||||||
|               ${this.hass.localize( |               ${this.hass.localize( | ||||||
|                 "ui.components.selectors.media.browse_not_supported" |                 "ui.components.selectors.media.browse_not_supported" | ||||||
| @@ -154,7 +156,8 @@ export class HaMediaSelector extends LitElement { | |||||||
|               .computeHelper=${this._computeHelperCallback} |               .computeHelper=${this._computeHelperCallback} | ||||||
|             ></ha-form> |             ></ha-form> | ||||||
|           ` |           ` | ||||||
|         : html`<ha-card |         : html`${this.label ? html`<label>${this.label}</label>` : nothing} | ||||||
|  |             <ha-card | ||||||
|               outlined |               outlined | ||||||
|               tabindex="0" |               tabindex="0" | ||||||
|               role="button" |               role="button" | ||||||
|   | |||||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -321,6 +321,10 @@ class HaWebRtcPlayer extends LitElement { | |||||||
|     if (!this._remoteStream) { |     if (!this._remoteStream) { | ||||||
|       return; |       return; | ||||||
|     } |     } | ||||||
|  |     // If the track is audio and the player is muted, we do not add it to the stream. | ||||||
|  |     if (event.track.kind === "audio" && this.muted) { | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|     this._remoteStream.addTrack(event.track); |     this._remoteStream.addTrack(event.track); | ||||||
|     if (!this.hasUpdated) { |     if (!this.hasUpdated) { | ||||||
|       await this.updateComplete; |       await this.updateComplete; | ||||||
|   | |||||||
							
								
								
									
										104
									
								
								src/components/target-picker/dialog/dialog-target-details.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										104
									
								
								src/components/target-picker/dialog/dialog-target-details.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,104 @@ | |||||||
|  | import { mdiClose } from "@mdi/js"; | ||||||
|  | import { css, html, LitElement, nothing } from "lit"; | ||||||
|  | import { customElement, property, query, state } from "lit/decorators"; | ||||||
|  | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
|  | import type { HassDialog } from "../../../dialogs/make-dialog-manager"; | ||||||
|  | import type { HomeAssistant } from "../../../types"; | ||||||
|  | import "../../ha-dialog-header"; | ||||||
|  | import "../../ha-icon-button"; | ||||||
|  | import "../../ha-icon-next"; | ||||||
|  | import "../../ha-md-dialog"; | ||||||
|  | import type { HaMdDialog } from "../../ha-md-dialog"; | ||||||
|  | import "../../ha-md-list"; | ||||||
|  | import "../../ha-md-list-item"; | ||||||
|  | import "../../ha-svg-icon"; | ||||||
|  | import "../ha-target-picker-item-row"; | ||||||
|  | import type { TargetDetailsDialogParams } from "./show-dialog-target-details"; | ||||||
|  |  | ||||||
|  | @customElement("ha-dialog-target-details") | ||||||
|  | class DialogTargetDetails extends LitElement implements HassDialog { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @state() private _params?: TargetDetailsDialogParams; | ||||||
|  |  | ||||||
|  |   @query("ha-md-dialog") private _dialog?: HaMdDialog; | ||||||
|  |  | ||||||
|  |   public showDialog(params: TargetDetailsDialogParams): void { | ||||||
|  |     this._params = params; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   public closeDialog() { | ||||||
|  |     this._dialog?.close(); | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _dialogClosed() { | ||||||
|  |     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||||
|  |     this._params = undefined; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     if (!this._params) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <ha-md-dialog open @closed=${this._dialogClosed}> | ||||||
|  |         <ha-dialog-header slot="headline"> | ||||||
|  |           <ha-icon-button | ||||||
|  |             slot="navigationIcon" | ||||||
|  |             @click=${this.closeDialog} | ||||||
|  |             .label=${this.hass.localize("ui.common.close")} | ||||||
|  |             .path=${mdiClose} | ||||||
|  |           ></ha-icon-button> | ||||||
|  |           <span slot="title" | ||||||
|  |             >${this.hass.localize( | ||||||
|  |               "ui.components.target-picker.target_details" | ||||||
|  |             )}</span | ||||||
|  |           > | ||||||
|  |           <span slot="subtitle" | ||||||
|  |             >${this.hass.localize( | ||||||
|  |               `ui.components.target-picker.type.${this._params.type}` | ||||||
|  |             )}: | ||||||
|  |             ${this._params.title}</span | ||||||
|  |           > | ||||||
|  |         </ha-dialog-header> | ||||||
|  |         <div slot="content"> | ||||||
|  |           <ha-target-picker-item-row | ||||||
|  |             .hass=${this.hass} | ||||||
|  |             .type=${this._params.type} | ||||||
|  |             .itemId=${this._params.itemId} | ||||||
|  |             .deviceFilter=${this._params.deviceFilter} | ||||||
|  |             .entityFilter=${this._params.entityFilter} | ||||||
|  |             .includeDomains=${this._params.includeDomains} | ||||||
|  |             .includeDeviceClasses=${this._params.includeDeviceClasses} | ||||||
|  |             expand | ||||||
|  |           ></ha-target-picker-item-row> | ||||||
|  |         </div> | ||||||
|  |       </ha-md-dialog> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static styles = css` | ||||||
|  |     ha-md-dialog { | ||||||
|  |       min-width: 400px; | ||||||
|  |       max-height: 90%; | ||||||
|  |       --dialog-content-padding: var(--ha-space-2) var(--ha-space-6) | ||||||
|  |         max(var(--safe-area-inset-bottom, var(--ha-space-0)), var(--ha-space-8)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     @media all and (max-width: 600px), all and (max-height: 500px) { | ||||||
|  |       ha-md-dialog { | ||||||
|  |         --md-dialog-container-shape: var(--ha-space-0); | ||||||
|  |         min-width: 100%; | ||||||
|  |         min-height: 100%; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   `; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-dialog-target-details": DialogTargetDetails; | ||||||
|  |   } | ||||||
|  | } | ||||||
| @@ -0,0 +1,28 @@ | |||||||
|  | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity"; | ||||||
|  | import type { TargetType } from "../../../data/target"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "../../device/ha-device-picker"; | ||||||
|  |  | ||||||
|  | export type NewBackupType = "automatic" | "manual"; | ||||||
|  |  | ||||||
|  | export interface TargetDetailsDialogParams { | ||||||
|  |   title: string; | ||||||
|  |   type: TargetType; | ||||||
|  |   itemId: string; | ||||||
|  |   deviceFilter?: HaDevicePickerDeviceFilterFunc; | ||||||
|  |   entityFilter?: HaEntityPickerEntityFilterFunc; | ||||||
|  |   includeDomains?: string[]; | ||||||
|  |   includeDeviceClasses?: string[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const loadTargetDetailsDialog = () => import("./dialog-target-details"); | ||||||
|  |  | ||||||
|  | export const showTargetDetailsDialog = ( | ||||||
|  |   element: HTMLElement, | ||||||
|  |   params: TargetDetailsDialogParams | ||||||
|  | ) => | ||||||
|  |   fireEvent(element, "show-dialog", { | ||||||
|  |     dialogTag: "ha-dialog-target-details", | ||||||
|  |     dialogImport: loadTargetDetailsDialog, | ||||||
|  |     dialogParams: params, | ||||||
|  |   }); | ||||||
							
								
								
									
										105
									
								
								src/components/target-picker/ha-target-picker-item-group.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								src/components/target-picker/ha-target-picker-item-group.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | |||||||
|  | import { css, html, LitElement, nothing } from "lit"; | ||||||
|  | import { customElement, property } from "lit/decorators"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; | ||||||
|  | import type { TargetType, TargetTypeFloorless } from "../../data/target"; | ||||||
|  | import type { HomeAssistant } from "../../types"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker"; | ||||||
|  | import "../ha-expansion-panel"; | ||||||
|  | import "../ha-md-list"; | ||||||
|  | import "./ha-target-picker-item-row"; | ||||||
|  |  | ||||||
|  | @customElement("ha-target-picker-item-group") | ||||||
|  | export class HaTargetPickerItemGroup extends LitElement { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property() public type!: TargetTypeFloorless; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) public items!: Partial< | ||||||
|  |     Record<TargetType, string[]> | ||||||
|  |   >; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public collapsed = false; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public deviceFilter?: HaDevicePickerDeviceFilterFunc; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public entityFilter?: HaEntityPickerEntityFilterFunc; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only targets with entities from specific domains. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-domains | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-domains" }) | ||||||
|  |   public includeDomains?: string[]; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only targets with entities of these device classes. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-device-classes | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-device-classes" }) | ||||||
|  |   public includeDeviceClasses?: string[]; | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     let count = 0; | ||||||
|  |     Object.values(this.items).forEach((items) => { | ||||||
|  |       if (items) { | ||||||
|  |         count += items.length; | ||||||
|  |       } | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     return html`<ha-expansion-panel .expanded=${!this.collapsed} left-chevron> | ||||||
|  |       <div slot="header" class="heading"> | ||||||
|  |         ${this.hass.localize( | ||||||
|  |           `ui.components.target-picker.selected.${this.type}`, | ||||||
|  |           { | ||||||
|  |             count, | ||||||
|  |           } | ||||||
|  |         )} | ||||||
|  |       </div> | ||||||
|  |       ${Object.entries(this.items).map(([type, items]) => | ||||||
|  |         items | ||||||
|  |           ? items.map( | ||||||
|  |               (item) => | ||||||
|  |                 html`<ha-target-picker-item-row | ||||||
|  |                   .hass=${this.hass} | ||||||
|  |                   .type=${type as TargetTypeFloorless} | ||||||
|  |                   .itemId=${item} | ||||||
|  |                   .deviceFilter=${this.deviceFilter} | ||||||
|  |                   .entityFilter=${this.entityFilter} | ||||||
|  |                   .includeDomains=${this.includeDomains} | ||||||
|  |                   .includeDeviceClasses=${this.includeDeviceClasses} | ||||||
|  |                 ></ha-target-picker-item-row>` | ||||||
|  |             ) | ||||||
|  |           : nothing | ||||||
|  |       )} | ||||||
|  |     </ha-expansion-panel>`; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static styles = css` | ||||||
|  |     :host { | ||||||
|  |       display: block; | ||||||
|  |       --expansion-panel-content-padding: var(--ha-space-0); | ||||||
|  |     } | ||||||
|  |     ha-expansion-panel::part(summary) { | ||||||
|  |       background-color: var(--ha-color-fill-neutral-quiet-resting); | ||||||
|  |       padding: var(--ha-space-1) var(--ha-space-2); | ||||||
|  |       font-weight: var(--ha-font-weight-bold); | ||||||
|  |       color: var(--secondary-text-color); | ||||||
|  |       display: flex; | ||||||
|  |       justify-content: space-between; | ||||||
|  |       min-height: unset; | ||||||
|  |     } | ||||||
|  |     ha-md-list { | ||||||
|  |       padding: var(--ha-space-0); | ||||||
|  |     } | ||||||
|  |   `; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-target-picker-item-group": HaTargetPickerItemGroup; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										690
									
								
								src/components/target-picker/ha-target-picker-item-row.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										690
									
								
								src/components/target-picker/ha-target-picker-item-row.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,690 @@ | |||||||
|  | import { consume } from "@lit/context"; | ||||||
|  | import { | ||||||
|  |   mdiClose, | ||||||
|  |   mdiDevices, | ||||||
|  |   mdiHome, | ||||||
|  |   mdiLabel, | ||||||
|  |   mdiTextureBox, | ||||||
|  | } from "@mdi/js"; | ||||||
|  | import { css, html, LitElement, nothing, type PropertyValues } from "lit"; | ||||||
|  | import { customElement, property, query, state } from "lit/decorators"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
|  | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  | import { computeAreaName } from "../../common/entity/compute_area_name"; | ||||||
|  | import { | ||||||
|  |   computeDeviceName, | ||||||
|  |   computeDeviceNameDisplay, | ||||||
|  | } from "../../common/entity/compute_device_name"; | ||||||
|  | import { computeDomain } from "../../common/entity/compute_domain"; | ||||||
|  | import { computeEntityName } from "../../common/entity/compute_entity_name"; | ||||||
|  | import { getEntityContext } from "../../common/entity/context/get_entity_context"; | ||||||
|  | import { computeRTL } from "../../common/util/compute_rtl"; | ||||||
|  | import { getConfigEntry } from "../../data/config_entries"; | ||||||
|  | import { labelsContext } from "../../data/context"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; | ||||||
|  | import { domainToName } from "../../data/integration"; | ||||||
|  | import type { LabelRegistryEntry } from "../../data/label_registry"; | ||||||
|  | import { | ||||||
|  |   areaMeetsFilter, | ||||||
|  |   deviceMeetsFilter, | ||||||
|  |   entityRegMeetsFilter, | ||||||
|  |   extractFromTarget, | ||||||
|  |   type ExtractFromTargetResult, | ||||||
|  |   type ExtractFromTargetResultReferenced, | ||||||
|  |   type TargetType, | ||||||
|  | } from "../../data/target"; | ||||||
|  | import { buttonLinkStyle } from "../../resources/styles"; | ||||||
|  | import type { HomeAssistant } from "../../types"; | ||||||
|  | import { brandsUrl } from "../../util/brands-url"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "../device/ha-device-picker"; | ||||||
|  | import { floorDefaultIconPath } from "../ha-floor-icon"; | ||||||
|  | import "../ha-icon-button"; | ||||||
|  | import "../ha-md-list"; | ||||||
|  | import type { HaMdList } from "../ha-md-list"; | ||||||
|  | import "../ha-md-list-item"; | ||||||
|  | import type { HaMdListItem } from "../ha-md-list-item"; | ||||||
|  | import "../ha-state-icon"; | ||||||
|  | import "../ha-svg-icon"; | ||||||
|  | import { showTargetDetailsDialog } from "./dialog/show-dialog-target-details"; | ||||||
|  |  | ||||||
|  | @customElement("ha-target-picker-item-row") | ||||||
|  | export class HaTargetPickerItemRow extends LitElement { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property({ reflect: true }) public type!: TargetType; | ||||||
|  |  | ||||||
|  |   @property({ attribute: "item-id" }) public itemId!: string; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean }) public expand = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "sub-entry", reflect: true }) | ||||||
|  |   public subEntry = false; | ||||||
|  |  | ||||||
|  |   @property({ type: Boolean, attribute: "hide-context" }) | ||||||
|  |   public hideContext = false; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public parentEntries?: ExtractFromTargetResultReferenced; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public deviceFilter?: HaDevicePickerDeviceFilterFunc; | ||||||
|  |  | ||||||
|  |   @property({ attribute: false }) | ||||||
|  |   public entityFilter?: HaEntityPickerEntityFilterFunc; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only targets with entities from specific domains. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-domains | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-domains" }) | ||||||
|  |   public includeDomains?: string[]; | ||||||
|  |  | ||||||
|  |   /** | ||||||
|  |    * Show only targets with entities of these device classes. | ||||||
|  |    * @type {Array} | ||||||
|  |    * @attr include-device-classes | ||||||
|  |    */ | ||||||
|  |   @property({ type: Array, attribute: "include-device-classes" }) | ||||||
|  |   public includeDeviceClasses?: string[]; | ||||||
|  |  | ||||||
|  |   @state() private _iconImg?: string; | ||||||
|  |  | ||||||
|  |   @state() private _domainName?: string; | ||||||
|  |  | ||||||
|  |   @state() private _entries?: ExtractFromTargetResult; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   @consume({ context: labelsContext, subscribe: true }) | ||||||
|  |   _labelRegistry!: LabelRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   @query("ha-md-list-item") public item?: HaMdListItem; | ||||||
|  |  | ||||||
|  |   @query("ha-md-list") public list?: HaMdList; | ||||||
|  |  | ||||||
|  |   @query("ha-target-picker-item-row") public itemRow?: HaTargetPickerItemRow; | ||||||
|  |  | ||||||
|  |   protected willUpdate(changedProps: PropertyValues) { | ||||||
|  |     if (!this.subEntry && changedProps.has("itemId")) { | ||||||
|  |       this._updateItemData(); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     const { name, context, iconPath, fallbackIconPath, stateObject } = | ||||||
|  |       this._itemData(this.type, this.itemId); | ||||||
|  |  | ||||||
|  |     const showDevices = ["floor", "area", "label"].includes(this.type); | ||||||
|  |     const showEntities = this.type !== "entity"; | ||||||
|  |  | ||||||
|  |     const entries = this.parentEntries || this._entries; | ||||||
|  |  | ||||||
|  |     // Don't show sub entries that have no entities | ||||||
|  |     if ( | ||||||
|  |       this.subEntry && | ||||||
|  |       this.type !== "entity" && | ||||||
|  |       (!entries || entries.referenced_entities.length === 0) | ||||||
|  |     ) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <ha-md-list-item type="text"> | ||||||
|  |         <div slot="start"> | ||||||
|  |           ${this.subEntry | ||||||
|  |             ? html` | ||||||
|  |                 <div class="horizontal-line-wrapper"> | ||||||
|  |                   <div class="horizontal-line"></div> | ||||||
|  |                 </div> | ||||||
|  |               ` | ||||||
|  |             : nothing} | ||||||
|  |           ${iconPath | ||||||
|  |             ? html`<ha-icon .icon=${iconPath}></ha-icon>` | ||||||
|  |             : this._iconImg | ||||||
|  |               ? html`<img | ||||||
|  |                   alt=${this._domainName || ""} | ||||||
|  |                   crossorigin="anonymous" | ||||||
|  |                   referrerpolicy="no-referrer" | ||||||
|  |                   src=${this._iconImg} | ||||||
|  |                 />` | ||||||
|  |               : fallbackIconPath | ||||||
|  |                 ? html`<ha-svg-icon .path=${fallbackIconPath}></ha-svg-icon>` | ||||||
|  |                 : stateObject | ||||||
|  |                   ? html` | ||||||
|  |                       <ha-state-icon | ||||||
|  |                         .hass=${this.hass} | ||||||
|  |                         .stateObj=${stateObject} | ||||||
|  |                       > | ||||||
|  |                       </ha-state-icon> | ||||||
|  |                     ` | ||||||
|  |                   : nothing} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|  |         <div slot="headline">${name}</div> | ||||||
|  |         ${context && !this.hideContext | ||||||
|  |           ? html`<span slot="supporting-text">${context}</span>` | ||||||
|  |           : this._domainName && this.subEntry | ||||||
|  |             ? html`<span slot="supporting-text" class="domain" | ||||||
|  |                 >${this._domainName}</span | ||||||
|  |               >` | ||||||
|  |             : nothing} | ||||||
|  |         ${!this.subEntry && | ||||||
|  |         ((entries && (showEntities || showDevices)) || this._domainName) | ||||||
|  |           ? html` | ||||||
|  |               <div slot="end" class="summary"> | ||||||
|  |                 ${showEntities && !this.expand | ||||||
|  |                   ? html`<button class="main link" @click=${this._openDetails}> | ||||||
|  |                       ${this.hass.localize( | ||||||
|  |                         "ui.components.target-picker.entities_count", | ||||||
|  |                         { | ||||||
|  |                           count: entries?.referenced_entities.length, | ||||||
|  |                         } | ||||||
|  |                       )} | ||||||
|  |                     </button>` | ||||||
|  |                   : showEntities | ||||||
|  |                     ? html`<span class="main"> | ||||||
|  |                         ${this.hass.localize( | ||||||
|  |                           "ui.components.target-picker.entities_count", | ||||||
|  |                           { | ||||||
|  |                             count: entries?.referenced_entities.length, | ||||||
|  |                           } | ||||||
|  |                         )} | ||||||
|  |                       </span>` | ||||||
|  |                     : nothing} | ||||||
|  |                 ${showDevices | ||||||
|  |                   ? html`<span class="secondary" | ||||||
|  |                       >${this.hass.localize( | ||||||
|  |                         "ui.components.target-picker.devices_count", | ||||||
|  |                         { | ||||||
|  |                           count: entries?.referenced_devices.length, | ||||||
|  |                         } | ||||||
|  |                       )}</span | ||||||
|  |                     >` | ||||||
|  |                   : nothing} | ||||||
|  |                 ${this._domainName && !showDevices | ||||||
|  |                   ? html`<span class="secondary domain" | ||||||
|  |                       >${this._domainName}</span | ||||||
|  |                     >` | ||||||
|  |                   : nothing} | ||||||
|  |               </div> | ||||||
|  |             ` | ||||||
|  |           : nothing} | ||||||
|  |         ${!this.expand && !this.subEntry | ||||||
|  |           ? html` | ||||||
|  |               <ha-icon-button | ||||||
|  |                 .path=${mdiClose} | ||||||
|  |                 slot="end" | ||||||
|  |                 @click=${this._removeItem} | ||||||
|  |               ></ha-icon-button> | ||||||
|  |             ` | ||||||
|  |           : nothing} | ||||||
|  |       </ha-md-list-item> | ||||||
|  |       ${this.expand && entries && entries.referenced_entities | ||||||
|  |         ? this._renderEntries() | ||||||
|  |         : nothing} | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _renderEntries() { | ||||||
|  |     const entries = this.parentEntries || this._entries; | ||||||
|  |  | ||||||
|  |     let nextType: TargetType = | ||||||
|  |       this.type === "floor" | ||||||
|  |         ? "area" | ||||||
|  |         : this.type === "area" | ||||||
|  |           ? "device" | ||||||
|  |           : "entity"; | ||||||
|  |  | ||||||
|  |     if (this.type === "label") { | ||||||
|  |       if (entries?.referenced_areas.length) { | ||||||
|  |         nextType = "area"; | ||||||
|  |       } else if (entries?.referenced_devices.length) { | ||||||
|  |         nextType = "device"; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const rows1 = | ||||||
|  |       (nextType === "area" | ||||||
|  |         ? entries?.referenced_areas | ||||||
|  |         : nextType === "device" | ||||||
|  |           ? entries?.referenced_devices | ||||||
|  |           : entries?.referenced_entities) || []; | ||||||
|  |  | ||||||
|  |     const devicesInAreas = [] as string[]; | ||||||
|  |  | ||||||
|  |     const rows1Entries = | ||||||
|  |       nextType === "entity" | ||||||
|  |         ? undefined | ||||||
|  |         : rows1.map((rowItem) => { | ||||||
|  |             const nextEntries = { | ||||||
|  |               referenced_areas: [] as string[], | ||||||
|  |               referenced_devices: [] as string[], | ||||||
|  |               referenced_entities: [] as string[], | ||||||
|  |             }; | ||||||
|  |  | ||||||
|  |             if (nextType === "area") { | ||||||
|  |               nextEntries.referenced_devices = | ||||||
|  |                 entries?.referenced_devices.filter( | ||||||
|  |                   (device_id) => | ||||||
|  |                     this.hass.devices?.[device_id]?.area_id === rowItem && | ||||||
|  |                     entries?.referenced_entities.some( | ||||||
|  |                       (entity_id) => | ||||||
|  |                         this.hass.entities?.[entity_id]?.device_id === device_id | ||||||
|  |                     ) | ||||||
|  |                 ) || ([] as string[]); | ||||||
|  |  | ||||||
|  |               devicesInAreas.push(...nextEntries.referenced_devices); | ||||||
|  |  | ||||||
|  |               nextEntries.referenced_entities = | ||||||
|  |                 entries?.referenced_entities.filter((entity_id) => { | ||||||
|  |                   const entity = this.hass.entities[entity_id]; | ||||||
|  |                   return ( | ||||||
|  |                     entity.area_id === rowItem || | ||||||
|  |                     !entity.device_id || | ||||||
|  |                     nextEntries.referenced_devices.includes(entity.device_id) | ||||||
|  |                   ); | ||||||
|  |                 }) || ([] as string[]); | ||||||
|  |  | ||||||
|  |               return nextEntries; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             nextEntries.referenced_entities = | ||||||
|  |               entries?.referenced_entities.filter( | ||||||
|  |                 (entity_id) => | ||||||
|  |                   this.hass.entities?.[entity_id]?.device_id === rowItem | ||||||
|  |               ) || ([] as string[]); | ||||||
|  |  | ||||||
|  |             return nextEntries; | ||||||
|  |           }); | ||||||
|  |  | ||||||
|  |     const entityRows = | ||||||
|  |       this.type === "label" && entries | ||||||
|  |         ? entries.referenced_entities.filter((entity_id) => | ||||||
|  |             this.hass.entities[entity_id].labels.includes(this.itemId) | ||||||
|  |           ) | ||||||
|  |         : nextType === "device" && entries | ||||||
|  |           ? entries.referenced_entities.filter( | ||||||
|  |               (entity_id) => | ||||||
|  |                 this.hass.entities[entity_id].area_id === this.itemId | ||||||
|  |             ) | ||||||
|  |           : []; | ||||||
|  |  | ||||||
|  |     const deviceRows = | ||||||
|  |       this.type === "label" && entries | ||||||
|  |         ? entries.referenced_devices.filter( | ||||||
|  |             (device_id) => | ||||||
|  |               !devicesInAreas.includes(device_id) && | ||||||
|  |               this.hass.devices[device_id].labels.includes(this.itemId) | ||||||
|  |           ) | ||||||
|  |         : []; | ||||||
|  |  | ||||||
|  |     const deviceRowsEntries = | ||||||
|  |       deviceRows.length === 0 | ||||||
|  |         ? undefined | ||||||
|  |         : deviceRows.map((device_id) => ({ | ||||||
|  |             referenced_areas: [] as string[], | ||||||
|  |             referenced_devices: [] as string[], | ||||||
|  |             referenced_entities: | ||||||
|  |               entries?.referenced_entities.filter( | ||||||
|  |                 (entity_id) => | ||||||
|  |                   this.hass.entities?.[entity_id]?.device_id === device_id | ||||||
|  |               ) || ([] as string[]), | ||||||
|  |           })); | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div class="entries-tree"> | ||||||
|  |         <div class="line-wrapper"> | ||||||
|  |           <div class="line"></div> | ||||||
|  |         </div> | ||||||
|  |         <ha-md-list class="entries"> | ||||||
|  |           ${rows1.map( | ||||||
|  |             (itemId, index) => html` | ||||||
|  |               <ha-target-picker-item-row | ||||||
|  |                 sub-entry | ||||||
|  |                 .hass=${this.hass} | ||||||
|  |                 .type=${nextType} | ||||||
|  |                 .itemId=${itemId} | ||||||
|  |                 .parentEntries=${rows1Entries?.[index]} | ||||||
|  |                 .hideContext=${this.hideContext || this.type !== "label"} | ||||||
|  |                 expand | ||||||
|  |               ></ha-target-picker-item-row> | ||||||
|  |             ` | ||||||
|  |           )} | ||||||
|  |           ${deviceRows.map( | ||||||
|  |             (itemId, index) => html` | ||||||
|  |               <ha-target-picker-item-row | ||||||
|  |                 sub-entry | ||||||
|  |                 .hass=${this.hass} | ||||||
|  |                 type="device" | ||||||
|  |                 .itemId=${itemId} | ||||||
|  |                 .parentEntries=${deviceRowsEntries?.[index]} | ||||||
|  |                 .hideContext=${this.hideContext || this.type !== "label"} | ||||||
|  |                 expand | ||||||
|  |               ></ha-target-picker-item-row> | ||||||
|  |             ` | ||||||
|  |           )} | ||||||
|  |           ${entityRows.map( | ||||||
|  |             (itemId) => html` | ||||||
|  |               <ha-target-picker-item-row | ||||||
|  |                 sub-entry | ||||||
|  |                 .hass=${this.hass} | ||||||
|  |                 type="entity" | ||||||
|  |                 .itemId=${itemId} | ||||||
|  |                 .hideContext=${this.hideContext || this.type !== "label"} | ||||||
|  |               ></ha-target-picker-item-row> | ||||||
|  |             ` | ||||||
|  |           )} | ||||||
|  |         </ha-md-list> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _updateItemData() { | ||||||
|  |     if (this.type === "entity") { | ||||||
|  |       this._entries = undefined; | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     try { | ||||||
|  |       const entries = await extractFromTarget(this.hass, { | ||||||
|  |         [`${this.type}_id`]: [this.itemId], | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       const hiddenAreaIds: string[] = []; | ||||||
|  |       if (this.type === "floor" || this.type === "label") { | ||||||
|  |         entries.referenced_areas = entries.referenced_areas.filter( | ||||||
|  |           (area_id) => { | ||||||
|  |             const area = this.hass.areas[area_id]; | ||||||
|  |             if ( | ||||||
|  |               (this.type === "floor" || area.labels.includes(this.itemId)) && | ||||||
|  |               areaMeetsFilter( | ||||||
|  |                 area, | ||||||
|  |                 this.hass.devices, | ||||||
|  |                 this.hass.entities, | ||||||
|  |                 this.deviceFilter, | ||||||
|  |                 this.includeDomains, | ||||||
|  |                 this.includeDeviceClasses, | ||||||
|  |                 this.hass.states, | ||||||
|  |                 this.entityFilter | ||||||
|  |               ) | ||||||
|  |             ) { | ||||||
|  |               return true; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             hiddenAreaIds.push(area_id); | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       const hiddenDeviceIds: string[] = []; | ||||||
|  |       if ( | ||||||
|  |         this.type === "floor" || | ||||||
|  |         this.type === "area" || | ||||||
|  |         this.type === "label" | ||||||
|  |       ) { | ||||||
|  |         entries.referenced_devices = entries.referenced_devices.filter( | ||||||
|  |           (device_id) => { | ||||||
|  |             const device = this.hass.devices[device_id]; | ||||||
|  |             if ( | ||||||
|  |               !hiddenAreaIds.includes(device.area_id || "") && | ||||||
|  |               (this.type !== "label" || device.labels.includes(this.itemId)) && | ||||||
|  |               deviceMeetsFilter( | ||||||
|  |                 device, | ||||||
|  |                 this.hass.entities, | ||||||
|  |                 this.deviceFilter, | ||||||
|  |                 this.includeDomains, | ||||||
|  |                 this.includeDeviceClasses, | ||||||
|  |                 this.hass.states, | ||||||
|  |                 this.entityFilter | ||||||
|  |               ) | ||||||
|  |             ) { | ||||||
|  |               return true; | ||||||
|  |             } | ||||||
|  |  | ||||||
|  |             hiddenDeviceIds.push(device_id); | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |         ); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       entries.referenced_entities = entries.referenced_entities.filter( | ||||||
|  |         (entity_id) => { | ||||||
|  |           const entity = this.hass.entities[entity_id]; | ||||||
|  |           if (hiddenDeviceIds.includes(entity.device_id || "")) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |           if ( | ||||||
|  |             (this.type === "area" && entity.area_id === this.itemId) || | ||||||
|  |             (this.type === "label" && entity.labels.includes(this.itemId)) || | ||||||
|  |             entries.referenced_devices.includes(entity.device_id || "") | ||||||
|  |           ) { | ||||||
|  |             return entityRegMeetsFilter( | ||||||
|  |               entity, | ||||||
|  |               this.type === "label", | ||||||
|  |               this.includeDomains, | ||||||
|  |               this.includeDeviceClasses, | ||||||
|  |               this.hass.states, | ||||||
|  |               this.entityFilter | ||||||
|  |             ); | ||||||
|  |           } | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |       ); | ||||||
|  |  | ||||||
|  |       this._entries = entries; | ||||||
|  |     } catch (e) { | ||||||
|  |       // eslint-disable-next-line no-console | ||||||
|  |       console.error("Failed to extract target", e); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _itemData = memoizeOne((type: TargetType, item: string) => { | ||||||
|  |     if (type === "floor") { | ||||||
|  |       const floor = this.hass.floors?.[item]; | ||||||
|  |       return { | ||||||
|  |         name: floor?.name || item, | ||||||
|  |         iconPath: floor?.icon, | ||||||
|  |         fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (type === "area") { | ||||||
|  |       const area = this.hass.areas?.[item]; | ||||||
|  |       return { | ||||||
|  |         name: area?.name || item, | ||||||
|  |         context: area.floor_id && this.hass.floors?.[area.floor_id]?.name, | ||||||
|  |         iconPath: area?.icon, | ||||||
|  |         fallbackIconPath: mdiTextureBox, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (type === "device") { | ||||||
|  |       const device = this.hass.devices?.[item]; | ||||||
|  |  | ||||||
|  |       if (device.primary_config_entry) { | ||||||
|  |         this._getDeviceDomain(device.primary_config_entry); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         name: device ? computeDeviceNameDisplay(device, this.hass) : item, | ||||||
|  |         context: device?.area_id && this.hass.areas?.[device.area_id]?.name, | ||||||
|  |         fallbackIconPath: mdiDevices, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (type === "entity") { | ||||||
|  |       this._setDomainName(computeDomain(item)); | ||||||
|  |  | ||||||
|  |       const stateObject = this.hass.states[item]; | ||||||
|  |       const entityName = computeEntityName( | ||||||
|  |         stateObject, | ||||||
|  |         this.hass.entities, | ||||||
|  |         this.hass.devices | ||||||
|  |       ); | ||||||
|  |       const { area, device } = getEntityContext( | ||||||
|  |         stateObject, | ||||||
|  |         this.hass.entities, | ||||||
|  |         this.hass.devices, | ||||||
|  |         this.hass.areas, | ||||||
|  |         this.hass.floors | ||||||
|  |       ); | ||||||
|  |       const deviceName = device ? computeDeviceName(device) : undefined; | ||||||
|  |       const areaName = area ? computeAreaName(area) : undefined; | ||||||
|  |       const context = [areaName, entityName ? deviceName : undefined] | ||||||
|  |         .filter(Boolean) | ||||||
|  |         .join(computeRTL(this.hass) ? " ◂ " : " ▸ "); | ||||||
|  |       return { | ||||||
|  |         name: entityName || deviceName || item, | ||||||
|  |         context, | ||||||
|  |         stateObject, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // type label | ||||||
|  |     const label = this._labelRegistry.find((lab) => lab.label_id === item); | ||||||
|  |     return { | ||||||
|  |       name: label?.name || item, | ||||||
|  |       iconPath: label?.icon, | ||||||
|  |       fallbackIconPath: mdiLabel, | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   private _setDomainName(domain: string) { | ||||||
|  |     this._domainName = domainToName(this.hass.localize, domain); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _removeItem(ev) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     fireEvent(this, "remove-target-item", { | ||||||
|  |       type: this.type, | ||||||
|  |       id: this.itemId, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _getDeviceDomain(configEntryId: string) { | ||||||
|  |     try { | ||||||
|  |       const data = await getConfigEntry(this.hass, configEntryId); | ||||||
|  |       const domain = data.config_entry.domain; | ||||||
|  |       this._iconImg = brandsUrl({ | ||||||
|  |         domain: domain, | ||||||
|  |         type: "icon", | ||||||
|  |         darkOptimized: this.hass.themes?.darkMode, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       this._setDomainName(domain); | ||||||
|  |     } catch { | ||||||
|  |       // failed to load config entry -> ignore | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _openDetails() { | ||||||
|  |     showTargetDetailsDialog(this, { | ||||||
|  |       title: this._itemData(this.type, this.itemId).name, | ||||||
|  |       type: this.type, | ||||||
|  |       itemId: this.itemId, | ||||||
|  |       deviceFilter: this.deviceFilter, | ||||||
|  |       entityFilter: this.entityFilter, | ||||||
|  |       includeDomains: this.includeDomains, | ||||||
|  |       includeDeviceClasses: this.includeDeviceClasses, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static styles = [ | ||||||
|  |     buttonLinkStyle, | ||||||
|  |     css` | ||||||
|  |       :host { | ||||||
|  |         --md-list-item-top-space: var(--ha-space-0); | ||||||
|  |         --md-list-item-bottom-space: var(--ha-space-0); | ||||||
|  |         --md-list-item-leading-space: var(--ha-space-2); | ||||||
|  |         --md-list-item-trailing-space: var(--ha-space-2); | ||||||
|  |         --md-list-item-two-line-container-height: 56px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       :host([expand]:not([sub-entry])) ha-md-list-item { | ||||||
|  |         border: 2px solid var(--ha-color-border-neutral-loud); | ||||||
|  |         background-color: var(--ha-color-fill-neutral-quiet-resting); | ||||||
|  |         border-radius: var(--ha-card-border-radius, var(--ha-border-radius-lg)); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       state-badge { | ||||||
|  |         color: var(--ha-color-on-neutral-quiet); | ||||||
|  |       } | ||||||
|  |       img { | ||||||
|  |         width: 24px; | ||||||
|  |         height: 24px; | ||||||
|  |       } | ||||||
|  |       ha-icon-button { | ||||||
|  |         --mdc-icon-button-size: 32px; | ||||||
|  |       } | ||||||
|  |       .summary { | ||||||
|  |         display: flex; | ||||||
|  |         flex-direction: column; | ||||||
|  |         align-items: flex-end; | ||||||
|  |         line-height: var(--ha-line-height-condensed); | ||||||
|  |       } | ||||||
|  |       :host([sub-entry]) .summary { | ||||||
|  |         margin-right: var(--ha-space-12); | ||||||
|  |       } | ||||||
|  |       .summary .main { | ||||||
|  |         font-weight: var(--ha-font-weight-medium); | ||||||
|  |       } | ||||||
|  |       .summary .secondary { | ||||||
|  |         font-size: var(--ha-font-size-s); | ||||||
|  |         color: var(--secondary-text-color); | ||||||
|  |       } | ||||||
|  |       .domain { | ||||||
|  |         font-family: var(--ha-font-family-code); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .entries-tree { | ||||||
|  |         display: flex; | ||||||
|  |         position: relative; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .entries-tree .line-wrapper { | ||||||
|  |         padding: var(--ha-space-5); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .entries-tree .line-wrapper .line { | ||||||
|  |         border-left: 2px dashed var(--divider-color); | ||||||
|  |         height: calc(100% - 28px); | ||||||
|  |         position: absolute; | ||||||
|  |         top: 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       :host([sub-entry]) .entries-tree .line-wrapper .line { | ||||||
|  |         height: calc(100% - 12px); | ||||||
|  |         top: -18px; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .entries { | ||||||
|  |         padding: 0; | ||||||
|  |         --md-item-overflow: visible; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       .horizontal-line-wrapper { | ||||||
|  |         position: relative; | ||||||
|  |       } | ||||||
|  |       .horizontal-line-wrapper .horizontal-line { | ||||||
|  |         position: absolute; | ||||||
|  |         top: 11px; | ||||||
|  |         margin-inline-start: -28px; | ||||||
|  |         width: 29px; | ||||||
|  |         border-top: 2px dashed var(--divider-color); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       button.link { | ||||||
|  |         text-decoration: none; | ||||||
|  |         color: var(--primary-color); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       button.link:hover, | ||||||
|  |       button.link:focus { | ||||||
|  |         text-decoration: underline; | ||||||
|  |       } | ||||||
|  |     `, | ||||||
|  |   ]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-target-picker-item-row": HaTargetPickerItemRow; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										1107
									
								
								src/components/target-picker/ha-target-picker-selector.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1107
									
								
								src/components/target-picker/ha-target-picker-selector.ts
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										354
									
								
								src/components/target-picker/ha-target-picker-value-chip.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										354
									
								
								src/components/target-picker/ha-target-picker-value-chip.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,354 @@ | |||||||
|  | import { consume } from "@lit/context"; | ||||||
|  | // @ts-ignore | ||||||
|  | import chipStyles from "@material/chips/dist/mdc.chips.min.css"; | ||||||
|  | import { | ||||||
|  |   mdiClose, | ||||||
|  |   mdiDevices, | ||||||
|  |   mdiHome, | ||||||
|  |   mdiLabel, | ||||||
|  |   mdiTextureBox, | ||||||
|  |   mdiUnfoldMoreVertical, | ||||||
|  | } from "@mdi/js"; | ||||||
|  | import { css, html, LitElement, nothing, unsafeCSS } from "lit"; | ||||||
|  | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import { classMap } from "lit/directives/class-map"; | ||||||
|  | import memoizeOne from "memoize-one"; | ||||||
|  | import { computeCssColor } from "../../common/color/compute-color"; | ||||||
|  | import { hex2rgb } from "../../common/color/convert-color"; | ||||||
|  | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
|  | import { | ||||||
|  |   computeDeviceName, | ||||||
|  |   computeDeviceNameDisplay, | ||||||
|  | } from "../../common/entity/compute_device_name"; | ||||||
|  | import { computeDomain } from "../../common/entity/compute_domain"; | ||||||
|  | import { computeEntityName } from "../../common/entity/compute_entity_name"; | ||||||
|  | import { getEntityContext } from "../../common/entity/context/get_entity_context"; | ||||||
|  | import { getConfigEntry } from "../../data/config_entries"; | ||||||
|  | import { labelsContext } from "../../data/context"; | ||||||
|  | import { domainToName } from "../../data/integration"; | ||||||
|  | import type { LabelRegistryEntry } from "../../data/label_registry"; | ||||||
|  | import type { TargetType } from "../../data/target"; | ||||||
|  | import type { HomeAssistant } from "../../types"; | ||||||
|  | import { brandsUrl } from "../../util/brands-url"; | ||||||
|  | import { floorDefaultIconPath } from "../ha-floor-icon"; | ||||||
|  | import "../ha-icon"; | ||||||
|  | import "../ha-icon-button"; | ||||||
|  | import "../ha-md-list"; | ||||||
|  | import "../ha-md-list-item"; | ||||||
|  | import "../ha-state-icon"; | ||||||
|  | import "../ha-tooltip"; | ||||||
|  |  | ||||||
|  | @customElement("ha-target-picker-value-chip") | ||||||
|  | export class HaTargetPickerValueChip extends LitElement { | ||||||
|  |   @property({ attribute: false }) public hass!: HomeAssistant; | ||||||
|  |  | ||||||
|  |   @property() public type!: TargetType; | ||||||
|  |  | ||||||
|  |   @property({ attribute: "item-id" }) public itemId!: string; | ||||||
|  |  | ||||||
|  |   @state() private _domainName?: string; | ||||||
|  |  | ||||||
|  |   @state() private _iconImg?: string; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   @consume({ context: labelsContext, subscribe: true }) | ||||||
|  |   _labelRegistry!: LabelRegistryEntry[]; | ||||||
|  |  | ||||||
|  |   protected render() { | ||||||
|  |     const { name, iconPath, fallbackIconPath, stateObject, color } = | ||||||
|  |       this._itemData(this.type, this.itemId); | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <div | ||||||
|  |         class="mdc-chip ${classMap({ | ||||||
|  |           [this.type]: true, | ||||||
|  |         })}" | ||||||
|  |         style=${color | ||||||
|  |           ? `--color: rgb(${color}); --background-color: rgba(${color}, .5)` | ||||||
|  |           : ""} | ||||||
|  |       > | ||||||
|  |         ${iconPath | ||||||
|  |           ? html`<ha-icon | ||||||
|  |               class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |               .icon=${iconPath} | ||||||
|  |             ></ha-icon>` | ||||||
|  |           : this._iconImg | ||||||
|  |             ? html`<img | ||||||
|  |                 class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |                 alt=${this._domainName || ""} | ||||||
|  |                 crossorigin="anonymous" | ||||||
|  |                 referrerpolicy="no-referrer" | ||||||
|  |                 src=${this._iconImg} | ||||||
|  |               />` | ||||||
|  |             : fallbackIconPath | ||||||
|  |               ? html`<ha-svg-icon | ||||||
|  |                   class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |                   .path=${fallbackIconPath} | ||||||
|  |                 ></ha-svg-icon>` | ||||||
|  |               : stateObject | ||||||
|  |                 ? html`<ha-state-icon | ||||||
|  |                     class="mdc-chip__icon mdc-chip__icon--leading" | ||||||
|  |                     .hass=${this.hass} | ||||||
|  |                     .stateObj=${stateObject} | ||||||
|  |                   ></ha-state-icon>` | ||||||
|  |                 : nothing} | ||||||
|  |         <span role="gridcell"> | ||||||
|  |           <span role="button" tabindex="0" class="mdc-chip__primary-action"> | ||||||
|  |             <span id="title-${this.itemId}" class="mdc-chip__text" | ||||||
|  |               >${name}</span | ||||||
|  |             > | ||||||
|  |           </span> | ||||||
|  |         </span> | ||||||
|  |         ${this.type === "entity" | ||||||
|  |           ? nothing | ||||||
|  |           : html`<span role="gridcell"> | ||||||
|  |               <ha-tooltip .for="expand-${this.itemId}" | ||||||
|  |                 >${this.hass.localize( | ||||||
|  |                   `ui.components.target-picker.expand_${this.type}_id` | ||||||
|  |                 )} | ||||||
|  |               </ha-tooltip> | ||||||
|  |               <ha-icon-button | ||||||
|  |                 class="expand-btn mdc-chip__icon mdc-chip__icon--trailing" | ||||||
|  |                 .label=${this.hass.localize( | ||||||
|  |                   "ui.components.target-picker.expand" | ||||||
|  |                 )} | ||||||
|  |                 .path=${mdiUnfoldMoreVertical} | ||||||
|  |                 hide-title | ||||||
|  |                 .id="expand-${this.itemId}" | ||||||
|  |                 .type=${this.type} | ||||||
|  |                 @click=${this._handleExpand} | ||||||
|  |               ></ha-icon-button> | ||||||
|  |             </span>`} | ||||||
|  |         <span role="gridcell"> | ||||||
|  |           <ha-tooltip .for="remove-${this.itemId}"> | ||||||
|  |             ${this.hass.localize( | ||||||
|  |               `ui.components.target-picker.remove_${this.type}_id` | ||||||
|  |             )} | ||||||
|  |           </ha-tooltip> | ||||||
|  |           <ha-icon-button | ||||||
|  |             class="mdc-chip__icon mdc-chip__icon--trailing" | ||||||
|  |             .label=${this.hass.localize("ui.components.target-picker.remove")} | ||||||
|  |             .path=${mdiClose} | ||||||
|  |             hide-title | ||||||
|  |             .id="remove-${this.itemId}" | ||||||
|  |             .type=${this.type} | ||||||
|  |             @click=${this._removeItem} | ||||||
|  |           ></ha-icon-button> | ||||||
|  |         </span> | ||||||
|  |       </div> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _itemData = memoizeOne((type: TargetType, itemId: string) => { | ||||||
|  |     if (type === "floor") { | ||||||
|  |       const floor = this.hass.floors?.[itemId]; | ||||||
|  |       return { | ||||||
|  |         name: floor?.name || itemId, | ||||||
|  |         iconPath: floor?.icon, | ||||||
|  |         fallbackIconPath: floor ? floorDefaultIconPath(floor) : mdiHome, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (type === "area") { | ||||||
|  |       const area = this.hass.areas?.[itemId]; | ||||||
|  |       return { | ||||||
|  |         name: area?.name || itemId, | ||||||
|  |         iconPath: area?.icon, | ||||||
|  |         fallbackIconPath: mdiTextureBox, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (type === "device") { | ||||||
|  |       const device = this.hass.devices?.[itemId]; | ||||||
|  |  | ||||||
|  |       if (device.primary_config_entry) { | ||||||
|  |         this._getDeviceDomain(device.primary_config_entry); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         name: device ? computeDeviceNameDisplay(device, this.hass) : itemId, | ||||||
|  |         fallbackIconPath: mdiDevices, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |     if (type === "entity") { | ||||||
|  |       this._setDomainName(computeDomain(itemId)); | ||||||
|  |  | ||||||
|  |       const 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; | ||||||
|  |       return { | ||||||
|  |         name: entityName || deviceName || itemId, | ||||||
|  |         stateObject, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     // type label | ||||||
|  |     const label = this._labelRegistry.find((lab) => lab.label_id === itemId); | ||||||
|  |     let color = label?.color ? computeCssColor(label.color) : undefined; | ||||||
|  |     if (color?.startsWith("var(")) { | ||||||
|  |       const computedStyles = getComputedStyle(this); | ||||||
|  |       color = computedStyles.getPropertyValue( | ||||||
|  |         color.substring(4, color.length - 1) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |     if (color?.startsWith("#")) { | ||||||
|  |       color = hex2rgb(color).join(","); | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       name: label?.name || itemId, | ||||||
|  |       iconPath: label?.icon, | ||||||
|  |       fallbackIconPath: mdiLabel, | ||||||
|  |       color, | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   private _setDomainName(domain: string) { | ||||||
|  |     this._domainName = domainToName(this.hass.localize, domain); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private async _getDeviceDomain(configEntryId: string) { | ||||||
|  |     try { | ||||||
|  |       const data = await getConfigEntry(this.hass, configEntryId); | ||||||
|  |       const domain = data.config_entry.domain; | ||||||
|  |       this._iconImg = brandsUrl({ | ||||||
|  |         domain: domain, | ||||||
|  |         type: "icon", | ||||||
|  |         darkOptimized: this.hass.themes?.darkMode, | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       this._setDomainName(domain); | ||||||
|  |     } catch { | ||||||
|  |       // failed to load config entry -> ignore | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _removeItem(ev) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     fireEvent(this, "remove-target-item", { | ||||||
|  |       type: this.type, | ||||||
|  |       id: this.itemId, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _handleExpand(ev) { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     fireEvent(this, "expand-target-item", { | ||||||
|  |       type: this.type, | ||||||
|  |       id: this.itemId, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   static styles = css` | ||||||
|  |     ${unsafeCSS(chipStyles)} | ||||||
|  |     .mdc-chip { | ||||||
|  |       color: var(--primary-text-color); | ||||||
|  |     } | ||||||
|  |     .mdc-chip.add { | ||||||
|  |       color: rgba(0, 0, 0, 0.87); | ||||||
|  |     } | ||||||
|  |     .add-container { | ||||||
|  |       position: relative; | ||||||
|  |       display: inline-flex; | ||||||
|  |     } | ||||||
|  |     .mdc-chip:not(.add) { | ||||||
|  |       cursor: default; | ||||||
|  |     } | ||||||
|  |     .mdc-chip ha-icon-button { | ||||||
|  |       --mdc-icon-button-size: 24px; | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       outline: none; | ||||||
|  |     } | ||||||
|  |     .mdc-chip ha-icon-button ha-svg-icon { | ||||||
|  |       border-radius: 50%; | ||||||
|  |       background: var(--secondary-text-color); | ||||||
|  |     } | ||||||
|  |     .mdc-chip__icon.mdc-chip__icon--trailing { | ||||||
|  |       width: var(--ha-space-4); | ||||||
|  |       height: var(--ha-space-4); | ||||||
|  |       --mdc-icon-size: 14px; | ||||||
|  |       color: var(--secondary-text-color); | ||||||
|  |       margin-inline-start: var(--ha-space-1) !important; | ||||||
|  |       margin-inline-end: calc(-1 * var(--ha-space-1)) !important; | ||||||
|  |       direction: var(--direction); | ||||||
|  |     } | ||||||
|  |     .mdc-chip__icon--leading { | ||||||
|  |       display: flex; | ||||||
|  |       align-items: center; | ||||||
|  |       justify-content: center; | ||||||
|  |       --mdc-icon-size: 20px; | ||||||
|  |       border-radius: var(--ha-border-radius-circle); | ||||||
|  |       padding: 6px; | ||||||
|  |       margin-left: -13px !important; | ||||||
|  |       margin-inline-start: -13px !important; | ||||||
|  |       margin-inline-end: var(--ha-space-1) !important; | ||||||
|  |       direction: var(--direction); | ||||||
|  |     } | ||||||
|  |     .expand-btn { | ||||||
|  |       margin-right: var(--ha-space-0); | ||||||
|  |       margin-inline-end: var(--ha-space-0); | ||||||
|  |       margin-inline-start: initial; | ||||||
|  |     } | ||||||
|  |     .mdc-chip.area:not(.add), | ||||||
|  |     .mdc-chip.floor:not(.add) { | ||||||
|  |       border: 1px solid #fed6a4; | ||||||
|  |       background: var(--card-background-color); | ||||||
|  |     } | ||||||
|  |     .mdc-chip.area:not(.add) .mdc-chip__icon--leading, | ||||||
|  |     .mdc-chip.area.add, | ||||||
|  |     .mdc-chip.floor:not(.add) .mdc-chip__icon--leading, | ||||||
|  |     .mdc-chip.floor.add { | ||||||
|  |       background: #fed6a4; | ||||||
|  |     } | ||||||
|  |     .mdc-chip.device:not(.add) { | ||||||
|  |       border: 1px solid #a8e1fb; | ||||||
|  |       background: var(--card-background-color); | ||||||
|  |     } | ||||||
|  |     .mdc-chip.device:not(.add) .mdc-chip__icon--leading, | ||||||
|  |     .mdc-chip.device.add { | ||||||
|  |       background: #a8e1fb; | ||||||
|  |     } | ||||||
|  |     .mdc-chip.entity:not(.add) { | ||||||
|  |       border: 1px solid #d2e7b9; | ||||||
|  |       background: var(--card-background-color); | ||||||
|  |     } | ||||||
|  |     .mdc-chip.entity:not(.add) .mdc-chip__icon--leading, | ||||||
|  |     .mdc-chip.entity.add { | ||||||
|  |       background: #d2e7b9; | ||||||
|  |     } | ||||||
|  |     .mdc-chip.label:not(.add) { | ||||||
|  |       border: 1px solid var(--color, #e0e0e0); | ||||||
|  |       background: var(--card-background-color); | ||||||
|  |     } | ||||||
|  |     .mdc-chip.label:not(.add) .mdc-chip__icon--leading, | ||||||
|  |     .mdc-chip.label.add { | ||||||
|  |       background: var(--background-color, #e0e0e0); | ||||||
|  |     } | ||||||
|  |     .mdc-chip:hover { | ||||||
|  |       z-index: 5; | ||||||
|  |     } | ||||||
|  |     :host([disabled]) .mdc-chip { | ||||||
|  |       opacity: var(--light-disabled-opacity); | ||||||
|  |       pointer-events: none; | ||||||
|  |     } | ||||||
|  |     .tooltip-icon-img { | ||||||
|  |       width: 24px; | ||||||
|  |       height: 24px; | ||||||
|  |     } | ||||||
|  |   `; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | declare global { | ||||||
|  |   interface HTMLElementTagNameMap { | ||||||
|  |     "ha-target-picker-value-chip": HaTargetPickerValueChip; | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										259
									
								
								src/data/area_floor.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										259
									
								
								src/data/area_floor.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,259 @@ | |||||||
|  | import { computeAreaName } from "../common/entity/compute_area_name"; | ||||||
|  | import { computeDomain } from "../common/entity/compute_domain"; | ||||||
|  | import { computeFloorName } from "../common/entity/compute_floor_name"; | ||||||
|  | import { stringCompare } from "../common/string/compare"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker"; | ||||||
|  | import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; | ||||||
|  | import type { HomeAssistant } from "../types"; | ||||||
|  | import type { AreaRegistryEntry } from "./area_registry"; | ||||||
|  | import { | ||||||
|  |   getDeviceEntityDisplayLookup, | ||||||
|  |   type DeviceEntityDisplayLookup, | ||||||
|  |   type DeviceRegistryEntry, | ||||||
|  | } from "./device_registry"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "./entity"; | ||||||
|  | import type { EntityRegistryDisplayEntry } from "./entity_registry"; | ||||||
|  | import { getFloorAreaLookup, type FloorRegistryEntry } from "./floor_registry"; | ||||||
|  |  | ||||||
|  | export interface FloorComboBoxItem extends PickerComboBoxItem { | ||||||
|  |   type: "floor" | "area"; | ||||||
|  |   floor?: FloorRegistryEntry; | ||||||
|  |   area?: AreaRegistryEntry; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface AreaFloorValue { | ||||||
|  |   id: string; | ||||||
|  |   type: "floor" | "area"; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const getAreasAndFloors = ( | ||||||
|  |   states: HomeAssistant["states"], | ||||||
|  |   haFloors: HomeAssistant["floors"], | ||||||
|  |   haAreas: HomeAssistant["areas"], | ||||||
|  |   haDevices: HomeAssistant["devices"], | ||||||
|  |   haEntities: HomeAssistant["entities"], | ||||||
|  |   formatId: (value: AreaFloorValue) => string, | ||||||
|  |   includeDomains?: string[], | ||||||
|  |   excludeDomains?: string[], | ||||||
|  |   includeDeviceClasses?: string[], | ||||||
|  |   deviceFilter?: HaDevicePickerDeviceFilterFunc, | ||||||
|  |   entityFilter?: HaEntityPickerEntityFilterFunc, | ||||||
|  |   excludeAreas?: string[], | ||||||
|  |   excludeFloors?: string[] | ||||||
|  | ): FloorComboBoxItem[] => { | ||||||
|  |   const floors = Object.values(haFloors); | ||||||
|  |   const areas = Object.values(haAreas); | ||||||
|  |   const devices = Object.values(haDevices); | ||||||
|  |   const entities = Object.values(haEntities); | ||||||
|  |  | ||||||
|  |   let deviceEntityLookup: DeviceEntityDisplayLookup = {}; | ||||||
|  |   let inputDevices: DeviceRegistryEntry[] | undefined; | ||||||
|  |   let inputEntities: EntityRegistryDisplayEntry[] | undefined; | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     includeDomains || | ||||||
|  |     excludeDomains || | ||||||
|  |     includeDeviceClasses || | ||||||
|  |     deviceFilter || | ||||||
|  |     entityFilter | ||||||
|  |   ) { | ||||||
|  |     deviceEntityLookup = getDeviceEntityDisplayLookup(entities); | ||||||
|  |     inputDevices = devices; | ||||||
|  |     inputEntities = entities.filter((entity) => entity.area_id); | ||||||
|  |  | ||||||
|  |     if (includeDomains) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => { | ||||||
|  |         const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |         if (!devEntities || !devEntities.length) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return deviceEntityLookup[device.id].some((entity) => | ||||||
|  |           includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |       inputEntities = inputEntities!.filter((entity) => | ||||||
|  |         includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (excludeDomains) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => { | ||||||
|  |         const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |         if (!devEntities || !devEntities.length) { | ||||||
|  |           return true; | ||||||
|  |         } | ||||||
|  |         return entities.every( | ||||||
|  |           (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |       inputEntities = inputEntities!.filter( | ||||||
|  |         (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (includeDeviceClasses) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => { | ||||||
|  |         const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |         if (!devEntities || !devEntities.length) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return deviceEntityLookup[device.id].some((entity) => { | ||||||
|  |           const stateObj = states[entity.entity_id]; | ||||||
|  |           if (!stateObj) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |           return ( | ||||||
|  |             stateObj.attributes.device_class && | ||||||
|  |             includeDeviceClasses.includes(stateObj.attributes.device_class) | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |       inputEntities = inputEntities!.filter((entity) => { | ||||||
|  |         const stateObj = states[entity.entity_id]; | ||||||
|  |         return ( | ||||||
|  |           stateObj.attributes.device_class && | ||||||
|  |           includeDeviceClasses.includes(stateObj.attributes.device_class) | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (deviceFilter) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => deviceFilter!(device)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (entityFilter) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => { | ||||||
|  |         const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |         if (!devEntities || !devEntities.length) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return deviceEntityLookup[device.id].some((entity) => { | ||||||
|  |           const stateObj = states[entity.entity_id]; | ||||||
|  |           if (!stateObj) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |           return entityFilter(stateObj); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |       inputEntities = inputEntities!.filter((entity) => { | ||||||
|  |         const stateObj = states[entity.entity_id]; | ||||||
|  |         if (!stateObj) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return entityFilter!(stateObj); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let outputAreas = areas; | ||||||
|  |  | ||||||
|  |   let areaIds: string[] | undefined; | ||||||
|  |  | ||||||
|  |   if (inputDevices) { | ||||||
|  |     areaIds = inputDevices | ||||||
|  |       .filter((device) => device.area_id) | ||||||
|  |       .map((device) => device.area_id!); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (inputEntities) { | ||||||
|  |     areaIds = (areaIds ?? []).concat( | ||||||
|  |       inputEntities | ||||||
|  |         .filter((entity) => entity.area_id) | ||||||
|  |         .map((entity) => entity.area_id!) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (areaIds) { | ||||||
|  |     outputAreas = outputAreas.filter((area) => areaIds!.includes(area.area_id)); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (excludeAreas) { | ||||||
|  |     outputAreas = outputAreas.filter( | ||||||
|  |       (area) => !excludeAreas!.includes(area.area_id) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (excludeFloors) { | ||||||
|  |     outputAreas = outputAreas.filter( | ||||||
|  |       (area) => !area.floor_id || !excludeFloors!.includes(area.floor_id) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const floorAreaLookup = getFloorAreaLookup(outputAreas); | ||||||
|  |   const unassignedAreas = Object.values(outputAreas).filter( | ||||||
|  |     (area) => !area.floor_id || !floorAreaLookup[area.floor_id] | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   // @ts-ignore | ||||||
|  |   const floorAreaEntries: [ | ||||||
|  |     FloorRegistryEntry | undefined, | ||||||
|  |     AreaRegistryEntry[], | ||||||
|  |   ][] = Object.entries(floorAreaLookup) | ||||||
|  |     .map(([floorId, floorAreas]) => { | ||||||
|  |       const floor = floors.find((fl) => fl.floor_id === floorId)!; | ||||||
|  |       return [floor, floorAreas] as const; | ||||||
|  |     }) | ||||||
|  |     .sort(([floorA], [floorB]) => { | ||||||
|  |       if (floorA.level !== floorB.level) { | ||||||
|  |         return (floorA.level ?? 0) - (floorB.level ?? 0); | ||||||
|  |       } | ||||||
|  |       return stringCompare(floorA.name, floorB.name); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |   const items: FloorComboBoxItem[] = []; | ||||||
|  |  | ||||||
|  |   floorAreaEntries.forEach(([floor, floorAreas]) => { | ||||||
|  |     if (floor) { | ||||||
|  |       const floorName = computeFloorName(floor); | ||||||
|  |  | ||||||
|  |       const areaSearchLabels = floorAreas | ||||||
|  |         .map((area) => { | ||||||
|  |           const areaName = computeAreaName(area) || area.area_id; | ||||||
|  |           return [area.area_id, areaName, ...area.aliases]; | ||||||
|  |         }) | ||||||
|  |         .flat(); | ||||||
|  |  | ||||||
|  |       items.push({ | ||||||
|  |         id: formatId({ id: floor.floor_id, type: "floor" }), | ||||||
|  |         type: "floor", | ||||||
|  |         primary: floorName, | ||||||
|  |         floor: floor, | ||||||
|  |         search_labels: [ | ||||||
|  |           floor.floor_id, | ||||||
|  |           floorName, | ||||||
|  |           ...floor.aliases, | ||||||
|  |           ...areaSearchLabels, | ||||||
|  |         ], | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     items.push( | ||||||
|  |       ...floorAreas.map((area) => { | ||||||
|  |         const areaName = computeAreaName(area) || area.area_id; | ||||||
|  |         return { | ||||||
|  |           id: formatId({ id: area.area_id, type: "area" }), | ||||||
|  |           type: "area" as const, | ||||||
|  |           primary: areaName, | ||||||
|  |           area: area, | ||||||
|  |           icon: area.icon || undefined, | ||||||
|  |           search_labels: [area.area_id, areaName, ...area.aliases], | ||||||
|  |         }; | ||||||
|  |       }) | ||||||
|  |     ); | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   items.push( | ||||||
|  |     ...unassignedAreas.map((area) => { | ||||||
|  |       const areaName = computeAreaName(area) || area.area_id; | ||||||
|  |       return { | ||||||
|  |         id: formatId({ id: area.area_id, type: "area" }), | ||||||
|  |         type: "area" as const, | ||||||
|  |         primary: areaName, | ||||||
|  |         area: area, | ||||||
|  |         icon: area.icon || undefined, | ||||||
|  |         search_labels: [area.area_id, areaName, ...area.aliases], | ||||||
|  |       }; | ||||||
|  |     }) | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   return items; | ||||||
|  | }; | ||||||
| @@ -79,6 +79,7 @@ export interface DataEntryFlowStepAbort { | |||||||
|   reason: string; |   reason: string; | ||||||
|   description_placeholders?: Record<string, string>; |   description_placeholders?: Record<string, string>; | ||||||
|   translation_domain?: string; |   translation_domain?: string; | ||||||
|  |   next_flow?: [FlowType, string]; // [flow_type, flow_id] | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface DataEntryFlowStepProgress { | export interface DataEntryFlowStepProgress { | ||||||
|   | |||||||
| @@ -1,12 +1,20 @@ | |||||||
|  | import { computeAreaName } from "../common/entity/compute_area_name"; | ||||||
|  | import { computeDeviceNameDisplay } from "../common/entity/compute_device_name"; | ||||||
|  | import { computeDomain } from "../common/entity/compute_domain"; | ||||||
| import { computeStateName } from "../common/entity/compute_state_name"; | import { computeStateName } from "../common/entity/compute_state_name"; | ||||||
|  | import { getDeviceContext } from "../common/entity/context/get_device_context"; | ||||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker"; | ||||||
|  | import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; | ||||||
| import type { HomeAssistant } from "../types"; | import type { HomeAssistant } from "../types"; | ||||||
| import type { ConfigEntry } from "./config_entries"; | import type { ConfigEntry } from "./config_entries"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "./entity"; | ||||||
| import type { | import type { | ||||||
|   EntityRegistryDisplayEntry, |   EntityRegistryDisplayEntry, | ||||||
|   EntityRegistryEntry, |   EntityRegistryEntry, | ||||||
| } from "./entity_registry"; | } from "./entity_registry"; | ||||||
| import type { EntitySources } from "./entity_sources"; | import type { EntitySources } from "./entity_sources"; | ||||||
|  | import { domainToName } from "./integration"; | ||||||
| import type { RegistryEntry } from "./registry"; | import type { RegistryEntry } from "./registry"; | ||||||
|  |  | ||||||
| export { | export { | ||||||
| @@ -163,3 +171,147 @@ export const getDeviceIntegrationLookup = ( | |||||||
|   } |   } | ||||||
|   return deviceIntegrations; |   return deviceIntegrations; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | export interface DevicePickerItem extends PickerComboBoxItem { | ||||||
|  |   domain?: string; | ||||||
|  |   domain_name?: string; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const getDevices = ( | ||||||
|  |   hass: HomeAssistant, | ||||||
|  |   configEntryLookup: Record<string, ConfigEntry>, | ||||||
|  |   includeDomains?: string[], | ||||||
|  |   excludeDomains?: string[], | ||||||
|  |   includeDeviceClasses?: string[], | ||||||
|  |   deviceFilter?: HaDevicePickerDeviceFilterFunc, | ||||||
|  |   entityFilter?: HaEntityPickerEntityFilterFunc, | ||||||
|  |   excludeDevices?: string[], | ||||||
|  |   value?: string | ||||||
|  | ): DevicePickerItem[] => { | ||||||
|  |   const devices = Object.values(hass.devices); | ||||||
|  |   const entities = Object.values(hass.entities); | ||||||
|  |  | ||||||
|  |   let deviceEntityLookup: DeviceEntityDisplayLookup = {}; | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     includeDomains || | ||||||
|  |     excludeDomains || | ||||||
|  |     includeDeviceClasses || | ||||||
|  |     entityFilter | ||||||
|  |   ) { | ||||||
|  |     deviceEntityLookup = getDeviceEntityDisplayLookup(entities); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let inputDevices = devices.filter( | ||||||
|  |     (device) => device.id === value || !device.disabled_by | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if (includeDomains) { | ||||||
|  |     inputDevices = inputDevices.filter((device) => { | ||||||
|  |       const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |       if (!devEntities || !devEntities.length) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       return deviceEntityLookup[device.id].some((entity) => | ||||||
|  |         includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (excludeDomains) { | ||||||
|  |     inputDevices = inputDevices.filter((device) => { | ||||||
|  |       const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |       if (!devEntities || !devEntities.length) { | ||||||
|  |         return true; | ||||||
|  |       } | ||||||
|  |       return entities.every( | ||||||
|  |         (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (excludeDevices) { | ||||||
|  |     inputDevices = inputDevices.filter( | ||||||
|  |       (device) => !excludeDevices!.includes(device.id) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (includeDeviceClasses) { | ||||||
|  |     inputDevices = inputDevices.filter((device) => { | ||||||
|  |       const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |       if (!devEntities || !devEntities.length) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       return deviceEntityLookup[device.id].some((entity) => { | ||||||
|  |         const stateObj = hass.states[entity.entity_id]; | ||||||
|  |         if (!stateObj) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return ( | ||||||
|  |           stateObj.attributes.device_class && | ||||||
|  |           includeDeviceClasses.includes(stateObj.attributes.device_class) | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (entityFilter) { | ||||||
|  |     inputDevices = inputDevices.filter((device) => { | ||||||
|  |       const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |       if (!devEntities || !devEntities.length) { | ||||||
|  |         return false; | ||||||
|  |       } | ||||||
|  |       return devEntities.some((entity) => { | ||||||
|  |         const stateObj = hass.states[entity.entity_id]; | ||||||
|  |         if (!stateObj) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return entityFilter(stateObj); | ||||||
|  |       }); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (deviceFilter) { | ||||||
|  |     inputDevices = inputDevices.filter( | ||||||
|  |       (device) => | ||||||
|  |         // We always want to include the device of the current value | ||||||
|  |         device.id === value || deviceFilter!(device) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const outputDevices = inputDevices.map<DevicePickerItem>((device) => { | ||||||
|  |     const deviceName = computeDeviceNameDisplay( | ||||||
|  |       device, | ||||||
|  |       hass, | ||||||
|  |       deviceEntityLookup[device.id] | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const { area } = getDeviceContext(device, hass); | ||||||
|  |  | ||||||
|  |     const areaName = area ? computeAreaName(area) : undefined; | ||||||
|  |  | ||||||
|  |     const configEntry = device.primary_config_entry | ||||||
|  |       ? configEntryLookup?.[device.primary_config_entry] | ||||||
|  |       : undefined; | ||||||
|  |  | ||||||
|  |     const domain = configEntry?.domain; | ||||||
|  |     const domainName = domain ? domainToName(hass.localize, domain) : undefined; | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       id: device.id, | ||||||
|  |       label: "", | ||||||
|  |       primary: | ||||||
|  |         deviceName || | ||||||
|  |         hass.localize("ui.components.device-picker.unnamed_device"), | ||||||
|  |       secondary: areaName, | ||||||
|  |       domain: configEntry?.domain, | ||||||
|  |       domain_name: domainName, | ||||||
|  |       search_labels: [deviceName, areaName, domain, domainName].filter( | ||||||
|  |         Boolean | ||||||
|  |       ) as string[], | ||||||
|  |       sorting_label: deviceName || "zzz", | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   return outputDevices; | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -102,7 +102,6 @@ export type EnergySolarForecasts = Record<string, EnergySolarForecast>; | |||||||
| export interface DeviceConsumptionEnergyPreference { | export interface DeviceConsumptionEnergyPreference { | ||||||
|   // This is an ever increasing value |   // This is an ever increasing value | ||||||
|   stat_consumption: string; |   stat_consumption: string; | ||||||
|   stat_power?: string; |  | ||||||
|   name?: string; |   name?: string; | ||||||
|   included_in_stat?: string; |   included_in_stat?: string; | ||||||
| } | } | ||||||
| @@ -131,17 +130,11 @@ export interface FlowToGridSourceEnergyPreference { | |||||||
|   number_energy_price: number | null; |   number_energy_price: number | null; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface GridPowerSourceEnergyPreference { |  | ||||||
|   // W meter |  | ||||||
|   stat_power: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface GridSourceTypeEnergyPreference { | export interface GridSourceTypeEnergyPreference { | ||||||
|   type: "grid"; |   type: "grid"; | ||||||
|  |  | ||||||
|   flow_from: FlowFromGridSourceEnergyPreference[]; |   flow_from: FlowFromGridSourceEnergyPreference[]; | ||||||
|   flow_to: FlowToGridSourceEnergyPreference[]; |   flow_to: FlowToGridSourceEnergyPreference[]; | ||||||
|   power?: GridPowerSourceEnergyPreference[]; |  | ||||||
|  |  | ||||||
|   cost_adjustment_day: number; |   cost_adjustment_day: number; | ||||||
| } | } | ||||||
| @@ -150,7 +143,6 @@ export interface SolarSourceTypeEnergyPreference { | |||||||
|   type: "solar"; |   type: "solar"; | ||||||
|  |  | ||||||
|   stat_energy_from: string; |   stat_energy_from: string; | ||||||
|   stat_power?: string; |  | ||||||
|   config_entry_solar_forecast: string[] | null; |   config_entry_solar_forecast: string[] | null; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -158,7 +150,6 @@ export interface BatterySourceTypeEnergyPreference { | |||||||
|   type: "battery"; |   type: "battery"; | ||||||
|   stat_energy_from: string; |   stat_energy_from: string; | ||||||
|   stat_energy_to: string; |   stat_energy_to: string; | ||||||
|   stat_power?: string; |  | ||||||
| } | } | ||||||
| export interface GasSourceTypeEnergyPreference { | export interface GasSourceTypeEnergyPreference { | ||||||
|   type: "gas"; |   type: "gas"; | ||||||
| @@ -360,35 +351,6 @@ export const getReferencedStatisticIds = ( | |||||||
|   return statIDs; |   return statIDs; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const getReferencedStatisticIdsPower = ( |  | ||||||
|   prefs: EnergyPreferences |  | ||||||
| ): string[] => { |  | ||||||
|   const statIDs: (string | undefined)[] = []; |  | ||||||
|  |  | ||||||
|   for (const source of prefs.energy_sources) { |  | ||||||
|     if (source.type === "gas" || source.type === "water") { |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (source.type === "solar") { |  | ||||||
|       statIDs.push(source.stat_power); |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (source.type === "battery") { |  | ||||||
|       statIDs.push(source.stat_power); |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     if (source.power) { |  | ||||||
|       statIDs.push(...source.power.map((p) => p.stat_power)); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   statIDs.push(...prefs.device_consumption.map((d) => d.stat_power)); |  | ||||||
|  |  | ||||||
|   return statIDs.filter(Boolean) as string[]; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| export const enum CompareMode { | export const enum CompareMode { | ||||||
|   NONE = "", |   NONE = "", | ||||||
|   PREVIOUS = "previous", |   PREVIOUS = "previous", | ||||||
| @@ -436,10 +398,9 @@ const getEnergyData = async ( | |||||||
|     "gas", |     "gas", | ||||||
|     "device", |     "device", | ||||||
|   ]); |   ]); | ||||||
|   const powerStatIds = getReferencedStatisticIdsPower(prefs); |  | ||||||
|   const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]); |   const waterStatIds = getReferencedStatisticIds(prefs, info, ["water"]); | ||||||
|  |  | ||||||
|   const allStatIDs = [...energyStatIds, ...waterStatIds, ...powerStatIds]; |   const allStatIDs = [...energyStatIds, ...waterStatIds]; | ||||||
|  |  | ||||||
|   const dayDifference = differenceInDays(end || new Date(), start); |   const dayDifference = differenceInDays(end || new Date(), start); | ||||||
|   const period = |   const period = | ||||||
| @@ -450,8 +411,6 @@ const getEnergyData = async ( | |||||||
|       : dayDifference > 2 |       : dayDifference > 2 | ||||||
|         ? "day" |         ? "day" | ||||||
|         : "hour"; |         : "hour"; | ||||||
|   const finePeriod = |  | ||||||
|     dayDifference > 64 ? "day" : dayDifference > 8 ? "hour" : "5minute"; |  | ||||||
|  |  | ||||||
|   const statsMetadata: Record<string, StatisticsMetaData> = {}; |   const statsMetadata: Record<string, StatisticsMetaData> = {}; | ||||||
|   const statsMetadataArray = allStatIDs.length |   const statsMetadataArray = allStatIDs.length | ||||||
| @@ -473,9 +432,6 @@ const getEnergyData = async ( | |||||||
|       ? (gasUnit as (typeof VOLUME_UNITS)[number]) |       ? (gasUnit as (typeof VOLUME_UNITS)[number]) | ||||||
|       : undefined, |       : undefined, | ||||||
|   }; |   }; | ||||||
|   const powerUnits: StatisticsUnitConfiguration = { |  | ||||||
|     power: "kW", |  | ||||||
|   }; |  | ||||||
|   const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata); |   const waterUnit = getEnergyWaterUnit(hass, prefs, statsMetadata); | ||||||
|   const waterUnits: StatisticsUnitConfiguration = { |   const waterUnits: StatisticsUnitConfiguration = { | ||||||
|     volume: waterUnit, |     volume: waterUnit, | ||||||
| @@ -486,12 +442,6 @@ const getEnergyData = async ( | |||||||
|         "change", |         "change", | ||||||
|       ]) |       ]) | ||||||
|     : {}; |     : {}; | ||||||
|   const _powerStats: Statistics | Promise<Statistics> = powerStatIds.length |  | ||||||
|     ? fetchStatistics(hass!, start, end, powerStatIds, finePeriod, powerUnits, [ |  | ||||||
|         "mean", |  | ||||||
|       ]) |  | ||||||
|     : {}; |  | ||||||
|  |  | ||||||
|   const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length |   const _waterStats: Statistics | Promise<Statistics> = waterStatIds.length | ||||||
|     ? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [ |     ? fetchStatistics(hass!, start, end, waterStatIds, period, waterUnits, [ | ||||||
|         "change", |         "change", | ||||||
| @@ -598,7 +548,6 @@ const getEnergyData = async ( | |||||||
|  |  | ||||||
|   const [ |   const [ | ||||||
|     energyStats, |     energyStats, | ||||||
|     powerStats, |  | ||||||
|     waterStats, |     waterStats, | ||||||
|     energyStatsCompare, |     energyStatsCompare, | ||||||
|     waterStatsCompare, |     waterStatsCompare, | ||||||
| @@ -606,14 +555,13 @@ const getEnergyData = async ( | |||||||
|     fossilEnergyConsumptionCompare, |     fossilEnergyConsumptionCompare, | ||||||
|   ] = await Promise.all([ |   ] = await Promise.all([ | ||||||
|     _energyStats, |     _energyStats, | ||||||
|     _powerStats, |  | ||||||
|     _waterStats, |     _waterStats, | ||||||
|     _energyStatsCompare, |     _energyStatsCompare, | ||||||
|     _waterStatsCompare, |     _waterStatsCompare, | ||||||
|     _fossilEnergyConsumption, |     _fossilEnergyConsumption, | ||||||
|     _fossilEnergyConsumptionCompare, |     _fossilEnergyConsumptionCompare, | ||||||
|   ]); |   ]); | ||||||
|   const stats = { ...energyStats, ...waterStats, ...powerStats }; |   const stats = { ...energyStats, ...waterStats }; | ||||||
|   if (compare) { |   if (compare) { | ||||||
|     statsCompare = { ...energyStatsCompare, ...waterStatsCompare }; |     statsCompare = { ...energyStatsCompare, ...waterStatsCompare }; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import type { HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { arrayLiteralIncludes } from "../common/array/literal-includes"; | import { arrayLiteralIncludes } from "../common/array/literal-includes"; | ||||||
|  |  | ||||||
| export const UNAVAILABLE = "unavailable"; | export const UNAVAILABLE = "unavailable"; | ||||||
| @@ -10,3 +11,5 @@ export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const; | |||||||
|  |  | ||||||
| export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES); | export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES); | ||||||
| export const isOffState = arrayLiteralIncludes(OFF_STATES); | export const isOffState = arrayLiteralIncludes(OFF_STATES); | ||||||
|  |  | ||||||
|  | export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | ||||||
|   | |||||||
| @@ -1,12 +1,17 @@ | |||||||
| import type { Connection } from "home-assistant-js-websocket"; | import type { Connection, HassEntity } from "home-assistant-js-websocket"; | ||||||
| import { createCollection } from "home-assistant-js-websocket"; | import { createCollection } from "home-assistant-js-websocket"; | ||||||
| import type { Store } from "home-assistant-js-websocket/dist/store"; | import type { Store } from "home-assistant-js-websocket/dist/store"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { computeDomain } from "../common/entity/compute_domain"; | import { computeDomain } from "../common/entity/compute_domain"; | ||||||
|  | import { computeEntityNameList } from "../common/entity/compute_entity_name_display"; | ||||||
| import { computeStateName } from "../common/entity/compute_state_name"; | import { computeStateName } from "../common/entity/compute_state_name"; | ||||||
| import { caseInsensitiveStringCompare } from "../common/string/compare"; | import { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||||
|  | import { computeRTL } from "../common/util/compute_rtl"; | ||||||
| import { debounce } from "../common/util/debounce"; | import { debounce } from "../common/util/debounce"; | ||||||
|  | import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; | ||||||
| import type { HomeAssistant } from "../types"; | import type { HomeAssistant } from "../types"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "./entity"; | ||||||
|  | import { domainToName } from "./integration"; | ||||||
| import type { LightColor } from "./light"; | import type { LightColor } from "./light"; | ||||||
| import type { RegistryEntry } from "./registry"; | import type { RegistryEntry } from "./registry"; | ||||||
|  |  | ||||||
| @@ -324,3 +329,122 @@ export const getAutomaticEntityIds = ( | |||||||
|     type: "config/entity_registry/get_automatic_entity_ids", |     type: "config/entity_registry/get_automatic_entity_ids", | ||||||
|     entity_ids, |     entity_ids, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  | export interface EntityComboBoxItem extends PickerComboBoxItem { | ||||||
|  |   domain_name?: string; | ||||||
|  |   stateObj?: HassEntity; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const getEntities = ( | ||||||
|  |   hass: HomeAssistant, | ||||||
|  |   includeDomains?: string[], | ||||||
|  |   excludeDomains?: string[], | ||||||
|  |   entityFilter?: HaEntityPickerEntityFilterFunc, | ||||||
|  |   includeDeviceClasses?: string[], | ||||||
|  |   includeUnitOfMeasurement?: string[], | ||||||
|  |   includeEntities?: string[], | ||||||
|  |   excludeEntities?: string[], | ||||||
|  |   value?: string | ||||||
|  | ): EntityComboBoxItem[] => { | ||||||
|  |   let items: EntityComboBoxItem[] = []; | ||||||
|  |  | ||||||
|  |   let entityIds = Object.keys(hass.states); | ||||||
|  |  | ||||||
|  |   if (includeEntities) { | ||||||
|  |     entityIds = entityIds.filter((entityId) => | ||||||
|  |       includeEntities.includes(entityId) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (excludeEntities) { | ||||||
|  |     entityIds = entityIds.filter( | ||||||
|  |       (entityId) => !excludeEntities.includes(entityId) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (includeDomains) { | ||||||
|  |     entityIds = entityIds.filter((eid) => | ||||||
|  |       includeDomains.includes(computeDomain(eid)) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (excludeDomains) { | ||||||
|  |     entityIds = entityIds.filter( | ||||||
|  |       (eid) => !excludeDomains.includes(computeDomain(eid)) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   items = entityIds.map<EntityComboBoxItem>((entityId) => { | ||||||
|  |     const stateObj = hass.states[entityId]; | ||||||
|  |  | ||||||
|  |     const friendlyName = computeStateName(stateObj); // Keep this for search | ||||||
|  |     const [entityName, deviceName, areaName] = computeEntityNameList( | ||||||
|  |       stateObj, | ||||||
|  |       [{ type: "entity" }, { type: "device" }, { type: "area" }], | ||||||
|  |       hass.entities, | ||||||
|  |       hass.devices, | ||||||
|  |       hass.areas, | ||||||
|  |       hass.floors | ||||||
|  |     ); | ||||||
|  |  | ||||||
|  |     const domainName = domainToName(hass.localize, computeDomain(entityId)); | ||||||
|  |  | ||||||
|  |     const isRTL = computeRTL(hass); | ||||||
|  |  | ||||||
|  |     const primary = entityName || deviceName || entityId; | ||||||
|  |     const secondary = [areaName, entityName ? deviceName : undefined] | ||||||
|  |       .filter(Boolean) | ||||||
|  |       .join(isRTL ? " ◂ " : " ▸ "); | ||||||
|  |     const a11yLabel = [deviceName, entityName].filter(Boolean).join(" - "); | ||||||
|  |  | ||||||
|  |     return { | ||||||
|  |       id: entityId, | ||||||
|  |       primary: primary, | ||||||
|  |       secondary: secondary, | ||||||
|  |       domain_name: domainName, | ||||||
|  |       sorting_label: [deviceName, entityName].filter(Boolean).join("_"), | ||||||
|  |       search_labels: [ | ||||||
|  |         entityName, | ||||||
|  |         deviceName, | ||||||
|  |         areaName, | ||||||
|  |         domainName, | ||||||
|  |         friendlyName, | ||||||
|  |         entityId, | ||||||
|  |       ].filter(Boolean) as string[], | ||||||
|  |       a11y_label: a11yLabel, | ||||||
|  |       stateObj: stateObj, | ||||||
|  |     }; | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  |   if (includeDeviceClasses) { | ||||||
|  |     items = items.filter( | ||||||
|  |       (item) => | ||||||
|  |         // We always want to include the entity of the current value | ||||||
|  |         item.id === value || | ||||||
|  |         (item.stateObj?.attributes.device_class && | ||||||
|  |           includeDeviceClasses.includes(item.stateObj.attributes.device_class)) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (includeUnitOfMeasurement) { | ||||||
|  |     items = items.filter( | ||||||
|  |       (item) => | ||||||
|  |         // We always want to include the entity of the current value | ||||||
|  |         item.id === value || | ||||||
|  |         (item.stateObj?.attributes.unit_of_measurement && | ||||||
|  |           includeUnitOfMeasurement.includes( | ||||||
|  |             item.stateObj.attributes.unit_of_measurement | ||||||
|  |           )) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (entityFilter) { | ||||||
|  |     items = items.filter( | ||||||
|  |       (item) => | ||||||
|  |         // We always want to include the entity of the current value | ||||||
|  |         item.id === value || (item.stateObj && entityFilter!(item.stateObj)) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return items; | ||||||
|  | }; | ||||||
|   | |||||||
| @@ -1,9 +1,20 @@ | |||||||
|  | import { mdiLabel } from "@mdi/js"; | ||||||
| import type { Connection } from "home-assistant-js-websocket"; | import type { Connection } from "home-assistant-js-websocket"; | ||||||
| import { createCollection } from "home-assistant-js-websocket"; | import { createCollection } from "home-assistant-js-websocket"; | ||||||
| import type { Store } from "home-assistant-js-websocket/dist/store"; | import type { Store } from "home-assistant-js-websocket/dist/store"; | ||||||
|  | import { computeDomain } from "../common/entity/compute_domain"; | ||||||
| import { stringCompare } from "../common/string/compare"; | import { stringCompare } from "../common/string/compare"; | ||||||
| import { debounce } from "../common/util/debounce"; | import { debounce } from "../common/util/debounce"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker"; | ||||||
|  | import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; | ||||||
| import type { HomeAssistant } from "../types"; | import type { HomeAssistant } from "../types"; | ||||||
|  | import { | ||||||
|  |   getDeviceEntityDisplayLookup, | ||||||
|  |   type DeviceEntityDisplayLookup, | ||||||
|  |   type DeviceRegistryEntry, | ||||||
|  | } from "./device_registry"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "./entity"; | ||||||
|  | import type { EntityRegistryDisplayEntry } from "./entity_registry"; | ||||||
| import type { RegistryEntry } from "./registry"; | import type { RegistryEntry } from "./registry"; | ||||||
|  |  | ||||||
| export interface LabelRegistryEntry extends RegistryEntry { | export interface LabelRegistryEntry extends RegistryEntry { | ||||||
| @@ -88,3 +99,178 @@ export const deleteLabelRegistryEntry = ( | |||||||
|     type: "config/label_registry/delete", |     type: "config/label_registry/delete", | ||||||
|     label_id: labelId, |     label_id: labelId, | ||||||
|   }); |   }); | ||||||
|  |  | ||||||
|  | export const getLabels = ( | ||||||
|  |   hass: HomeAssistant, | ||||||
|  |   labels?: LabelRegistryEntry[], | ||||||
|  |   includeDomains?: string[], | ||||||
|  |   excludeDomains?: string[], | ||||||
|  |   includeDeviceClasses?: string[], | ||||||
|  |   deviceFilter?: HaDevicePickerDeviceFilterFunc, | ||||||
|  |   entityFilter?: HaEntityPickerEntityFilterFunc, | ||||||
|  |   excludeLabels?: string[] | ||||||
|  | ): PickerComboBoxItem[] => { | ||||||
|  |   if (!labels || labels.length === 0) { | ||||||
|  |     return []; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const devices = Object.values(hass.devices); | ||||||
|  |   const entities = Object.values(hass.entities); | ||||||
|  |  | ||||||
|  |   let deviceEntityLookup: DeviceEntityDisplayLookup = {}; | ||||||
|  |   let inputDevices: DeviceRegistryEntry[] | undefined; | ||||||
|  |   let inputEntities: EntityRegistryDisplayEntry[] | undefined; | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     includeDomains || | ||||||
|  |     excludeDomains || | ||||||
|  |     includeDeviceClasses || | ||||||
|  |     deviceFilter || | ||||||
|  |     entityFilter | ||||||
|  |   ) { | ||||||
|  |     deviceEntityLookup = getDeviceEntityDisplayLookup(entities); | ||||||
|  |     inputDevices = devices; | ||||||
|  |     inputEntities = entities.filter((entity) => entity.labels.length > 0); | ||||||
|  |  | ||||||
|  |     if (includeDomains) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => { | ||||||
|  |         const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |         if (!devEntities || !devEntities.length) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return deviceEntityLookup[device.id].some((entity) => | ||||||
|  |           includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |       inputEntities = inputEntities!.filter((entity) => | ||||||
|  |         includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (excludeDomains) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => { | ||||||
|  |         const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |         if (!devEntities || !devEntities.length) { | ||||||
|  |           return true; | ||||||
|  |         } | ||||||
|  |         return entities.every( | ||||||
|  |           (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |       inputEntities = inputEntities!.filter( | ||||||
|  |         (entity) => !excludeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |       ); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (includeDeviceClasses) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => { | ||||||
|  |         const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |         if (!devEntities || !devEntities.length) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return deviceEntityLookup[device.id].some((entity) => { | ||||||
|  |           const stateObj = hass.states[entity.entity_id]; | ||||||
|  |           if (!stateObj) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |           return ( | ||||||
|  |             stateObj.attributes.device_class && | ||||||
|  |             includeDeviceClasses.includes(stateObj.attributes.device_class) | ||||||
|  |           ); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |       inputEntities = inputEntities!.filter((entity) => { | ||||||
|  |         const stateObj = hass.states[entity.entity_id]; | ||||||
|  |         return ( | ||||||
|  |           stateObj.attributes.device_class && | ||||||
|  |           includeDeviceClasses.includes(stateObj.attributes.device_class) | ||||||
|  |         ); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (deviceFilter) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => deviceFilter!(device)); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (entityFilter) { | ||||||
|  |       inputDevices = inputDevices!.filter((device) => { | ||||||
|  |         const devEntities = deviceEntityLookup[device.id]; | ||||||
|  |         if (!devEntities || !devEntities.length) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return deviceEntityLookup[device.id].some((entity) => { | ||||||
|  |           const stateObj = hass.states[entity.entity_id]; | ||||||
|  |           if (!stateObj) { | ||||||
|  |             return false; | ||||||
|  |           } | ||||||
|  |           return entityFilter(stateObj); | ||||||
|  |         }); | ||||||
|  |       }); | ||||||
|  |       inputEntities = inputEntities!.filter((entity) => { | ||||||
|  |         const stateObj = hass.states[entity.entity_id]; | ||||||
|  |         if (!stateObj) { | ||||||
|  |           return false; | ||||||
|  |         } | ||||||
|  |         return entityFilter!(stateObj); | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   let outputLabels = labels; | ||||||
|  |   const usedLabels = new Set<string>(); | ||||||
|  |  | ||||||
|  |   let areaIds: string[] | undefined; | ||||||
|  |  | ||||||
|  |   if (inputDevices) { | ||||||
|  |     areaIds = inputDevices | ||||||
|  |       .filter((device) => device.area_id) | ||||||
|  |       .map((device) => device.area_id!); | ||||||
|  |  | ||||||
|  |     inputDevices.forEach((device) => { | ||||||
|  |       device.labels.forEach((label) => usedLabels.add(label)); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (inputEntities) { | ||||||
|  |     areaIds = (areaIds ?? []).concat( | ||||||
|  |       inputEntities | ||||||
|  |         .filter((entity) => entity.area_id) | ||||||
|  |         .map((entity) => entity.area_id!) | ||||||
|  |     ); | ||||||
|  |     inputEntities.forEach((entity) => { | ||||||
|  |       entity.labels.forEach((label) => usedLabels.add(label)); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (areaIds) { | ||||||
|  |     areaIds.forEach((areaId) => { | ||||||
|  |       const area = hass.areas[areaId]; | ||||||
|  |       area.labels.forEach((label) => usedLabels.add(label)); | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (excludeLabels) { | ||||||
|  |     outputLabels = outputLabels.filter( | ||||||
|  |       (label) => !excludeLabels!.includes(label.label_id) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (inputDevices || inputEntities) { | ||||||
|  |     outputLabels = outputLabels.filter((label) => | ||||||
|  |       usedLabels.has(label.label_id) | ||||||
|  |     ); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const items = outputLabels.map<PickerComboBoxItem>((label) => ({ | ||||||
|  |     id: label.label_id, | ||||||
|  |     primary: label.name, | ||||||
|  |     icon: label.icon || undefined, | ||||||
|  |     icon_path: label.icon ? undefined : mdiLabel, | ||||||
|  |     sorting_label: label.name, | ||||||
|  |     search_labels: [label.name, label.label_id, label.description].filter( | ||||||
|  |       (v): v is string => Boolean(v) | ||||||
|  |     ), | ||||||
|  |   })); | ||||||
|  |  | ||||||
|  |   return items; | ||||||
|  | }; | ||||||
|   | |||||||
							
								
								
									
										164
									
								
								src/data/target.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										164
									
								
								src/data/target.ts
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,164 @@ | |||||||
|  | import type { HassServiceTarget } from "home-assistant-js-websocket"; | ||||||
|  | import { computeDomain } from "../common/entity/compute_domain"; | ||||||
|  | import type { HaDevicePickerDeviceFilterFunc } from "../components/device/ha-device-picker"; | ||||||
|  | import type { HomeAssistant } from "../types"; | ||||||
|  | import type { AreaRegistryEntry } from "./area_registry"; | ||||||
|  | import type { DeviceRegistryEntry } from "./device_registry"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "./entity"; | ||||||
|  | import type { EntityRegistryDisplayEntry } from "./entity_registry"; | ||||||
|  |  | ||||||
|  | export type TargetType = "entity" | "device" | "area" | "label" | "floor"; | ||||||
|  | export type TargetTypeFloorless = Exclude<TargetType, "floor">; | ||||||
|  |  | ||||||
|  | export interface ExtractFromTargetResult { | ||||||
|  |   missing_areas: string[]; | ||||||
|  |   missing_devices: string[]; | ||||||
|  |   missing_floors: string[]; | ||||||
|  |   missing_labels: string[]; | ||||||
|  |   referenced_areas: string[]; | ||||||
|  |   referenced_devices: string[]; | ||||||
|  |   referenced_entities: string[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export interface ExtractFromTargetResultReferenced { | ||||||
|  |   referenced_areas: string[]; | ||||||
|  |   referenced_devices: string[]; | ||||||
|  |   referenced_entities: string[]; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | export const extractFromTarget = async ( | ||||||
|  |   hass: HomeAssistant, | ||||||
|  |   target: HassServiceTarget | ||||||
|  | ) => | ||||||
|  |   hass.callWS<ExtractFromTargetResult>({ | ||||||
|  |     type: "extract_from_target", | ||||||
|  |     target, | ||||||
|  |   }); | ||||||
|  |  | ||||||
|  | export const areaMeetsFilter = ( | ||||||
|  |   area: AreaRegistryEntry, | ||||||
|  |   devices: HomeAssistant["devices"], | ||||||
|  |   entities: HomeAssistant["entities"], | ||||||
|  |   deviceFilter?: HaDevicePickerDeviceFilterFunc, | ||||||
|  |   includeDomains?: string[], | ||||||
|  |   includeDeviceClasses?: string[], | ||||||
|  |   states?: HomeAssistant["states"], | ||||||
|  |   entityFilter?: HaEntityPickerEntityFilterFunc | ||||||
|  | ): boolean => { | ||||||
|  |   const areaDevices = Object.values(devices).filter( | ||||||
|  |     (device) => device.area_id === area.area_id | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     areaDevices.some((device) => | ||||||
|  |       deviceMeetsFilter( | ||||||
|  |         device, | ||||||
|  |         entities, | ||||||
|  |         deviceFilter, | ||||||
|  |         includeDomains, | ||||||
|  |         includeDeviceClasses, | ||||||
|  |         states, | ||||||
|  |         entityFilter | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   ) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   const areaEntities = Object.values(entities).filter( | ||||||
|  |     (entity) => entity.area_id === area.area_id | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     areaEntities.some((entity) => | ||||||
|  |       entityRegMeetsFilter( | ||||||
|  |         entity, | ||||||
|  |         false, | ||||||
|  |         includeDomains, | ||||||
|  |         includeDeviceClasses, | ||||||
|  |         states, | ||||||
|  |         entityFilter | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   ) { | ||||||
|  |     return true; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return false; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const deviceMeetsFilter = ( | ||||||
|  |   device: DeviceRegistryEntry, | ||||||
|  |   entities: HomeAssistant["entities"], | ||||||
|  |   deviceFilter?: HaDevicePickerDeviceFilterFunc, | ||||||
|  |   includeDomains?: string[], | ||||||
|  |   includeDeviceClasses?: string[], | ||||||
|  |   states?: HomeAssistant["states"], | ||||||
|  |   entityFilter?: HaEntityPickerEntityFilterFunc | ||||||
|  | ): boolean => { | ||||||
|  |   const devEntities = Object.values(entities).filter( | ||||||
|  |     (entity) => entity.device_id === device.id | ||||||
|  |   ); | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     !devEntities.some((entity) => | ||||||
|  |       entityRegMeetsFilter( | ||||||
|  |         entity, | ||||||
|  |         false, | ||||||
|  |         includeDomains, | ||||||
|  |         includeDeviceClasses, | ||||||
|  |         states, | ||||||
|  |         entityFilter | ||||||
|  |       ) | ||||||
|  |     ) | ||||||
|  |   ) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (deviceFilter) { | ||||||
|  |     return deviceFilter(device); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   return true; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | export const entityRegMeetsFilter = ( | ||||||
|  |   entity: EntityRegistryDisplayEntry, | ||||||
|  |   includeSecondary = false, | ||||||
|  |   includeDomains?: string[], | ||||||
|  |   includeDeviceClasses?: string[], | ||||||
|  |   states?: HomeAssistant["states"], | ||||||
|  |   entityFilter?: HaEntityPickerEntityFilterFunc | ||||||
|  | ): boolean => { | ||||||
|  |   if (entity.hidden || (entity.entity_category && !includeSecondary)) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if ( | ||||||
|  |     includeDomains && | ||||||
|  |     !includeDomains.includes(computeDomain(entity.entity_id)) | ||||||
|  |   ) { | ||||||
|  |     return false; | ||||||
|  |   } | ||||||
|  |   if (includeDeviceClasses) { | ||||||
|  |     const stateObj = states?.[entity.entity_id]; | ||||||
|  |     if (!stateObj) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     if ( | ||||||
|  |       !stateObj.attributes.device_class || | ||||||
|  |       !includeDeviceClasses!.includes(stateObj.attributes.device_class) | ||||||
|  |     ) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   if (entityFilter) { | ||||||
|  |     const stateObj = states?.[entity.entity_id]; | ||||||
|  |     if (!stateObj) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|  |     return entityFilter!(stateObj); | ||||||
|  |   } | ||||||
|  |   return true; | ||||||
|  | }; | ||||||
| @@ -472,7 +472,10 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|     this._step = undefined; |     this._step = undefined; | ||||||
|     await this.updateComplete; |     await this.updateComplete; | ||||||
|     this._step = _step; |     this._step = _step; | ||||||
|     if (_step.type === "create_entry" && _step.next_flow) { |     if ( | ||||||
|  |       (_step.type === "create_entry" || _step.type === "abort") && | ||||||
|  |       _step.next_flow | ||||||
|  |     ) { | ||||||
|       // skip device rename if there is a chained flow |       // skip device rename if there is a chained flow | ||||||
|       this._step = undefined; |       this._step = undefined; | ||||||
|       this._handler = undefined; |       this._handler = undefined; | ||||||
| @@ -486,32 +489,36 @@ class DataEntryFlowDialog extends LitElement { | |||||||
|           carryOverDevices: this._devices( |           carryOverDevices: this._devices( | ||||||
|             this._params!.flowConfig.showDevices, |             this._params!.flowConfig.showDevices, | ||||||
|             Object.values(this.hass.devices), |             Object.values(this.hass.devices), | ||||||
|             _step.result?.entry_id, |             _step.type === "create_entry" ? _step.result?.entry_id : undefined, | ||||||
|             this._params!.carryOverDevices |             this._params!.carryOverDevices | ||||||
|           ).map((device) => device.id), |           ).map((device) => device.id), | ||||||
|           dialogClosedCallback: this._params!.dialogClosedCallback, |           dialogClosedCallback: this._params!.dialogClosedCallback, | ||||||
|         }); |         }); | ||||||
|       } else if (_step.next_flow[0] === "options_flow") { |       } else if (_step.next_flow[0] === "options_flow") { | ||||||
|         showOptionsFlowDialog( |         if (_step.type === "create_entry") { | ||||||
|           this._params!.dialogParentElement!, |           showOptionsFlowDialog( | ||||||
|           _step.result!, |             this._params!.dialogParentElement!, | ||||||
|           { |             _step.result!, | ||||||
|             continueFlowId: _step.next_flow[1], |             { | ||||||
|             navigateToResult: this._params!.navigateToResult, |               continueFlowId: _step.next_flow[1], | ||||||
|             dialogClosedCallback: this._params!.dialogClosedCallback, |               navigateToResult: this._params!.navigateToResult, | ||||||
|           } |               dialogClosedCallback: this._params!.dialogClosedCallback, | ||||||
|         ); |             } | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|       } else if (_step.next_flow[0] === "config_subentries_flow") { |       } else if (_step.next_flow[0] === "config_subentries_flow") { | ||||||
|         showSubConfigFlowDialog( |         if (_step.type === "create_entry") { | ||||||
|           this._params!.dialogParentElement!, |           showSubConfigFlowDialog( | ||||||
|           _step.result!, |             this._params!.dialogParentElement!, | ||||||
|           _step.next_flow[0], |             _step.result!, | ||||||
|           { |             _step.next_flow[0], | ||||||
|             continueFlowId: _step.next_flow[1], |             { | ||||||
|             navigateToResult: this._params!.navigateToResult, |               continueFlowId: _step.next_flow[1], | ||||||
|             dialogClosedCallback: this._params!.dialogClosedCallback, |               navigateToResult: this._params!.navigateToResult, | ||||||
|           } |               dialogClosedCallback: this._params!.dialogClosedCallback, | ||||||
|         ); |             } | ||||||
|  |           ); | ||||||
|  |         } | ||||||
|       } else { |       } else { | ||||||
|         this.closeDialog(); |         this.closeDialog(); | ||||||
|         showAlertDialog(this._params!.dialogParentElement!, { |         showAlertDialog(this._params!.dialogParentElement!, { | ||||||
|   | |||||||
| @@ -77,84 +77,80 @@ class MoreInfoMediaPlayer extends LitElement { | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (!stateActive(this.stateObj)) { |  | ||||||
|       return nothing; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const supportsMute = supportsFeature( |     const supportsMute = supportsFeature( | ||||||
|       this.stateObj, |       this.stateObj, | ||||||
|       MediaPlayerEntityFeature.VOLUME_MUTE |       MediaPlayerEntityFeature.VOLUME_MUTE | ||||||
|     ); |     ); | ||||||
|     const supportsSet = supportsFeature( |     const supportsSliding = supportsFeature( | ||||||
|       this.stateObj, |       this.stateObj, | ||||||
|       MediaPlayerEntityFeature.VOLUME_SET |       MediaPlayerEntityFeature.VOLUME_SET | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const supportsStep = supportsFeature( |     return html`${(supportsFeature( | ||||||
|       this.stateObj, |       this.stateObj!, | ||||||
|       MediaPlayerEntityFeature.VOLUME_STEP |       MediaPlayerEntityFeature.VOLUME_SET | ||||||
|     ); |     ) || | ||||||
|  |       supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) && | ||||||
|     if (!supportsMute && !supportsSet && !supportsStep) { |     stateActive(this.stateObj!) | ||||||
|       return nothing; |       ? html` | ||||||
|     } |           <div class="volume"> | ||||||
|  |             ${supportsMute | ||||||
|     return html` |               ? html` | ||||||
|       <div class="volume"> |                   <ha-icon-button | ||||||
|         ${supportsMute |                     .path=${this.stateObj.attributes.is_volume_muted | ||||||
|           ? html` |                       ? mdiVolumeOff | ||||||
|               <ha-icon-button |                       : mdiVolumeHigh} | ||||||
|                 .path=${this.stateObj.attributes.is_volume_muted |                     .label=${this.hass.localize( | ||||||
|                   ? mdiVolumeOff |                       `ui.card.media_player.${ | ||||||
|                   : mdiVolumeHigh} |                         this.stateObj.attributes.is_volume_muted | ||||||
|                 .label=${this.hass.localize( |                           ? "media_volume_unmute" | ||||||
|                   `ui.card.media_player.${ |                           : "media_volume_mute" | ||||||
|                     this.stateObj.attributes.is_volume_muted |                       }` | ||||||
|                       ? "media_volume_unmute" |                     )} | ||||||
|                       : "media_volume_mute" |                     @click=${this._toggleMute} | ||||||
|                   }` |                   ></ha-icon-button> | ||||||
|                 )} |                 ` | ||||||
|                 @click=${this._toggleMute} |               : ""} | ||||||
|               ></ha-icon-button> |             ${supportsFeature( | ||||||
|             ` |               this.stateObj, | ||||||
|           : nothing} |               MediaPlayerEntityFeature.VOLUME_STEP | ||||||
|         ${supportsStep |             ) && !supportsSliding | ||||||
|           ? html` <ha-icon-button |               ? html` | ||||||
|               action="volume_down" |                   <ha-icon-button | ||||||
|               .path=${mdiVolumeMinus} |                     action="volume_down" | ||||||
|               .label=${this.hass.localize( |                     .path=${mdiVolumeMinus} | ||||||
|                 "ui.card.media_player.media_volume_down" |                     .label=${this.hass.localize( | ||||||
|               )} |                       "ui.card.media_player.media_volume_down" | ||||||
|               @click=${this._handleClick} |                     )} | ||||||
|             ></ha-icon-button>` |                     @click=${this._handleClick} | ||||||
|           : nothing} |                   ></ha-icon-button> | ||||||
|         ${supportsSet |                   <ha-icon-button | ||||||
|           ? html` |                     action="volume_up" | ||||||
|               ${!supportsMute && !supportsStep |                     .path=${mdiVolumePlus} | ||||||
|                 ? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>` |                     .label=${this.hass.localize( | ||||||
|                 : nothing} |                       "ui.card.media_player.media_volume_up" | ||||||
|               <ha-slider |                     )} | ||||||
|                 labeled |                     @click=${this._handleClick} | ||||||
|                 id="input" |                   ></ha-icon-button> | ||||||
|                 .value=${Number(this.stateObj.attributes.volume_level) * 100} |                 ` | ||||||
|                 @change=${this._selectedValueChanged} |               : nothing} | ||||||
|               ></ha-slider> |             ${supportsSliding | ||||||
|             ` |               ? html` | ||||||
|           : nothing} |                   ${!supportsMute | ||||||
|         ${supportsStep |                     ? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>` | ||||||
|           ? html` |                     : nothing} | ||||||
|               <ha-icon-button |                   <ha-slider | ||||||
|                 action="volume_up" |                     labeled | ||||||
|                 .path=${mdiVolumePlus} |                     id="input" | ||||||
|                 .label=${this.hass.localize( |                     .value=${Number(this.stateObj.attributes.volume_level) * | ||||||
|                   "ui.card.media_player.media_volume_up" |                     100} | ||||||
|                 )} |                     @change=${this._selectedValueChanged} | ||||||
|                 @click=${this._handleClick} |                   ></ha-slider> | ||||||
|               ></ha-icon-button> |                 ` | ||||||
|             ` |               : nothing} | ||||||
|           : nothing} |           </div> | ||||||
|       </div> |         ` | ||||||
|     `; |       : nothing}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected _renderSourceControl() { |   protected _renderSourceControl() { | ||||||
|   | |||||||
| @@ -15,7 +15,6 @@ import type { PropertyValues } from "lit"; | |||||||
| import { LitElement, css, html, nothing } from "lit"; | import { LitElement, css, html, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { cache } from "lit/directives/cache"; | import { cache } from "lit/directives/cache"; | ||||||
| import { join } from "lit/directives/join"; |  | ||||||
| import { keyed } from "lit/directives/keyed"; | import { keyed } from "lit/directives/keyed"; | ||||||
| import { dynamicElement } from "../../common/dom/dynamic-element-directive"; | import { dynamicElement } from "../../common/dom/dynamic-element-directive"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| @@ -33,6 +32,7 @@ import { | |||||||
| } from "../../common/entity/context/get_entity_context"; | } from "../../common/entity/context/get_entity_context"; | ||||||
| import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event"; | import { shouldHandleRequestSelectedEvent } from "../../common/mwc/handle-request-selected-event"; | ||||||
| import { navigate } from "../../common/navigate"; | import { navigate } from "../../common/navigate"; | ||||||
|  | import { computeRTL } from "../../common/util/compute_rtl"; | ||||||
| import "../../components/ha-button-menu"; | import "../../components/ha-button-menu"; | ||||||
| import "../../components/ha-dialog"; | import "../../components/ha-dialog"; | ||||||
| import "../../components/ha-dialog-header"; | import "../../components/ha-dialog-header"; | ||||||
| @@ -361,6 +361,8 @@ export class MoreInfoDialog extends LitElement { | |||||||
|     ); |     ); | ||||||
|     const title = this._childView?.viewTitle || breadcrumb.pop() || entityId; |     const title = this._childView?.viewTitle || breadcrumb.pop() || entityId; | ||||||
|  |  | ||||||
|  |     const isRTL = computeRTL(this.hass); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-dialog | ||||||
|         open |         open | ||||||
| @@ -394,17 +396,13 @@ export class MoreInfoDialog extends LitElement { | |||||||
|             ${breadcrumb.length > 0 |             ${breadcrumb.length > 0 | ||||||
|               ? !__DEMO__ && isAdmin |               ? !__DEMO__ && isAdmin | ||||||
|                 ? html` |                 ? html` | ||||||
|                     <button |                     <button class="breadcrumb" @click=${this._breadcrumbClick}> | ||||||
|                       class="breadcrumb" |                       ${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")} | ||||||
|                       @click=${this._breadcrumbClick} |  | ||||||
|                       aria-label=${breadcrumb.join(" > ")} |  | ||||||
|                     > |  | ||||||
|                       ${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)} |  | ||||||
|                     </button> |                     </button> | ||||||
|                   ` |                   ` | ||||||
|                 : html` |                 : html` | ||||||
|                     <p class="breadcrumb"> |                     <p class="breadcrumb"> | ||||||
|                       ${join(breadcrumb, html`<ha-icon-next></ha-icon-next>`)} |                       ${breadcrumb.join(isRTL ? " ◂ " : " ▸ ")} | ||||||
|                     </p> |                     </p> | ||||||
|                   ` |                   ` | ||||||
|               : nothing} |               : nothing} | ||||||
|   | |||||||
| @@ -1,14 +1,10 @@ | |||||||
| import { mdiClose } from "@mdi/js"; |  | ||||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||||
| import type { CSSResultGroup } from "lit"; | import type { CSSResultGroup } from "lit"; | ||||||
| import { LitElement, css, html, nothing } from "lit"; | import { LitElement, css, html } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../common/dom/fire_event"; | import { fireEvent } from "../../common/dom/fire_event"; | ||||||
| import "../../components/ha-alert"; | import "../../components/ha-alert"; | ||||||
| import "../../components/ha-dialog-header"; | import "../../components/ha-wa-dialog"; | ||||||
| import "../../components/ha-icon-button"; |  | ||||||
| import "../../components/ha-md-dialog"; |  | ||||||
| import type { HaMdDialog } from "../../components/ha-md-dialog"; |  | ||||||
| import "../../components/ha-spinner"; | import "../../components/ha-spinner"; | ||||||
| import { | import { | ||||||
|   subscribeBackupEvents, |   subscribeBackupEvents, | ||||||
| @@ -37,8 +33,6 @@ class DialogRestartWait extends LitElement { | |||||||
|  |  | ||||||
|   private _backupEventsSubscription?: Promise<UnsubscribeFunc>; |   private _backupEventsSubscription?: Promise<UnsubscribeFunc>; | ||||||
|  |  | ||||||
|   @query("ha-md-dialog") private _dialog?: HaMdDialog; |  | ||||||
|  |  | ||||||
|   public async showDialog(params: RestartWaitDialogParams): Promise<void> { |   public async showDialog(params: RestartWaitDialogParams): Promise<void> { | ||||||
|     this._open = true; |     this._open = true; | ||||||
|     this._loadBackupState(); |     this._loadBackupState(); | ||||||
| @@ -49,9 +43,11 @@ class DialogRestartWait extends LitElement { | |||||||
|     this._actionOnIdle = params.action; |     this._actionOnIdle = params.action; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _dialogClosed(): void { |   public closeDialog(): void { | ||||||
|     this._open = false; |     this._open = false; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   private _dialogClosed(): void { | ||||||
|     if (this._backupEventsSubscription) { |     if (this._backupEventsSubscription) { | ||||||
|       this._backupEventsSubscription.then((unsub) => { |       this._backupEventsSubscription.then((unsub) => { | ||||||
|         unsub(); |         unsub(); | ||||||
| @@ -62,10 +58,6 @@ class DialogRestartWait extends LitElement { | |||||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); |     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public closeDialog(): void { |  | ||||||
|     this._dialog?.close(); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _getWaitMessage() { |   private _getWaitMessage() { | ||||||
|     switch (this._backupState) { |     switch (this._backupState) { | ||||||
|       case "create_backup": |       case "create_backup": | ||||||
| @@ -80,28 +72,17 @@ class DialogRestartWait extends LitElement { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     if (!this._open) { |  | ||||||
|       return nothing; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const waitMessage = this._getWaitMessage(); |     const waitMessage = this._getWaitMessage(); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-md-dialog |       <ha-wa-dialog | ||||||
|         open |         .hass=${this.hass} | ||||||
|  |         .open=${this._open} | ||||||
|  |         .headerTitle=${this._title} | ||||||
|  |         width="medium" | ||||||
|         @closed=${this._dialogClosed} |         @closed=${this._dialogClosed} | ||||||
|         .disableCancelAction=${true} |  | ||||||
|       > |       > | ||||||
|         <ha-dialog-header slot="headline"> |         <div class="content"> | ||||||
|           <ha-icon-button |  | ||||||
|             slot="navigationIcon" |  | ||||||
|             .label=${this.hass.localize("ui.common.cancel")} |  | ||||||
|             .path=${mdiClose} |  | ||||||
|             @click=${this.closeDialog} |  | ||||||
|           ></ha-icon-button> |  | ||||||
|           <span slot="title" .title=${this._title}> ${this._title} </span> |  | ||||||
|         </ha-dialog-header> |  | ||||||
|         <div slot="content" class="content"> |  | ||||||
|           ${this._error |           ${this._error | ||||||
|             ? html`<ha-alert alert-type="error" |             ? html`<ha-alert alert-type="error" | ||||||
|                 >${this.hass.localize("ui.dialogs.restart.error_backup_state", { |                 >${this.hass.localize("ui.dialogs.restart.error_backup_state", { | ||||||
| @@ -113,7 +94,7 @@ class DialogRestartWait extends LitElement { | |||||||
|                 ${waitMessage} |                 ${waitMessage} | ||||||
|               `} |               `} | ||||||
|         </div> |         </div> | ||||||
|       </ha-md-dialog> |       </ha-wa-dialog> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -139,15 +120,9 @@ class DialogRestartWait extends LitElement { | |||||||
|       haStyle, |       haStyle, | ||||||
|       haStyleDialog, |       haStyleDialog, | ||||||
|       css` |       css` | ||||||
|         ha-md-dialog { |         ha-wa-dialog { | ||||||
|           --dialog-content-padding: 0; |           --dialog-content-padding: 0; | ||||||
|         } |         } | ||||||
|         @media all and (min-width: 550px) { |  | ||||||
|           ha-md-dialog { |  | ||||||
|             min-width: 500px; |  | ||||||
|             max-width: 500px; |  | ||||||
|           } |  | ||||||
|         } |  | ||||||
|         .content { |         .content { | ||||||
|           display: flex; |           display: flex; | ||||||
|           flex-direction: column; |           flex-direction: column; | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ const COMPONENTS = { | |||||||
|   "media-browser": () => |   "media-browser": () => | ||||||
|     import("../panels/media-browser/ha-panel-media-browser"), |     import("../panels/media-browser/ha-panel-media-browser"), | ||||||
|   light: () => import("../panels/light/ha-panel-light"), |   light: () => import("../panels/light/ha-panel-light"), | ||||||
|   security: () => import("../panels/security/ha-panel-security"), |   safety: () => import("../panels/safety/ha-panel-safety"), | ||||||
|   climate: () => import("../panels/climate/ha-panel-climate"), |   climate: () => import("../panels/climate/ha-panel-climate"), | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,5 +1,6 @@ | |||||||
| import { css, html, LitElement, nothing } from "lit"; | import { css, html, LitElement, nothing, type PropertyValues } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, query, state } from "lit/decorators"; | ||||||
|  | import { tinykeys } from "tinykeys"; | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { computeRTL } from "../../../common/util/compute_rtl"; | import { computeRTL } from "../../../common/util/compute_rtl"; | ||||||
| import "../../../components/ha-resizable-bottom-sheet"; | import "../../../components/ha-resizable-bottom-sheet"; | ||||||
| @@ -44,11 +45,27 @@ export default class HaAutomationSidebar extends LitElement { | |||||||
|   @query("ha-resizable-bottom-sheet") |   @query("ha-resizable-bottom-sheet") | ||||||
|   private _bottomSheetElement?: HaResizableBottomSheet; |   private _bottomSheetElement?: HaResizableBottomSheet; | ||||||
|  |  | ||||||
|  |   @query(".handle") | ||||||
|  |   private _handleElement?: HTMLDivElement; | ||||||
|  |  | ||||||
|   private _resizeStartX = 0; |   private _resizeStartX = 0; | ||||||
|  |  | ||||||
|  |   private _tinykeysUnsub?: () => void; | ||||||
|  |  | ||||||
|  |   protected updated(changedProperties: PropertyValues) { | ||||||
|  |     super.updated(changedProperties); | ||||||
|  |     if (changedProperties.has("config") || changedProperties.has("narrow")) { | ||||||
|  |       if (!this.config || this.narrow) { | ||||||
|  |         this._tinykeysUnsub?.(); | ||||||
|  |         this._tinykeysUnsub = undefined; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   disconnectedCallback() { |   disconnectedCallback() { | ||||||
|     super.disconnectedCallback(); |     super.disconnectedCallback(); | ||||||
|     this._unregisterResizeHandlers(); |     this._unregisterResizeHandlers(); | ||||||
|  |     this._tinykeysUnsub?.(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _renderContent() { |   private _renderContent() { | ||||||
| @@ -170,6 +187,9 @@ export default class HaAutomationSidebar extends LitElement { | |||||||
|         class="handle ${this._resizing ? "resizing" : ""}" |         class="handle ${this._resizing ? "resizing" : ""}" | ||||||
|         @mousedown=${this._handleMouseDown} |         @mousedown=${this._handleMouseDown} | ||||||
|         @touchstart=${this._handleMouseDown} |         @touchstart=${this._handleMouseDown} | ||||||
|  |         @focus=${this._startKeyboardResizing} | ||||||
|  |         @blur=${this._stopKeyboardResizing} | ||||||
|  |         tabindex="0" | ||||||
|       > |       > | ||||||
|         <div class="indicator ${this._resizing ? "" : "hidden"}"></div> |         <div class="indicator ${this._resizing ? "" : "hidden"}"></div> | ||||||
|       </div> |       </div> | ||||||
| @@ -288,6 +308,44 @@ export default class HaAutomationSidebar extends LitElement { | |||||||
|     document.removeEventListener("touchcancel", this._endResizing); |     document.removeEventListener("touchcancel", this._endResizing); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _startKeyboardResizing = (ev: KeyboardEvent) => { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     this._resizing = true; | ||||||
|  |     this._resizeStartX = 0; | ||||||
|  |     this._tinykeysUnsub = tinykeys(this._handleElement!, { | ||||||
|  |       ArrowLeft: this._increaseSize, | ||||||
|  |       ArrowRight: this._decreaseSize, | ||||||
|  |     }); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _stopKeyboardResizing = (ev: KeyboardEvent) => { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |     this._resizing = false; | ||||||
|  |     fireEvent(this, "sidebar-resizing-stopped"); | ||||||
|  |     this._tinykeysUnsub?.(); | ||||||
|  |     this._tinykeysUnsub = undefined; | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _increaseSize = (ev: KeyboardEvent) => { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |  | ||||||
|  |     this._resizeStartX -= computeRTL(this.hass) ? 10 : -10; | ||||||
|  |     this._keyboardResize(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _decreaseSize = (ev: KeyboardEvent) => { | ||||||
|  |     ev.stopPropagation(); | ||||||
|  |  | ||||||
|  |     this._resizeStartX += computeRTL(this.hass) ? 10 : -10; | ||||||
|  |     this._keyboardResize(); | ||||||
|  |   }; | ||||||
|  |  | ||||||
|  |   private _keyboardResize() { | ||||||
|  |     fireEvent(this, "sidebar-resized", { | ||||||
|  |       deltaInPx: this._resizeStartX, | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static styles = css` | ||||||
|     :host { |     :host { | ||||||
|       z-index: 6; |       z-index: 6; | ||||||
| @@ -342,6 +400,10 @@ export default class HaAutomationSidebar extends LitElement { | |||||||
|       transform: scale3d(0, 1, 1); |       transform: scale3d(0, 1, 1); | ||||||
|       opacity: 0; |       opacity: 0; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     .handle:focus-visible { | ||||||
|  |       outline: none; | ||||||
|  |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import { | |||||||
|   mdiChartBox, |   mdiChartBox, | ||||||
|   mdiCog, |   mdiCog, | ||||||
|   mdiFolder, |   mdiFolder, | ||||||
|  |   mdiInformation, | ||||||
|   mdiPlayBoxMultiple, |   mdiPlayBoxMultiple, | ||||||
|   mdiPuzzle, |   mdiPuzzle, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
| @@ -11,6 +12,7 @@ import { customElement, property, state } from "lit/decorators"; | |||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; | import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; | ||||||
| import { fireEvent } from "../../../../../common/dom/fire_event"; | import { fireEvent } from "../../../../../common/dom/fire_event"; | ||||||
|  | import "../../../../../components/ha-alert"; | ||||||
| import "../../../../../components/ha-button"; | import "../../../../../components/ha-button"; | ||||||
| import "../../../../../components/ha-expansion-panel"; | import "../../../../../components/ha-expansion-panel"; | ||||||
| import "../../../../../components/ha-md-list"; | import "../../../../../components/ha-md-list"; | ||||||
| @@ -18,10 +20,15 @@ import "../../../../../components/ha-md-list-item"; | |||||||
| import "../../../../../components/ha-md-select"; | import "../../../../../components/ha-md-select"; | ||||||
| import type { HaMdSelect } from "../../../../../components/ha-md-select"; | import type { HaMdSelect } from "../../../../../components/ha-md-select"; | ||||||
| import "../../../../../components/ha-md-select-option"; | import "../../../../../components/ha-md-select-option"; | ||||||
|  | import "../../../../../components/ha-spinner"; | ||||||
| import "../../../../../components/ha-switch"; | import "../../../../../components/ha-switch"; | ||||||
| import type { HaSwitch } from "../../../../../components/ha-switch"; | import type { HaSwitch } from "../../../../../components/ha-switch"; | ||||||
|  | import "../../../../../components/ha-tooltip"; | ||||||
| import { fetchHassioAddonsInfo } from "../../../../../data/hassio/addon"; | import { fetchHassioAddonsInfo } from "../../../../../data/hassio/addon"; | ||||||
|  | import type { HostDisksUsage } from "../../../../../data/hassio/host"; | ||||||
|  | import { fetchHostDisksUsage } from "../../../../../data/hassio/host"; | ||||||
| import type { HomeAssistant } from "../../../../../types"; | import type { HomeAssistant } from "../../../../../types"; | ||||||
|  | import { bytesToString } from "../../../../../util/bytes-to-string"; | ||||||
| import "../ha-backup-addons-picker"; | import "../ha-backup-addons-picker"; | ||||||
| import type { BackupAddonItem } from "../ha-backup-addons-picker"; | import type { BackupAddonItem } from "../ha-backup-addons-picker"; | ||||||
| import { getRecorderInfo } from "../../../../../data/recorder"; | import { getRecorderInfo } from "../../../../../data/recorder"; | ||||||
| @@ -78,11 +85,14 @@ class HaBackupConfigData extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _showDbOption = true; |   @state() private _showDbOption = true; | ||||||
|  |  | ||||||
|  |   @state() private _storageInfo?: HostDisksUsage | null; | ||||||
|  |  | ||||||
|   protected firstUpdated(changedProperties: PropertyValues): void { |   protected firstUpdated(changedProperties: PropertyValues): void { | ||||||
|     super.firstUpdated(changedProperties); |     super.firstUpdated(changedProperties); | ||||||
|     this._checkDbOption(); |     this._checkDbOption(); | ||||||
|     if (isComponentLoaded(this.hass, "hassio")) { |     if (isComponentLoaded(this.hass, "hassio")) { | ||||||
|       this._fetchAddons(); |       this._fetchAddons(); | ||||||
|  |       this._fetchStorageInfo(); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -114,10 +124,68 @@ class HaBackupConfigData extends LitElement { | |||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async _fetchStorageInfo() { | ||||||
|  |     try { | ||||||
|  |       this._storageInfo = await fetchHostDisksUsage(this.hass); | ||||||
|  |     } catch (_err: any) { | ||||||
|  |       this._storageInfo = null; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private _hasLocalAddons(addons: BackupAddonItem[]): boolean { |   private _hasLocalAddons(addons: BackupAddonItem[]): boolean { | ||||||
|     return addons.some((addon) => addon.slug === "local"); |     return addons.some((addon) => addon.slug === "local"); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _estimateBackupSize = memoizeOne( | ||||||
|  |     ( | ||||||
|  |       data: FormData, | ||||||
|  |       storageInfo: HostDisksUsage | null | undefined, | ||||||
|  |       addonsLength: number | ||||||
|  |     ): { | ||||||
|  |       compressedBytes: number; | ||||||
|  |       addonsNotAccurate: boolean; | ||||||
|  |     } | null => { | ||||||
|  |       if (!storageInfo?.children) { | ||||||
|  |         return null; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       let totalBytes = 0; | ||||||
|  |  | ||||||
|  |       const segments: Record<string, number> = {}; | ||||||
|  |       storageInfo.children.forEach((child) => { | ||||||
|  |         segments[child.id] = child.used_bytes; | ||||||
|  |       }); | ||||||
|  |  | ||||||
|  |       if (data.homeassistant) { | ||||||
|  |         totalBytes += segments.homeassistant ?? 0; | ||||||
|  |       } | ||||||
|  |       if (data.media) { | ||||||
|  |         totalBytes += segments.media ?? 0; | ||||||
|  |       } | ||||||
|  |       if (data.share) { | ||||||
|  |         totalBytes += segments.share ?? 0; | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if ( | ||||||
|  |         data.addons_mode === "all" || | ||||||
|  |         (data.addons_mode === "custom" && data.addons.length > 0) | ||||||
|  |       ) { | ||||||
|  |         // It would be better if we could receive individual addon sizes in the WS request instead | ||||||
|  |         totalBytes += | ||||||
|  |           (segments.addons_data ?? 0) + (segments.addons_config ?? 0); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       return { | ||||||
|  |         // Estimate compressed size (40% reduction typical for gzip) | ||||||
|  |         compressedBytes: Math.round(totalBytes * 0.6), | ||||||
|  |         addonsNotAccurate: | ||||||
|  |           data.addons_mode === "custom" && | ||||||
|  |           data.addons.length > 0 && | ||||||
|  |           data.addons.length !== addonsLength, | ||||||
|  |       }; | ||||||
|  |     } | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   private _getData = memoizeOne( |   private _getData = memoizeOne( | ||||||
|     (value: BackupConfigData | undefined, showAddon: boolean): FormData => { |     (value: BackupConfigData | undefined, showAddon: boolean): FormData => { | ||||||
|       if (!value) { |       if (!value) { | ||||||
| @@ -171,6 +239,7 @@ class HaBackupConfigData extends LitElement { | |||||||
|     const isHassio = isComponentLoaded(this.hass, "hassio"); |     const isHassio = isComponentLoaded(this.hass, "hassio"); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|  |       ${this._renderSizeEstimate()} | ||||||
|       <ha-md-list> |       <ha-md-list> | ||||||
|         <ha-md-list-item> |         <ha-md-list-item> | ||||||
|           <ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon> |           <ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon> | ||||||
| @@ -381,7 +450,103 @@ class HaBackupConfigData extends LitElement { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _renderSizeEstimate() { | ||||||
|  |     if (!isComponentLoaded(this.hass, "hassio")) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const data = this._getData(this.value, this._showAddons); | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |       !( | ||||||
|  |         data.homeassistant || | ||||||
|  |         data.database || | ||||||
|  |         data.media || | ||||||
|  |         data.share || | ||||||
|  |         data.local_addons || | ||||||
|  |         data.addons_mode === "all" || | ||||||
|  |         (data.addons_mode === "custom" && data.addons.length > 0) | ||||||
|  |       ) | ||||||
|  |     ) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this._storageInfo === undefined) { | ||||||
|  |       return html` | ||||||
|  |         <ha-alert alert-type="info"> | ||||||
|  |           <ha-spinner slot="icon"></ha-spinner> | ||||||
|  |           ${this.hass.localize( | ||||||
|  |             "ui.panel.config.backup.data.estimated_size_loading" | ||||||
|  |           )} | ||||||
|  |         </ha-alert> | ||||||
|  |       `; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (!this._storageInfo || !this._storageInfo.children) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const result = this._estimateBackupSize( | ||||||
|  |       data, | ||||||
|  |       this._storageInfo, | ||||||
|  |       this._addons.length | ||||||
|  |     ); | ||||||
|  |     if (result === null) { | ||||||
|  |       return nothing; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const { compressedBytes, addonsNotAccurate } = result; | ||||||
|  |  | ||||||
|  |     return html` | ||||||
|  |       <span class="estimated-size"> | ||||||
|  |         <span class="estimated-size-heading"> | ||||||
|  |           ${this.hass.localize("ui.panel.config.backup.data.estimated_size")} | ||||||
|  |           <ha-svg-icon | ||||||
|  |             id="estimated-size-info" | ||||||
|  |             .path=${mdiInformation} | ||||||
|  |           ></ha-svg-icon> | ||||||
|  |           <ha-tooltip for="estimated-size-info" placement="right"> | ||||||
|  |             ${this.hass.localize( | ||||||
|  |               "ui.panel.config.backup.data.estimated_size_disclaimer" | ||||||
|  |             )} | ||||||
|  |             ${addonsNotAccurate | ||||||
|  |               ? html`<br /><br />${this.hass.localize( | ||||||
|  |                     "ui.panel.config.backup.data.estimated_size_disclaimer_addons_custom" | ||||||
|  |                   )}` | ||||||
|  |               : nothing} | ||||||
|  |           </ha-tooltip> | ||||||
|  |         </span> | ||||||
|  |         <span class="estimated-size-value"> | ||||||
|  |           ${bytesToString(compressedBytes)} | ||||||
|  |         </span> | ||||||
|  |       </span> | ||||||
|  |     `; | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static styles = css` | ||||||
|  |     .estimated-size { | ||||||
|  |       display: block; | ||||||
|  |       margin-top: var(--ha-space-2); | ||||||
|  |       font-size: var(--ha-font-size-m); | ||||||
|  |     } | ||||||
|  |     .estimated-size-heading { | ||||||
|  |       font-size: var(--ha-font-size-m); | ||||||
|  |       line-height: var(--ha-line-height-expanded); | ||||||
|  |     } | ||||||
|  |     .estimated-size-heading ha-svg-icon { | ||||||
|  |       --mdc-icon-size: 16px; | ||||||
|  |       color: var(--secondary-text-color); | ||||||
|  |       margin-inline-start: var(--ha-space-1); | ||||||
|  |       vertical-align: middle; | ||||||
|  |     } | ||||||
|  |     .estimated-size-value { | ||||||
|  |       display: block; | ||||||
|  |       font-size: var(--ha-font-size-s); | ||||||
|  |       color: var(--secondary-text-color); | ||||||
|  |     } | ||||||
|  |     ha-spinner { | ||||||
|  |       --ha-spinner-size: 24px; | ||||||
|  |     } | ||||||
|     ha-md-list { |     ha-md-list { | ||||||
|       background: none; |       background: none; | ||||||
|       --md-list-item-leading-space: 0; |       --md-list-item-leading-space: 0; | ||||||
|   | |||||||
| @@ -1,14 +1,11 @@ | |||||||
| import { mdiCalendarSync, mdiClose, mdiGestureTap } from "@mdi/js"; | import { mdiCalendarSync, mdiGestureTap } from "@mdi/js"; | ||||||
| import type { CSSResultGroup } from "lit"; | import type { CSSResultGroup } from "lit"; | ||||||
| import { LitElement, css, html, nothing } from "lit"; | import { LitElement, css, html, nothing } from "lit"; | ||||||
| import { customElement, property, query, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | import { fireEvent } from "../../../../common/dom/fire_event"; | ||||||
| import "../../../../components/ha-dialog-header"; |  | ||||||
| import "../../../../components/ha-icon-button"; |  | ||||||
| import "../../../../components/ha-icon-next"; | import "../../../../components/ha-icon-next"; | ||||||
| import "../../../../components/ha-md-dialog"; |  | ||||||
| import type { HaMdDialog } from "../../../../components/ha-md-dialog"; |  | ||||||
| import "../../../../components/ha-md-list"; | import "../../../../components/ha-md-list"; | ||||||
|  | import "../../../../components/ha-wa-dialog"; | ||||||
| import "../../../../components/ha-md-list-item"; | import "../../../../components/ha-md-list-item"; | ||||||
| import "../../../../components/ha-svg-icon"; | import "../../../../components/ha-svg-icon"; | ||||||
| import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; | import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; | ||||||
| @@ -24,92 +21,80 @@ class DialogNewBackup extends LitElement implements HassDialog { | |||||||
|  |  | ||||||
|   @state() private _params?: NewBackupDialogParams; |   @state() private _params?: NewBackupDialogParams; | ||||||
|  |  | ||||||
|   @query("ha-md-dialog") private _dialog?: HaMdDialog; |  | ||||||
|  |  | ||||||
|   public showDialog(params: NewBackupDialogParams): void { |   public showDialog(params: NewBackupDialogParams): void { | ||||||
|     this._opened = true; |     this._opened = true; | ||||||
|     this._params = params; |     this._params = params; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public closeDialog() { |   public closeDialog() { | ||||||
|     this._dialog?.close(); |     this._opened = false; | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _dialogClosed() { |   private _dialogClosed() { | ||||||
|     if (this._params!.cancel) { |     if (this._params?.cancel) { | ||||||
|       this._params!.cancel(); |       this._params.cancel(); | ||||||
|     } |     } | ||||||
|     if (this._opened) { |  | ||||||
|       fireEvent(this, "dialog-closed", { dialog: this.localName }); |  | ||||||
|     } |  | ||||||
|     this._opened = false; |  | ||||||
|     this._params = undefined; |     this._params = undefined; | ||||||
|  |     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
|     if (!this._opened || !this._params) { |     if (!this._params) { | ||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-md-dialog open @closed=${this._dialogClosed}> |       <ha-wa-dialog | ||||||
|         <ha-dialog-header slot="headline"> |         .hass=${this.hass} | ||||||
|           <ha-icon-button |         .open=${this._opened} | ||||||
|             slot="navigationIcon" |         header-title=${this.hass.localize( | ||||||
|             @click=${this.closeDialog} |           "ui.panel.config.backup.dialogs.new.title" | ||||||
|             .label=${this.hass.localize("ui.common.close")} |         )} | ||||||
|             .path=${mdiClose} |         @closed=${this._dialogClosed} | ||||||
|           ></ha-icon-button> |       > | ||||||
|           <span slot="title"> |         <ha-md-list | ||||||
|             ${this.hass.localize("ui.panel.config.backup.dialogs.new.title")} |           innerRole="listbox" | ||||||
|           </span> |           itemRoles="option" | ||||||
|         </ha-dialog-header> |           .innerAriaLabel=${this.hass.localize( | ||||||
|         <div slot="content"> |             "ui.panel.config.backup.dialogs.new.options" | ||||||
|           <ha-md-list |           )} | ||||||
|             innerRole="listbox" |           rootTabbable | ||||||
|             itemRoles="option" |         > | ||||||
|             .innerAriaLabel=${this.hass.localize( |           <ha-md-list-item | ||||||
|               "ui.panel.config.backup.dialogs.new.options" |             @click=${this._automatic} | ||||||
|             )} |             type="button" | ||||||
|             rootTabbable |             .disabled=${!this._params.config.create_backup.password} | ||||||
|             dialogInitialFocus |  | ||||||
|           > |           > | ||||||
|             <ha-md-list-item |             <ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon> | ||||||
|               @click=${this._automatic} |             <span slot="headline"> | ||||||
|               type="button" |               ${this.hass.localize( | ||||||
|               .disabled=${!this._params.config.create_backup.password} |                 "ui.panel.config.backup.dialogs.new.automatic.title" | ||||||
|             > |               )} | ||||||
|               <ha-svg-icon slot="start" .path=${mdiCalendarSync}></ha-svg-icon> |             </span> | ||||||
|               <span slot="headline"> |             <span slot="supporting-text"> | ||||||
|                 ${this.hass.localize( |               ${this.hass.localize( | ||||||
|                   "ui.panel.config.backup.dialogs.new.automatic.title" |                 "ui.panel.config.backup.dialogs.new.automatic.description" | ||||||
|                 )} |               )} | ||||||
|               </span> |             </span> | ||||||
|               <span slot="supporting-text"> |             <ha-icon-next slot="end"></ha-icon-next> | ||||||
|                 ${this.hass.localize( |           </ha-md-list-item> | ||||||
|                   "ui.panel.config.backup.dialogs.new.automatic.description" |           <ha-md-list-item @click=${this._manual} type="button"> | ||||||
|                 )} |             <ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon> | ||||||
|               </span> |             <span slot="headline"> | ||||||
|               <ha-icon-next slot="end"></ha-icon-next> |               ${this.hass.localize( | ||||||
|             </ha-md-list-item> |                 "ui.panel.config.backup.dialogs.new.manual.title" | ||||||
|             <ha-md-list-item @click=${this._manual} type="button"> |               )} | ||||||
|               <ha-svg-icon slot="start" .path=${mdiGestureTap}></ha-svg-icon> |             </span> | ||||||
|               <span slot="headline"> |             <span slot="supporting-text"> | ||||||
|                 ${this.hass.localize( |               ${this.hass.localize( | ||||||
|                   "ui.panel.config.backup.dialogs.new.manual.title" |                 "ui.panel.config.backup.dialogs.new.manual.description" | ||||||
|                 )} |               )} | ||||||
|               </span> |             </span> | ||||||
|               <span slot="supporting-text"> |             <ha-icon-next slot="end"></ha-icon-next> | ||||||
|                 ${this.hass.localize( |           </ha-md-list-item> | ||||||
|                   "ui.panel.config.backup.dialogs.new.manual.description" |         </ha-md-list> | ||||||
|                 )} |       </ha-wa-dialog> | ||||||
|               </span> |  | ||||||
|               <ha-icon-next slot="end"></ha-icon-next> |  | ||||||
|             </ha-md-list-item> |  | ||||||
|           </ha-md-list> |  | ||||||
|         </div> |  | ||||||
|       </ha-md-dialog> |  | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -128,24 +113,13 @@ class DialogNewBackup extends LitElement implements HassDialog { | |||||||
|       haStyle, |       haStyle, | ||||||
|       haStyleDialog, |       haStyleDialog, | ||||||
|       css` |       css` | ||||||
|         ha-md-dialog { |         ha-wa-dialog { | ||||||
|           --dialog-content-padding: 0; |           --dialog-content-padding: 0; | ||||||
|           max-width: 500px; |  | ||||||
|         } |  | ||||||
|         @media all and (max-width: 450px), all and (max-height: 500px) { |  | ||||||
|           ha-md-dialog { |  | ||||||
|             max-width: none; |  | ||||||
|           } |  | ||||||
|           div[slot="content"] { |  | ||||||
|             margin-top: 0; |  | ||||||
|           } |  | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         ha-md-list { |         ha-md-list { | ||||||
|           background: none; |           background: none; | ||||||
|         } |         } | ||||||
|         ha-md-list-item { |  | ||||||
|         } |  | ||||||
|         ha-icon-next { |         ha-icon-next { | ||||||
|           width: 24px; |           width: 24px; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -26,7 +26,6 @@ import type { | |||||||
|   EnergySource, |   EnergySource, | ||||||
|   FlowFromGridSourceEnergyPreference, |   FlowFromGridSourceEnergyPreference, | ||||||
|   FlowToGridSourceEnergyPreference, |   FlowToGridSourceEnergyPreference, | ||||||
|   GridPowerSourceEnergyPreference, |  | ||||||
|   GridSourceTypeEnergyPreference, |   GridSourceTypeEnergyPreference, | ||||||
| } from "../../../../data/energy"; | } from "../../../../data/energy"; | ||||||
| import { | import { | ||||||
| @@ -48,7 +47,6 @@ import { documentationUrl } from "../../../../util/documentation-url"; | |||||||
| import { | import { | ||||||
|   showEnergySettingsGridFlowFromDialog, |   showEnergySettingsGridFlowFromDialog, | ||||||
|   showEnergySettingsGridFlowToDialog, |   showEnergySettingsGridFlowToDialog, | ||||||
|   showEnergySettingsGridPowerDialog, |  | ||||||
| } from "../dialogs/show-dialogs-energy"; | } from "../dialogs/show-dialogs-energy"; | ||||||
| import "./ha-energy-validation-result"; | import "./ha-energy-validation-result"; | ||||||
| import { energyCardStyles } from "./styles"; | import { energyCardStyles } from "./styles"; | ||||||
| @@ -228,58 +226,6 @@ export class EnergyGridSettings extends LitElement { | |||||||
|             > |             > | ||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
|           <h3> |  | ||||||
|             ${this.hass.localize("ui.panel.config.energy.grid.grid_power")} |  | ||||||
|           </h3> |  | ||||||
|           ${gridSource.power?.map((power) => { |  | ||||||
|             const entityState = this.hass.states[power.stat_power]; |  | ||||||
|             return html` |  | ||||||
|               <div class="row" .source=${power}> |  | ||||||
|                 ${entityState?.attributes.icon |  | ||||||
|                   ? html`<ha-icon |  | ||||||
|                       .icon=${entityState.attributes.icon} |  | ||||||
|                     ></ha-icon>` |  | ||||||
|                   : html`<ha-svg-icon |  | ||||||
|                       .path=${mdiTransmissionTower} |  | ||||||
|                     ></ha-svg-icon>`} |  | ||||||
|                 <span class="content" |  | ||||||
|                   >${getStatisticLabel( |  | ||||||
|                     this.hass, |  | ||||||
|                     power.stat_power, |  | ||||||
|                     this.statsMetadata?.[power.stat_power] |  | ||||||
|                   )}</span |  | ||||||
|                 > |  | ||||||
|                 <ha-icon-button |  | ||||||
|                   .label=${this.hass.localize( |  | ||||||
|                     "ui.panel.config.energy.grid.edit_power" |  | ||||||
|                   )} |  | ||||||
|                   @click=${this._editPowerSource} |  | ||||||
|                   .path=${mdiPencil} |  | ||||||
|                 ></ha-icon-button> |  | ||||||
|                 <ha-icon-button |  | ||||||
|                   .label=${this.hass.localize( |  | ||||||
|                     "ui.panel.config.energy.grid.delete_power" |  | ||||||
|                   )} |  | ||||||
|                   @click=${this._deletePowerSource} |  | ||||||
|                   .path=${mdiDelete} |  | ||||||
|                 ></ha-icon-button> |  | ||||||
|               </div> |  | ||||||
|             `; |  | ||||||
|           })} |  | ||||||
|           <div class="row border-bottom"> |  | ||||||
|             <ha-svg-icon .path=${mdiTransmissionTower}></ha-svg-icon> |  | ||||||
|             <ha-button |  | ||||||
|               @click=${this._addPowerSource} |  | ||||||
|               appearance="filled" |  | ||||||
|               size="small" |  | ||||||
|             > |  | ||||||
|               <ha-svg-icon .path=${mdiPlus} slot="start"></ha-svg-icon |  | ||||||
|               >${this.hass.localize( |  | ||||||
|                 "ui.panel.config.energy.grid.add_power" |  | ||||||
|               )}</ha-button |  | ||||||
|             > |  | ||||||
|           </div> |  | ||||||
|  |  | ||||||
|           <h3> |           <h3> | ||||||
|             ${this.hass.localize( |             ${this.hass.localize( | ||||||
|               "ui.panel.config.energy.grid.grid_carbon_footprint" |               "ui.panel.config.energy.grid.grid_carbon_footprint" | ||||||
| @@ -553,97 +499,6 @@ export class EnergyGridSettings extends LitElement { | |||||||
|     await this._savePreferences(cleanedPreferences); |     await this._savePreferences(cleanedPreferences); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _addPowerSource() { |  | ||||||
|     const gridSource = this.preferences.energy_sources.find( |  | ||||||
|       (src) => src.type === "grid" |  | ||||||
|     ) as GridSourceTypeEnergyPreference | undefined; |  | ||||||
|     showEnergySettingsGridPowerDialog(this, { |  | ||||||
|       grid_source: gridSource, |  | ||||||
|       saveCallback: async (power) => { |  | ||||||
|         let preferences: EnergyPreferences; |  | ||||||
|         if (!gridSource) { |  | ||||||
|           preferences = { |  | ||||||
|             ...this.preferences, |  | ||||||
|             energy_sources: [ |  | ||||||
|               ...this.preferences.energy_sources, |  | ||||||
|               { |  | ||||||
|                 ...emptyGridSourceEnergyPreference(), |  | ||||||
|                 power: [power], |  | ||||||
|               }, |  | ||||||
|             ], |  | ||||||
|           }; |  | ||||||
|         } else { |  | ||||||
|           preferences = { |  | ||||||
|             ...this.preferences, |  | ||||||
|             energy_sources: this.preferences.energy_sources.map((src) => |  | ||||||
|               src.type === "grid" |  | ||||||
|                 ? { ...src, power: [...(gridSource.power || []), power] } |  | ||||||
|                 : src |  | ||||||
|             ), |  | ||||||
|           }; |  | ||||||
|         } |  | ||||||
|         await this._savePreferences(preferences); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _editPowerSource(ev) { |  | ||||||
|     const origSource: GridPowerSourceEnergyPreference = |  | ||||||
|       ev.currentTarget.closest(".row").source; |  | ||||||
|     const gridSource = this.preferences.energy_sources.find( |  | ||||||
|       (src) => src.type === "grid" |  | ||||||
|     ) as GridSourceTypeEnergyPreference | undefined; |  | ||||||
|     showEnergySettingsGridPowerDialog(this, { |  | ||||||
|       source: { ...origSource }, |  | ||||||
|       grid_source: gridSource, |  | ||||||
|       saveCallback: async (source) => { |  | ||||||
|         const power = |  | ||||||
|           energySourcesByType(this.preferences).grid![0].power || []; |  | ||||||
|  |  | ||||||
|         const preferences: EnergyPreferences = { |  | ||||||
|           ...this.preferences, |  | ||||||
|           energy_sources: this.preferences.energy_sources.map((src) => |  | ||||||
|             src.type === "grid" |  | ||||||
|               ? { |  | ||||||
|                   ...src, |  | ||||||
|                   power: power.map((p) => (p === origSource ? source : p)), |  | ||||||
|                 } |  | ||||||
|               : src |  | ||||||
|           ), |  | ||||||
|         }; |  | ||||||
|         await this._savePreferences(preferences); |  | ||||||
|       }, |  | ||||||
|     }); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _deletePowerSource(ev) { |  | ||||||
|     const sourceToDelete: GridPowerSourceEnergyPreference = |  | ||||||
|       ev.currentTarget.closest(".row").source; |  | ||||||
|  |  | ||||||
|     if ( |  | ||||||
|       !(await showConfirmationDialog(this, { |  | ||||||
|         title: this.hass.localize("ui.panel.config.energy.delete_source"), |  | ||||||
|       })) |  | ||||||
|     ) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const power = |  | ||||||
|       energySourcesByType(this.preferences).grid![0].power?.filter( |  | ||||||
|         (p) => p !== sourceToDelete |  | ||||||
|       ) || []; |  | ||||||
|  |  | ||||||
|     const preferences: EnergyPreferences = { |  | ||||||
|       ...this.preferences, |  | ||||||
|       energy_sources: this.preferences.energy_sources.map((source) => |  | ||||||
|         source.type === "grid" ? { ...source, power } : source |  | ||||||
|       ), |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     const cleanedPreferences = this._removeEmptySources(preferences); |  | ||||||
|     await this._savePreferences(cleanedPreferences); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _removeEmptySources(preferences: EnergyPreferences) { |   private _removeEmptySources(preferences: EnergyPreferences) { | ||||||
|     // Check if grid sources became an empty type and remove if so |     // Check if grid sources became an empty type and remove if so | ||||||
|     preferences.energy_sources = preferences.energy_sources.reduce< |     preferences.energy_sources = preferences.energy_sources.reduce< | ||||||
| @@ -652,8 +507,7 @@ export class EnergyGridSettings extends LitElement { | |||||||
|       if ( |       if ( | ||||||
|         source.type !== "grid" || |         source.type !== "grid" || | ||||||
|         source.flow_from.length > 0 || |         source.flow_from.length > 0 || | ||||||
|         source.flow_to.length > 0 || |         source.flow_to.length > 0 | ||||||
|         (source.power && source.power.length > 0) |  | ||||||
|       ) { |       ) { | ||||||
|         acc.push(source); |         acc.push(source); | ||||||
|       } |       } | ||||||
|   | |||||||
| @@ -18,7 +18,6 @@ import type { HomeAssistant } from "../../../../types"; | |||||||
| import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy"; | import type { EnergySettingsBatteryDialogParams } from "./show-dialogs-energy"; | ||||||
|  |  | ||||||
| const energyUnitClasses = ["energy"]; | const energyUnitClasses = ["energy"]; | ||||||
| const powerUnitClasses = ["power"]; |  | ||||||
|  |  | ||||||
| @customElement("dialog-energy-battery-settings") | @customElement("dialog-energy-battery-settings") | ||||||
| export class DialogEnergyBatterySettings | export class DialogEnergyBatterySettings | ||||||
| @@ -33,14 +32,10 @@ export class DialogEnergyBatterySettings | |||||||
|  |  | ||||||
|   @state() private _energy_units?: string[]; |   @state() private _energy_units?: string[]; | ||||||
|  |  | ||||||
|   @state() private _power_units?: string[]; |  | ||||||
|  |  | ||||||
|   @state() private _error?: string; |   @state() private _error?: string; | ||||||
|  |  | ||||||
|   private _excludeList?: string[]; |   private _excludeList?: string[]; | ||||||
|  |  | ||||||
|   private _excludeListPower?: string[]; |  | ||||||
|  |  | ||||||
|   public async showDialog( |   public async showDialog( | ||||||
|     params: EnergySettingsBatteryDialogParams |     params: EnergySettingsBatteryDialogParams | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
| @@ -51,9 +46,6 @@ export class DialogEnergyBatterySettings | |||||||
|     this._energy_units = ( |     this._energy_units = ( | ||||||
|       await getSensorDeviceClassConvertibleUnits(this.hass, "energy") |       await getSensorDeviceClassConvertibleUnits(this.hass, "energy") | ||||||
|     ).units; |     ).units; | ||||||
|     this._power_units = ( |  | ||||||
|       await getSensorDeviceClassConvertibleUnits(this.hass, "power") |  | ||||||
|     ).units; |  | ||||||
|     const allSources: string[] = []; |     const allSources: string[] = []; | ||||||
|     this._params.battery_sources.forEach((entry) => { |     this._params.battery_sources.forEach((entry) => { | ||||||
|       allSources.push(entry.stat_energy_from); |       allSources.push(entry.stat_energy_from); | ||||||
| @@ -64,9 +56,6 @@ export class DialogEnergyBatterySettings | |||||||
|         id !== this._source?.stat_energy_from && |         id !== this._source?.stat_energy_from && | ||||||
|         id !== this._source?.stat_energy_to |         id !== this._source?.stat_energy_to | ||||||
|     ); |     ); | ||||||
|     this._excludeListPower = this._params.battery_sources |  | ||||||
|       .map((entry) => entry.stat_power) |  | ||||||
|       .filter((id) => id && id !== this._source?.stat_power) as string[]; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public closeDialog() { |   public closeDialog() { | ||||||
| @@ -83,6 +72,8 @@ export class DialogEnergyBatterySettings | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const pickableUnit = this._energy_units?.join(", ") || ""; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-dialog | ||||||
|         open |         open | ||||||
| @@ -94,6 +85,12 @@ export class DialogEnergyBatterySettings | |||||||
|         @closed=${this.closeDialog} |         @closed=${this.closeDialog} | ||||||
|       > |       > | ||||||
|         ${this._error ? html`<p class="error">${this._error}</p>` : ""} |         ${this._error ? html`<p class="error">${this._error}</p>` : ""} | ||||||
|  |         <div> | ||||||
|  |           ${this.hass.localize( | ||||||
|  |             "ui.panel.config.energy.battery.dialog.entity_para", | ||||||
|  |             { unit: pickableUnit } | ||||||
|  |           )} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <ha-statistic-picker |         <ha-statistic-picker | ||||||
|           .hass=${this.hass} |           .hass=${this.hass} | ||||||
| @@ -108,10 +105,6 @@ export class DialogEnergyBatterySettings | |||||||
|             this._source.stat_energy_from, |             this._source.stat_energy_from, | ||||||
|           ]} |           ]} | ||||||
|           @value-changed=${this._statisticToChanged} |           @value-changed=${this._statisticToChanged} | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.battery.dialog.energy_helper_into", |  | ||||||
|             { unit: this._energy_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|           dialogInitialFocus |           dialogInitialFocus | ||||||
|         ></ha-statistic-picker> |         ></ha-statistic-picker> | ||||||
|  |  | ||||||
| @@ -128,25 +121,6 @@ export class DialogEnergyBatterySettings | |||||||
|             this._source.stat_energy_to, |             this._source.stat_energy_to, | ||||||
|           ]} |           ]} | ||||||
|           @value-changed=${this._statisticFromChanged} |           @value-changed=${this._statisticFromChanged} | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.battery.dialog.energy_helper_out", |  | ||||||
|             { unit: this._energy_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|         ></ha-statistic-picker> |  | ||||||
|  |  | ||||||
|         <ha-statistic-picker |  | ||||||
|           .hass=${this.hass} |  | ||||||
|           .includeUnitClass=${powerUnitClasses} |  | ||||||
|           .value=${this._source.stat_power} |  | ||||||
|           .label=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.battery.dialog.power" |  | ||||||
|           )} |  | ||||||
|           .excludeStatistics=${this._excludeListPower} |  | ||||||
|           @value-changed=${this._powerChanged} |  | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.battery.dialog.power_helper", |  | ||||||
|             { unit: this._power_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|         ></ha-statistic-picker> |         ></ha-statistic-picker> | ||||||
|  |  | ||||||
|         <ha-button |         <ha-button | ||||||
| @@ -176,10 +150,6 @@ export class DialogEnergyBatterySettings | |||||||
|     this._source = { ...this._source!, stat_energy_from: ev.detail.value }; |     this._source = { ...this._source!, stat_energy_from: ev.detail.value }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _powerChanged(ev: CustomEvent<{ value: string }>) { |  | ||||||
|     this._source = { ...this._source!, stat_power: ev.detail.value }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _save() { |   private async _save() { | ||||||
|     try { |     try { | ||||||
|       await this._params!.saveCallback(this._source!); |       await this._params!.saveCallback(this._source!); | ||||||
| @@ -198,11 +168,7 @@ export class DialogEnergyBatterySettings | |||||||
|           --mdc-dialog-max-width: 430px; |           --mdc-dialog-max-width: 430px; | ||||||
|         } |         } | ||||||
|         ha-statistic-picker { |         ha-statistic-picker { | ||||||
|           display: block; |           width: 100%; | ||||||
|           margin-bottom: var(--ha-space-4); |  | ||||||
|         } |  | ||||||
|         ha-statistic-picker:last-of-type { |  | ||||||
|           margin-bottom: 0; |  | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   | |||||||
| @@ -21,7 +21,6 @@ import type { HomeAssistant } from "../../../../types"; | |||||||
| import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy"; | import type { EnergySettingsDeviceDialogParams } from "./show-dialogs-energy"; | ||||||
|  |  | ||||||
| const energyUnitClasses = ["energy"]; | const energyUnitClasses = ["energy"]; | ||||||
| const powerUnitClasses = ["power"]; |  | ||||||
|  |  | ||||||
| @customElement("dialog-energy-device-settings") | @customElement("dialog-energy-device-settings") | ||||||
| export class DialogEnergyDeviceSettings | export class DialogEnergyDeviceSettings | ||||||
| @@ -36,14 +35,10 @@ export class DialogEnergyDeviceSettings | |||||||
|  |  | ||||||
|   @state() private _energy_units?: string[]; |   @state() private _energy_units?: string[]; | ||||||
|  |  | ||||||
|   @state() private _power_units?: string[]; |  | ||||||
|  |  | ||||||
|   @state() private _error?: string; |   @state() private _error?: string; | ||||||
|  |  | ||||||
|   private _excludeList?: string[]; |   private _excludeList?: string[]; | ||||||
|  |  | ||||||
|   private _excludeListPower?: string[]; |  | ||||||
|  |  | ||||||
|   private _possibleParents: DeviceConsumptionEnergyPreference[] = []; |   private _possibleParents: DeviceConsumptionEnergyPreference[] = []; | ||||||
|  |  | ||||||
|   public async showDialog( |   public async showDialog( | ||||||
| @@ -55,15 +50,9 @@ export class DialogEnergyDeviceSettings | |||||||
|     this._energy_units = ( |     this._energy_units = ( | ||||||
|       await getSensorDeviceClassConvertibleUnits(this.hass, "energy") |       await getSensorDeviceClassConvertibleUnits(this.hass, "energy") | ||||||
|     ).units; |     ).units; | ||||||
|     this._power_units = ( |  | ||||||
|       await getSensorDeviceClassConvertibleUnits(this.hass, "power") |  | ||||||
|     ).units; |  | ||||||
|     this._excludeList = this._params.device_consumptions |     this._excludeList = this._params.device_consumptions | ||||||
|       .map((entry) => entry.stat_consumption) |       .map((entry) => entry.stat_consumption) | ||||||
|       .filter((id) => id !== this._device?.stat_consumption); |       .filter((id) => id !== this._device?.stat_consumption); | ||||||
|     this._excludeListPower = this._params.device_consumptions |  | ||||||
|       .map((entry) => entry.stat_power) |  | ||||||
|       .filter((id) => id && id !== this._device?.stat_power) as string[]; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _computePossibleParents() { |   private _computePossibleParents() { | ||||||
| @@ -104,6 +93,8 @@ export class DialogEnergyDeviceSettings | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const pickableUnit = this._energy_units?.join(", ") || ""; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-dialog | ||||||
|         open |         open | ||||||
| @@ -117,6 +108,12 @@ export class DialogEnergyDeviceSettings | |||||||
|         @closed=${this.closeDialog} |         @closed=${this.closeDialog} | ||||||
|       > |       > | ||||||
|         ${this._error ? html`<p class="error">${this._error}</p>` : ""} |         ${this._error ? html`<p class="error">${this._error}</p>` : ""} | ||||||
|  |         <div> | ||||||
|  |           ${this.hass.localize( | ||||||
|  |             "ui.panel.config.energy.device_consumption.dialog.selected_stat_intro", | ||||||
|  |             { unit: pickableUnit } | ||||||
|  |           )} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <ha-statistic-picker |         <ha-statistic-picker | ||||||
|           .hass=${this.hass} |           .hass=${this.hass} | ||||||
| @@ -128,28 +125,9 @@ export class DialogEnergyDeviceSettings | |||||||
|           )} |           )} | ||||||
|           .excludeStatistics=${this._excludeList} |           .excludeStatistics=${this._excludeList} | ||||||
|           @value-changed=${this._statisticChanged} |           @value-changed=${this._statisticChanged} | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.device_consumption.dialog.selected_stat_intro", |  | ||||||
|             { unit: this._energy_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|           dialogInitialFocus |           dialogInitialFocus | ||||||
|         ></ha-statistic-picker> |         ></ha-statistic-picker> | ||||||
|  |  | ||||||
|         <ha-statistic-picker |  | ||||||
|           .hass=${this.hass} |  | ||||||
|           .includeUnitClass=${powerUnitClasses} |  | ||||||
|           .value=${this._device?.stat_power} |  | ||||||
|           .label=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.device_consumption.dialog.device_consumption_power" |  | ||||||
|           )} |  | ||||||
|           .excludeStatistics=${this._excludeListPower} |  | ||||||
|           @value-changed=${this._powerStatisticChanged} |  | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.device_consumption.dialog.selected_stat_intro", |  | ||||||
|             { unit: this._power_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|         ></ha-statistic-picker> |  | ||||||
|  |  | ||||||
|         <ha-textfield |         <ha-textfield | ||||||
|           .label=${this.hass.localize( |           .label=${this.hass.localize( | ||||||
|             "ui.panel.config.energy.device_consumption.dialog.display_name" |             "ui.panel.config.energy.device_consumption.dialog.display_name" | ||||||
| @@ -232,20 +210,6 @@ export class DialogEnergyDeviceSettings | |||||||
|     this._computePossibleParents(); |     this._computePossibleParents(); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) { |  | ||||||
|     if (!this._device) { |  | ||||||
|       return; |  | ||||||
|     } |  | ||||||
|     const newDevice = { |  | ||||||
|       ...this._device, |  | ||||||
|       stat_power: ev.detail.value, |  | ||||||
|     } as DeviceConsumptionEnergyPreference; |  | ||||||
|     if (!newDevice.stat_power) { |  | ||||||
|       delete newDevice.stat_power; |  | ||||||
|     } |  | ||||||
|     this._device = newDevice; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _nameChanged(ev) { |   private _nameChanged(ev) { | ||||||
|     const newDevice = { |     const newDevice = { | ||||||
|       ...this._device!, |       ...this._device!, | ||||||
| @@ -281,19 +245,15 @@ export class DialogEnergyDeviceSettings | |||||||
|     return [ |     return [ | ||||||
|       haStyleDialog, |       haStyleDialog, | ||||||
|       css` |       css` | ||||||
|         ha-statistic-picker { |  | ||||||
|           display: block; |  | ||||||
|           margin-bottom: var(--ha-space-2); |  | ||||||
|         } |  | ||||||
|         ha-statistic-picker { |         ha-statistic-picker { | ||||||
|           width: 100%; |           width: 100%; | ||||||
|         } |         } | ||||||
|         ha-select { |         ha-select { | ||||||
|           margin-top: var(--ha-space-4); |           margin-top: 16px; | ||||||
|           width: 100%; |           width: 100%; | ||||||
|         } |         } | ||||||
|         ha-textfield { |         ha-textfield { | ||||||
|           margin-top: var(--ha-space-4); |           margin-top: 16px; | ||||||
|           width: 100%; |           width: 100%; | ||||||
|         } |         } | ||||||
|       `, |       `, | ||||||
|   | |||||||
| @@ -115,6 +115,8 @@ export class DialogEnergyGridFlowSettings | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const pickableUnit = this._energy_units?.join(", ") || ""; | ||||||
|  |  | ||||||
|     const unitPriceSensor = this._pickedDisplayUnit |     const unitPriceSensor = this._pickedDisplayUnit | ||||||
|       ? `${this.hass.config.currency}/${this._pickedDisplayUnit}` |       ? `${this.hass.config.currency}/${this._pickedDisplayUnit}` | ||||||
|       : undefined; |       : undefined; | ||||||
| @@ -148,11 +150,19 @@ export class DialogEnergyGridFlowSettings | |||||||
|         @closed=${this.closeDialog} |         @closed=${this.closeDialog} | ||||||
|       > |       > | ||||||
|         ${this._error ? html`<p class="error">${this._error}</p>` : ""} |         ${this._error ? html`<p class="error">${this._error}</p>` : ""} | ||||||
|         <p> |         <div> | ||||||
|           ${this.hass.localize( |           <p> | ||||||
|             `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph` |             ${this.hass.localize( | ||||||
|           )} |               `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.paragraph` | ||||||
|         </p> |             )} | ||||||
|  |           </p> | ||||||
|  |           <p> | ||||||
|  |             ${this.hass.localize( | ||||||
|  |               `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`, | ||||||
|  |               { unit: pickableUnit } | ||||||
|  |             )} | ||||||
|  |           </p> | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <ha-statistic-picker |         <ha-statistic-picker | ||||||
|           .hass=${this.hass} |           .hass=${this.hass} | ||||||
| @@ -168,10 +178,6 @@ export class DialogEnergyGridFlowSettings | |||||||
|           )} |           )} | ||||||
|           .excludeStatistics=${this._excludeList} |           .excludeStatistics=${this._excludeList} | ||||||
|           @value-changed=${this._statisticChanged} |           @value-changed=${this._statisticChanged} | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             `ui.panel.config.energy.grid.flow_dialog.${this._params.direction}.entity_para`, |  | ||||||
|             { unit: this._energy_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|           dialogInitialFocus |           dialogInitialFocus | ||||||
|         ></ha-statistic-picker> |         ></ha-statistic-picker> | ||||||
|  |  | ||||||
| @@ -374,10 +380,6 @@ export class DialogEnergyGridFlowSettings | |||||||
|         ha-dialog { |         ha-dialog { | ||||||
|           --mdc-dialog-max-width: 430px; |           --mdc-dialog-max-width: 430px; | ||||||
|         } |         } | ||||||
|         ha-statistic-picker { |  | ||||||
|           display: block; |  | ||||||
|           margin: var(--ha-space-4) 0; |  | ||||||
|         } |  | ||||||
|         ha-formfield { |         ha-formfield { | ||||||
|           display: block; |           display: block; | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -1,153 +0,0 @@ | |||||||
| import { mdiTransmissionTower } from "@mdi/js"; |  | ||||||
| import type { CSSResultGroup } from "lit"; |  | ||||||
| import { css, html, LitElement, nothing } from "lit"; |  | ||||||
| import { customElement, property, state } from "lit/decorators"; |  | ||||||
| import { fireEvent } from "../../../../common/dom/fire_event"; |  | ||||||
| import "../../../../components/entity/ha-statistic-picker"; |  | ||||||
| import "../../../../components/ha-dialog"; |  | ||||||
| import "../../../../components/ha-button"; |  | ||||||
| import type { GridPowerSourceEnergyPreference } from "../../../../data/energy"; |  | ||||||
| import { energyStatisticHelpUrl } from "../../../../data/energy"; |  | ||||||
| import { getSensorDeviceClassConvertibleUnits } from "../../../../data/sensor"; |  | ||||||
| import type { HassDialog } from "../../../../dialogs/make-dialog-manager"; |  | ||||||
| import { haStyleDialog } from "../../../../resources/styles"; |  | ||||||
| import type { HomeAssistant } from "../../../../types"; |  | ||||||
| import type { EnergySettingsGridPowerDialogParams } from "./show-dialogs-energy"; |  | ||||||
|  |  | ||||||
| const powerUnitClasses = ["power"]; |  | ||||||
|  |  | ||||||
| @customElement("dialog-energy-grid-power-settings") |  | ||||||
| export class DialogEnergyGridPowerSettings |  | ||||||
|   extends LitElement |  | ||||||
|   implements HassDialog<EnergySettingsGridPowerDialogParams> |  | ||||||
| { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @state() private _params?: EnergySettingsGridPowerDialogParams; |  | ||||||
|  |  | ||||||
|   @state() private _source?: GridPowerSourceEnergyPreference; |  | ||||||
|  |  | ||||||
|   @state() private _power_units?: string[]; |  | ||||||
|  |  | ||||||
|   @state() private _error?: string; |  | ||||||
|  |  | ||||||
|   private _excludeListPower?: string[]; |  | ||||||
|  |  | ||||||
|   public async showDialog( |  | ||||||
|     params: EnergySettingsGridPowerDialogParams |  | ||||||
|   ): Promise<void> { |  | ||||||
|     this._params = params; |  | ||||||
|     this._source = params.source ? { ...params.source } : { stat_power: "" }; |  | ||||||
|  |  | ||||||
|     const initialSourceIdPower = this._source.stat_power; |  | ||||||
|  |  | ||||||
|     this._power_units = ( |  | ||||||
|       await getSensorDeviceClassConvertibleUnits(this.hass, "power") |  | ||||||
|     ).units; |  | ||||||
|  |  | ||||||
|     this._excludeListPower = [ |  | ||||||
|       ...(this._params.grid_source?.power?.map((entry) => entry.stat_power) || |  | ||||||
|         []), |  | ||||||
|     ].filter((id) => id && id !== initialSourceIdPower) as string[]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public closeDialog() { |  | ||||||
|     this._params = undefined; |  | ||||||
|     this._source = undefined; |  | ||||||
|     this._error = undefined; |  | ||||||
|     this._excludeListPower = undefined; |  | ||||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render() { |  | ||||||
|     if (!this._params || !this._source) { |  | ||||||
|       return nothing; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <ha-dialog |  | ||||||
|         open |  | ||||||
|         .heading=${html`<ha-svg-icon |  | ||||||
|             .path=${mdiTransmissionTower} |  | ||||||
|             style="--mdc-icon-size: 32px;" |  | ||||||
|           ></ha-svg-icon |  | ||||||
|           >${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.grid.power_dialog.header" |  | ||||||
|           )}`} |  | ||||||
|         @closed=${this.closeDialog} |  | ||||||
|       > |  | ||||||
|         ${this._error ? html`<p class="error">${this._error}</p>` : ""} |  | ||||||
|  |  | ||||||
|         <ha-statistic-picker |  | ||||||
|           .hass=${this.hass} |  | ||||||
|           .helpMissingEntityUrl=${energyStatisticHelpUrl} |  | ||||||
|           .includeUnitClass=${powerUnitClasses} |  | ||||||
|           .value=${this._source.stat_power} |  | ||||||
|           .label=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.grid.power_dialog.power_stat" |  | ||||||
|           )} |  | ||||||
|           .excludeStatistics=${this._excludeListPower} |  | ||||||
|           @value-changed=${this._powerStatisticChanged} |  | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.grid.power_dialog.power_helper", |  | ||||||
|             { unit: this._power_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|           dialogInitialFocus |  | ||||||
|         ></ha-statistic-picker> |  | ||||||
|  |  | ||||||
|         <ha-button |  | ||||||
|           appearance="plain" |  | ||||||
|           @click=${this.closeDialog} |  | ||||||
|           slot="primaryAction" |  | ||||||
|         > |  | ||||||
|           ${this.hass.localize("ui.common.cancel")} |  | ||||||
|         </ha-button> |  | ||||||
|         <ha-button |  | ||||||
|           @click=${this._save} |  | ||||||
|           .disabled=${!this._source.stat_power} |  | ||||||
|           slot="primaryAction" |  | ||||||
|         > |  | ||||||
|           ${this.hass.localize("ui.common.save")} |  | ||||||
|         </ha-button> |  | ||||||
|       </ha-dialog> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) { |  | ||||||
|     this._source = { |  | ||||||
|       ...this._source!, |  | ||||||
|       stat_power: ev.detail.value, |  | ||||||
|     }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _save() { |  | ||||||
|     try { |  | ||||||
|       await this._params!.saveCallback(this._source!); |  | ||||||
|       this.closeDialog(); |  | ||||||
|     } catch (err: any) { |  | ||||||
|       this._error = err.message; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static get styles(): CSSResultGroup { |  | ||||||
|     return [ |  | ||||||
|       haStyleDialog, |  | ||||||
|       css` |  | ||||||
|         ha-dialog { |  | ||||||
|           --mdc-dialog-max-width: 430px; |  | ||||||
|         } |  | ||||||
|         ha-statistic-picker { |  | ||||||
|           display: block; |  | ||||||
|           margin: var(--ha-space-4) 0; |  | ||||||
|         } |  | ||||||
|       `, |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "dialog-energy-grid-power-settings": DialogEnergyGridPowerSettings; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -28,7 +28,6 @@ import { brandsUrl } from "../../../../util/brands-url"; | |||||||
| import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy"; | import type { EnergySettingsSolarDialogParams } from "./show-dialogs-energy"; | ||||||
|  |  | ||||||
| const energyUnitClasses = ["energy"]; | const energyUnitClasses = ["energy"]; | ||||||
| const powerUnitClasses = ["power"]; |  | ||||||
|  |  | ||||||
| @customElement("dialog-energy-solar-settings") | @customElement("dialog-energy-solar-settings") | ||||||
| export class DialogEnergySolarSettings | export class DialogEnergySolarSettings | ||||||
| @@ -47,14 +46,10 @@ export class DialogEnergySolarSettings | |||||||
|  |  | ||||||
|   @state() private _energy_units?: string[]; |   @state() private _energy_units?: string[]; | ||||||
|  |  | ||||||
|   @state() private _power_units?: string[]; |  | ||||||
|  |  | ||||||
|   @state() private _error?: string; |   @state() private _error?: string; | ||||||
|  |  | ||||||
|   private _excludeList?: string[]; |   private _excludeList?: string[]; | ||||||
|  |  | ||||||
|   private _excludeListPower?: string[]; |  | ||||||
|  |  | ||||||
|   public async showDialog( |   public async showDialog( | ||||||
|     params: EnergySettingsSolarDialogParams |     params: EnergySettingsSolarDialogParams | ||||||
|   ): Promise<void> { |   ): Promise<void> { | ||||||
| @@ -67,15 +62,9 @@ export class DialogEnergySolarSettings | |||||||
|     this._energy_units = ( |     this._energy_units = ( | ||||||
|       await getSensorDeviceClassConvertibleUnits(this.hass, "energy") |       await getSensorDeviceClassConvertibleUnits(this.hass, "energy") | ||||||
|     ).units; |     ).units; | ||||||
|     this._power_units = ( |  | ||||||
|       await getSensorDeviceClassConvertibleUnits(this.hass, "power") |  | ||||||
|     ).units; |  | ||||||
|     this._excludeList = this._params.solar_sources |     this._excludeList = this._params.solar_sources | ||||||
|       .map((entry) => entry.stat_energy_from) |       .map((entry) => entry.stat_energy_from) | ||||||
|       .filter((id) => id !== this._source?.stat_energy_from); |       .filter((id) => id !== this._source?.stat_energy_from); | ||||||
|     this._excludeListPower = this._params.solar_sources |  | ||||||
|       .map((entry) => entry.stat_power) |  | ||||||
|       .filter((id) => id && id !== this._source?.stat_power) as string[]; |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   public closeDialog() { |   public closeDialog() { | ||||||
| @@ -92,6 +81,8 @@ export class DialogEnergySolarSettings | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const pickableUnit = this._energy_units?.join(", ") || ""; | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-dialog |       <ha-dialog | ||||||
|         open |         open | ||||||
| @@ -103,6 +94,12 @@ export class DialogEnergySolarSettings | |||||||
|         @closed=${this.closeDialog} |         @closed=${this.closeDialog} | ||||||
|       > |       > | ||||||
|         ${this._error ? html`<p class="error">${this._error}</p>` : ""} |         ${this._error ? html`<p class="error">${this._error}</p>` : ""} | ||||||
|  |         <div> | ||||||
|  |           ${this.hass.localize( | ||||||
|  |             "ui.panel.config.energy.solar.dialog.entity_para", | ||||||
|  |             { unit: pickableUnit } | ||||||
|  |           )} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <ha-statistic-picker |         <ha-statistic-picker | ||||||
|           .hass=${this.hass} |           .hass=${this.hass} | ||||||
| @@ -114,28 +111,9 @@ export class DialogEnergySolarSettings | |||||||
|           )} |           )} | ||||||
|           .excludeStatistics=${this._excludeList} |           .excludeStatistics=${this._excludeList} | ||||||
|           @value-changed=${this._statisticChanged} |           @value-changed=${this._statisticChanged} | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.solar.dialog.entity_para", |  | ||||||
|             { unit: this._energy_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|           dialogInitialFocus |           dialogInitialFocus | ||||||
|         ></ha-statistic-picker> |         ></ha-statistic-picker> | ||||||
|  |  | ||||||
|         <ha-statistic-picker |  | ||||||
|           .hass=${this.hass} |  | ||||||
|           .includeUnitClass=${powerUnitClasses} |  | ||||||
|           .value=${this._source.stat_power} |  | ||||||
|           .label=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.solar.dialog.solar_production_power" |  | ||||||
|           )} |  | ||||||
|           .excludeStatistics=${this._excludeListPower} |  | ||||||
|           @value-changed=${this._powerStatisticChanged} |  | ||||||
|           .helper=${this.hass.localize( |  | ||||||
|             "ui.panel.config.energy.solar.dialog.entity_para", |  | ||||||
|             { unit: this._power_units?.join(", ") || "" } |  | ||||||
|           )} |  | ||||||
|         ></ha-statistic-picker> |  | ||||||
|  |  | ||||||
|         <h3> |         <h3> | ||||||
|           ${this.hass.localize( |           ${this.hass.localize( | ||||||
|             "ui.panel.config.energy.solar.dialog.solar_production_forecast" |             "ui.panel.config.energy.solar.dialog.solar_production_forecast" | ||||||
| @@ -289,10 +267,6 @@ export class DialogEnergySolarSettings | |||||||
|     this._source = { ...this._source!, stat_energy_from: ev.detail.value }; |     this._source = { ...this._source!, stat_energy_from: ev.detail.value }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _powerStatisticChanged(ev: CustomEvent<{ value: string }>) { |  | ||||||
|     this._source = { ...this._source!, stat_power: ev.detail.value }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private async _save() { |   private async _save() { | ||||||
|     try { |     try { | ||||||
|       if (!this._forecast) { |       if (!this._forecast) { | ||||||
| @@ -313,10 +287,6 @@ export class DialogEnergySolarSettings | |||||||
|         ha-dialog { |         ha-dialog { | ||||||
|           --mdc-dialog-max-width: 430px; |           --mdc-dialog-max-width: 430px; | ||||||
|         } |         } | ||||||
|         ha-statistic-picker { |  | ||||||
|           display: block; |  | ||||||
|           margin-bottom: var(--ha-space-4); |  | ||||||
|         } |  | ||||||
|         img { |         img { | ||||||
|           height: 24px; |           height: 24px; | ||||||
|           margin-right: 16px; |           margin-right: 16px; | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import type { | |||||||
|   FlowFromGridSourceEnergyPreference, |   FlowFromGridSourceEnergyPreference, | ||||||
|   FlowToGridSourceEnergyPreference, |   FlowToGridSourceEnergyPreference, | ||||||
|   GasSourceTypeEnergyPreference, |   GasSourceTypeEnergyPreference, | ||||||
|   GridPowerSourceEnergyPreference, |  | ||||||
|   GridSourceTypeEnergyPreference, |   GridSourceTypeEnergyPreference, | ||||||
|   SolarSourceTypeEnergyPreference, |   SolarSourceTypeEnergyPreference, | ||||||
|   WaterSourceTypeEnergyPreference, |   WaterSourceTypeEnergyPreference, | ||||||
| @@ -42,12 +41,6 @@ export interface EnergySettingsGridFlowToDialogParams { | |||||||
|   saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise<void>; |   saveCallback: (source: FlowToGridSourceEnergyPreference) => Promise<void>; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface EnergySettingsGridPowerDialogParams { |  | ||||||
|   source?: GridPowerSourceEnergyPreference; |  | ||||||
|   grid_source?: GridSourceTypeEnergyPreference; |  | ||||||
|   saveCallback: (source: GridPowerSourceEnergyPreference) => Promise<void>; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface EnergySettingsSolarDialogParams { | export interface EnergySettingsSolarDialogParams { | ||||||
|   info: EnergyInfo; |   info: EnergyInfo; | ||||||
|   source?: SolarSourceTypeEnergyPreference; |   source?: SolarSourceTypeEnergyPreference; | ||||||
| @@ -159,14 +152,3 @@ export const showEnergySettingsGridFlowToDialog = ( | |||||||
|     dialogParams: { ...dialogParams, direction: "to" }, |     dialogParams: { ...dialogParams, direction: "to" }, | ||||||
|   }); |   }); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const showEnergySettingsGridPowerDialog = ( |  | ||||||
|   element: HTMLElement, |  | ||||||
|   dialogParams: EnergySettingsGridPowerDialogParams |  | ||||||
| ): void => { |  | ||||||
|   fireEvent(element, "show-dialog", { |  | ||||||
|     dialogTag: "dialog-energy-grid-power-settings", |  | ||||||
|     dialogImport: () => import("./dialog-energy-grid-power-settings"), |  | ||||||
|     dialogParams: dialogParams, |  | ||||||
|   }); |  | ||||||
| }; |  | ||||||
|   | |||||||
| @@ -31,7 +31,7 @@ import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog- | |||||||
| import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; | import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; | ||||||
| import "../../../layouts/hass-subpage"; | import "../../../layouts/hass-subpage"; | ||||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||||
| import type { ECOption } from "../../../resources/echarts"; | import type { ECOption } from "../../../resources/echarts/echarts"; | ||||||
| import { haStyle } from "../../../resources/styles"; | import { haStyle } from "../../../resources/styles"; | ||||||
| import { DefaultPrimaryColor } from "../../../resources/theme/color/color.globals"; | import { DefaultPrimaryColor } from "../../../resources/theme/color/color.globals"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
|   | |||||||
| @@ -2,6 +2,7 @@ import { | |||||||
|   mdiAlertCircle, |   mdiAlertCircle, | ||||||
|   mdiChevronDown, |   mdiChevronDown, | ||||||
|   mdiCogOutline, |   mdiCogOutline, | ||||||
|  |   mdiContentCopy, | ||||||
|   mdiDelete, |   mdiDelete, | ||||||
|   mdiDevices, |   mdiDevices, | ||||||
|   mdiDotsVertical, |   mdiDotsVertical, | ||||||
| @@ -71,6 +72,8 @@ import { | |||||||
| import "./ha-config-entry-device-row"; | import "./ha-config-entry-device-row"; | ||||||
| import { renderConfigEntryError } from "./ha-config-integration-page"; | import { renderConfigEntryError } from "./ha-config-integration-page"; | ||||||
| import "./ha-config-sub-entry-row"; | import "./ha-config-sub-entry-row"; | ||||||
|  | import { copyToClipboard } from "../../../common/util/copy-clipboard"; | ||||||
|  | import { showToast } from "../../../util/toast"; | ||||||
|  |  | ||||||
| @customElement("ha-config-entry-row") | @customElement("ha-config-entry-row") | ||||||
| class HaConfigEntryRow extends LitElement { | class HaConfigEntryRow extends LitElement { | ||||||
| @@ -315,6 +318,13 @@ class HaConfigEntryRow extends LitElement { | |||||||
|             )} |             )} | ||||||
|           </ha-md-menu-item> |           </ha-md-menu-item> | ||||||
|  |  | ||||||
|  |           <ha-md-menu-item @click=${this._handleCopy} graphic="icon"> | ||||||
|  |             <ha-svg-icon slot="start" .path=${mdiContentCopy}></ha-svg-icon> | ||||||
|  |             ${this.hass.localize( | ||||||
|  |               "ui.panel.config.integrations.config_entry.copy" | ||||||
|  |             )} | ||||||
|  |           </ha-md-menu-item> | ||||||
|  |  | ||||||
|           ${Object.keys(item.supported_subentry_types).map( |           ${Object.keys(item.supported_subentry_types).map( | ||||||
|             (flowType) => |             (flowType) => | ||||||
|               html`<ha-md-menu-item |               html`<ha-md-menu-item | ||||||
| @@ -623,6 +633,15 @@ class HaConfigEntryRow extends LitElement { | |||||||
|     }); |     }); | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private async _handleCopy() { | ||||||
|  |     await copyToClipboard(this.entry.entry_id); | ||||||
|  |     showToast(this, { | ||||||
|  |       message: | ||||||
|  |         this.hass?.localize("ui.common.copied_clipboard") || | ||||||
|  |         "Copied to clipboard", | ||||||
|  |     }); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   private async _handleRename() { |   private async _handleRename() { | ||||||
|     const newName = await showPromptDialog(this, { |     const newName = await showPromptDialog(this, { | ||||||
|       title: this.hass.localize("ui.panel.config.integrations.rename_dialog"), |       title: this.hass.localize("ui.panel.config.integrations.rename_dialog"), | ||||||
|   | |||||||
| @@ -128,11 +128,10 @@ class ZHAAddDevicesPage extends LitElement { | |||||||
|                               this.hass, |                               this.hass, | ||||||
|                               "/integrations/zha#adding-devices" |                               "/integrations/zha#adding-devices" | ||||||
|                             )} |                             )} | ||||||
|                           > |                             >${this.hass.localize( | ||||||
|                             ${this.hass.localize( |  | ||||||
|                               "ui.panel.config.zha.add_device_page.pairing_mode_link" |                               "ui.panel.config.zha.add_device_page.pairing_mode_link" | ||||||
|                             )} |                             )}</a | ||||||
|                           </a> |                           > | ||||||
|                         `, |                         `, | ||||||
|                       } |                       } | ||||||
|                     )} |                     )} | ||||||
|   | |||||||
| @@ -110,191 +110,200 @@ class ZHAConfigDashboard extends LitElement { | |||||||
|         back-path="/config/integrations" |         back-path="/config/integrations" | ||||||
|         has-fab |         has-fab | ||||||
|       > |       > | ||||||
|         <ha-card class="content network-status"> |         <div class="container"> | ||||||
|           ${this._error |           <ha-card class="content network-status"> | ||||||
|             ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` |             ${this._error | ||||||
|             : nothing} |               ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||||
|           <div class="card-content"> |               : nothing} | ||||||
|             <div class="heading"> |             <div class="card-content"> | ||||||
|               <div class="icon"> |               <div class="heading"> | ||||||
|                 <ha-svg-icon |                 <div class="icon"> | ||||||
|                   .path=${deviceOnline ? mdiCheckCircle : mdiAlertCircle} |                   <ha-svg-icon | ||||||
|                   class=${deviceOnline ? "online" : "offline"} |                     .path=${deviceOnline ? mdiCheckCircle : mdiAlertCircle} | ||||||
|                 ></ha-svg-icon> |                     class=${deviceOnline ? "online" : "offline"} | ||||||
|               </div> |                   ></ha-svg-icon> | ||||||
|               <div class="details"> |                 </div> | ||||||
|                 ZHA |                 <div class="details"> | ||||||
|                 ${this.hass.localize( |                   ZHA | ||||||
|                   "ui.panel.config.zha.configuration_page.status_title" |  | ||||||
|                 )}: |  | ||||||
|                 ${this.hass.localize( |  | ||||||
|                   `ui.panel.config.zha.configuration_page.status_${deviceOnline ? "online" : "offline"}` |  | ||||||
|                 )}<br /> |  | ||||||
|                 <small> |  | ||||||
|                   ${this.hass.localize( |                   ${this.hass.localize( | ||||||
|                     "ui.panel.config.zha.configuration_page.devices", |                     "ui.panel.config.zha.configuration_page.status_title" | ||||||
|                     { count: this._totalDevices } |                   )}: | ||||||
|                   )} |                   ${this.hass.localize( | ||||||
|                 </small> |                     `ui.panel.config.zha.configuration_page.status_${deviceOnline ? "online" : "offline"}` | ||||||
|                 <small class="offline"> |                   )}<br /> | ||||||
|                   ${this._offlineDevices > 0 |                   <small> | ||||||
|                     ? html`(${this.hass.localize( |                     ${this.hass.localize( | ||||||
|                         "ui.panel.config.zha.configuration_page.devices_offline", |                       "ui.panel.config.zha.configuration_page.devices", | ||||||
|                         { count: this._offlineDevices } |                       { count: this._totalDevices } | ||||||
|                       )})` |                     )} | ||||||
|                     : nothing} |                   </small> | ||||||
|                 </small> |                   <small class="offline"> | ||||||
|  |                     ${this._offlineDevices > 0 | ||||||
|  |                       ? html`(${this.hass.localize( | ||||||
|  |                           "ui.panel.config.zha.configuration_page.devices_offline", | ||||||
|  |                           { count: this._offlineDevices } | ||||||
|  |                         )})` | ||||||
|  |                       : nothing} | ||||||
|  |                   </small> | ||||||
|  |                 </div> | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|           </div> |             ${this.configEntryId | ||||||
|           ${this.configEntryId |               ? html`<div class="card-actions"> | ||||||
|             ? html`<div class="card-actions"> |                   <ha-button | ||||||
|                 <ha-button |                     href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`} | ||||||
|                   href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`} |                     appearance="plain" | ||||||
|                   appearance="plain" |                     size="small" | ||||||
|                   size="small" |  | ||||||
|                 > |  | ||||||
|                   ${this.hass.localize( |  | ||||||
|                     "ui.panel.config.devices.caption" |  | ||||||
|                   )}</ha-button |  | ||||||
|                 > |  | ||||||
|                 <ha-button |  | ||||||
|                   appearance="plain" |  | ||||||
|                   size="small" |  | ||||||
|                   href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`} |  | ||||||
|                 > |  | ||||||
|                   ${this.hass.localize( |  | ||||||
|                     "ui.panel.config.entities.caption" |  | ||||||
|                   )}</ha-button |  | ||||||
|                 > |  | ||||||
|               </div>` |  | ||||||
|             : ""} |  | ||||||
|         </ha-card> |  | ||||||
|         <ha-card |  | ||||||
|           class="network-settings" |  | ||||||
|           header=${this.hass.localize( |  | ||||||
|             "ui.panel.config.zha.configuration_page.network_settings_title" |  | ||||||
|           )} |  | ||||||
|         > |  | ||||||
|           ${this._networkSettings |  | ||||||
|             ? html`<div class="card-content"> |  | ||||||
|                 <ha-settings-row> |  | ||||||
|                   <span slot="description">PAN ID</span> |  | ||||||
|                   <span slot="heading" |  | ||||||
|                     >${this._networkSettings.settings.network_info.pan_id}</span |  | ||||||
|                   > |                   > | ||||||
|                 </ha-settings-row> |                     ${this.hass.localize( | ||||||
|  |                       "ui.panel.config.devices.caption" | ||||||
|                 <ha-settings-row> |                     )}</ha-button | ||||||
|                   <span slot="heading" |  | ||||||
|                     >${this._networkSettings.settings.network_info |  | ||||||
|                       .extended_pan_id}</span |  | ||||||
|                   > |                   > | ||||||
|                   <span slot="description">Extended PAN ID</span> |                   <ha-button | ||||||
|                 </ha-settings-row> |                     appearance="plain" | ||||||
|  |                     size="small" | ||||||
|                 <ha-settings-row> |                     href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`} | ||||||
|                   <span slot="description">Channel</span> |  | ||||||
|                   <span slot="heading" |  | ||||||
|                     >${this._networkSettings.settings.network_info |  | ||||||
|                       .channel}</span |  | ||||||
|                   > |                   > | ||||||
|  |                     ${this.hass.localize( | ||||||
|                   <ha-icon-button |                       "ui.panel.config.entities.caption" | ||||||
|                     .label=${this.hass.localize( |                     )}</ha-button | ||||||
|                       "ui.panel.config.zha.configuration_page.change_channel" |  | ||||||
|                     )} |  | ||||||
|                     .path=${mdiPencil} |  | ||||||
|                     @click=${this._showChannelMigrationDialog} |  | ||||||
|                   > |                   > | ||||||
|                   </ha-icon-button> |                 </div>` | ||||||
|                 </ha-settings-row> |               : ""} | ||||||
|  |           </ha-card> | ||||||
|  |           <ha-card | ||||||
|  |             class="network-settings" | ||||||
|  |             header=${this.hass.localize( | ||||||
|  |               "ui.panel.config.zha.configuration_page.network_settings_title" | ||||||
|  |             )} | ||||||
|  |           > | ||||||
|  |             ${this._networkSettings | ||||||
|  |               ? html`<div class="card-content"> | ||||||
|  |                   <ha-settings-row> | ||||||
|  |                     <span slot="description">PAN ID</span> | ||||||
|  |                     <span slot="heading" | ||||||
|  |                       >${this._networkSettings.settings.network_info | ||||||
|  |                         .pan_id}</span | ||||||
|  |                     > | ||||||
|  |                   </ha-settings-row> | ||||||
|  |  | ||||||
|                 <ha-settings-row> |                   <ha-settings-row> | ||||||
|                   <span slot="description">Coordinator IEEE</span> |                     <span slot="heading" | ||||||
|                   <span slot="heading" |                       >${this._networkSettings.settings.network_info | ||||||
|                     >${this._networkSettings.settings.node_info.ieee}</span |                         .extended_pan_id}</span | ||||||
|                   > |                     > | ||||||
|                 </ha-settings-row> |                     <span slot="description">Extended PAN ID</span> | ||||||
|  |                   </ha-settings-row> | ||||||
|  |  | ||||||
|                 <ha-settings-row> |                   <ha-settings-row> | ||||||
|                   <span slot="description">Radio type</span> |                     <span slot="description">Channel</span> | ||||||
|                   <span slot="heading" |                     <span slot="heading" | ||||||
|                     >${this._networkSettings.radio_type}</span |                       >${this._networkSettings.settings.network_info | ||||||
|                   > |                         .channel}</span | ||||||
|                 </ha-settings-row> |                     > | ||||||
|  |  | ||||||
|                 <ha-settings-row> |                     <ha-icon-button | ||||||
|                   <span slot="description">Serial port</span> |                       .label=${this.hass.localize( | ||||||
|                   <span slot="heading" |                         "ui.panel.config.zha.configuration_page.change_channel" | ||||||
|                     >${this._networkSettings.device.path}</span |  | ||||||
|                   > |  | ||||||
|                 </ha-settings-row> |  | ||||||
|  |  | ||||||
|                 ${this._networkSettings.device.baudrate && |  | ||||||
|                 !this._networkSettings.device.path.startsWith("socket://") |  | ||||||
|                   ? html` |  | ||||||
|                       <ha-settings-row> |  | ||||||
|                         <span slot="description">Baudrate</span> |  | ||||||
|                         <span slot="heading" |  | ||||||
|                           >${this._networkSettings.device.baudrate}</span |  | ||||||
|                         > |  | ||||||
|                       </ha-settings-row> |  | ||||||
|                     ` |  | ||||||
|                   : ""} |  | ||||||
|               </div>` |  | ||||||
|             : ""} |  | ||||||
|           <div class="card-actions"> |  | ||||||
|             <ha-progress-button |  | ||||||
|               appearance="plain" |  | ||||||
|               @click=${this._createAndDownloadBackup} |  | ||||||
|               .progress=${this._generatingBackup} |  | ||||||
|               .disabled=${!this._networkSettings || this._generatingBackup} |  | ||||||
|             > |  | ||||||
|               ${this.hass.localize( |  | ||||||
|                 "ui.panel.config.zha.configuration_page.download_backup" |  | ||||||
|               )} |  | ||||||
|             </ha-progress-button> |  | ||||||
|             <ha-button variant="danger" @click=${this._openOptionFlow}> |  | ||||||
|               ${this.hass.localize( |  | ||||||
|                 "ui.panel.config.zha.configuration_page.migrate_radio" |  | ||||||
|               )} |  | ||||||
|             </ha-button> |  | ||||||
|           </div> |  | ||||||
|         </ha-card> |  | ||||||
|         ${this._configuration |  | ||||||
|           ? Object.entries(this._configuration.schemas).map( |  | ||||||
|               ([section, schema]) => |  | ||||||
|                 html`<ha-card |  | ||||||
|                   header=${this.hass.localize( |  | ||||||
|                     `component.zha.config_panel.${section}.title` |  | ||||||
|                   )} |  | ||||||
|                 > |  | ||||||
|                   <div class="card-content"> |  | ||||||
|                     <ha-form |  | ||||||
|                       .hass=${this.hass} |  | ||||||
|                       .schema=${schema} |  | ||||||
|                       .data=${this._configuration!.data[section]} |  | ||||||
|                       @value-changed=${this._dataChanged} |  | ||||||
|                       .section=${section} |  | ||||||
|                       .computeLabel=${this._computeLabelCallback( |  | ||||||
|                         this.hass.localize, |  | ||||||
|                         section |  | ||||||
|                       )} |                       )} | ||||||
|                     ></ha-form> |                       .path=${mdiPencil} | ||||||
|                   </div> |                       @click=${this._showChannelMigrationDialog} | ||||||
|                 </ha-card>` |                     > | ||||||
|             ) |                     </ha-icon-button> | ||||||
|           : ""} |                   </ha-settings-row> | ||||||
|         <ha-card> |  | ||||||
|           <div class="card-actions"> |                   <ha-settings-row> | ||||||
|             <ha-button @click=${this._updateConfiguration}> |                     <span slot="description">Coordinator IEEE</span> | ||||||
|               ${this.hass.localize( |                     <span slot="heading" | ||||||
|                 "ui.panel.config.zha.configuration_page.update_button" |                       >${this._networkSettings.settings.node_info.ieee}</span | ||||||
|               )} |                     > | ||||||
|             </ha-button> |                   </ha-settings-row> | ||||||
|           </div> |  | ||||||
|         </ha-card> |                   <ha-settings-row> | ||||||
|  |                     <span slot="description">Radio type</span> | ||||||
|  |                     <span slot="heading" | ||||||
|  |                       >${this._networkSettings.radio_type}</span | ||||||
|  |                     > | ||||||
|  |                   </ha-settings-row> | ||||||
|  |  | ||||||
|  |                   <ha-settings-row> | ||||||
|  |                     <span slot="description">Serial port</span> | ||||||
|  |                     <span slot="heading" | ||||||
|  |                       >${this._networkSettings.device.path}</span | ||||||
|  |                     > | ||||||
|  |                   </ha-settings-row> | ||||||
|  |  | ||||||
|  |                   ${this._networkSettings.device.baudrate && | ||||||
|  |                   !this._networkSettings.device.path.startsWith("socket://") | ||||||
|  |                     ? html` | ||||||
|  |                         <ha-settings-row> | ||||||
|  |                           <span slot="description">Baudrate</span> | ||||||
|  |                           <span slot="heading" | ||||||
|  |                             >${this._networkSettings.device.baudrate}</span | ||||||
|  |                           > | ||||||
|  |                         </ha-settings-row> | ||||||
|  |                       ` | ||||||
|  |                     : nothing} | ||||||
|  |                 </div>` | ||||||
|  |               : nothing} | ||||||
|  |             <div class="card-actions"> | ||||||
|  |               <ha-progress-button | ||||||
|  |                 appearance="plain" | ||||||
|  |                 @click=${this._createAndDownloadBackup} | ||||||
|  |                 .progress=${this._generatingBackup} | ||||||
|  |                 .disabled=${!this._networkSettings || this._generatingBackup} | ||||||
|  |               > | ||||||
|  |                 ${this.hass.localize( | ||||||
|  |                   "ui.panel.config.zha.configuration_page.download_backup" | ||||||
|  |                 )} | ||||||
|  |               </ha-progress-button> | ||||||
|  |               <ha-button | ||||||
|  |                 appearance="filled" | ||||||
|  |                 variant="brand" | ||||||
|  |                 @click=${this._openOptionFlow} | ||||||
|  |               > | ||||||
|  |                 ${this.hass.localize( | ||||||
|  |                   "ui.panel.config.zha.configuration_page.migrate_radio" | ||||||
|  |                 )} | ||||||
|  |               </ha-button> | ||||||
|  |             </div> | ||||||
|  |           </ha-card> | ||||||
|  |           ${this._configuration | ||||||
|  |             ? Object.entries(this._configuration.schemas).map( | ||||||
|  |                 ([section, schema]) => | ||||||
|  |                   html`<ha-card | ||||||
|  |                     header=${this.hass.localize( | ||||||
|  |                       `component.zha.config_panel.${section}.title` | ||||||
|  |                     )} | ||||||
|  |                   > | ||||||
|  |                     <div class="card-content"> | ||||||
|  |                       <ha-form | ||||||
|  |                         .hass=${this.hass} | ||||||
|  |                         .schema=${schema} | ||||||
|  |                         .data=${this._configuration!.data[section]} | ||||||
|  |                         @value-changed=${this._dataChanged} | ||||||
|  |                         .section=${section} | ||||||
|  |                         .computeLabel=${this._computeLabelCallback( | ||||||
|  |                           this.hass.localize, | ||||||
|  |                           section | ||||||
|  |                         )} | ||||||
|  |                       ></ha-form> | ||||||
|  |                     </div> | ||||||
|  |                     <div class="card-actions"> | ||||||
|  |                       <ha-button | ||||||
|  |                         appearance="filled" | ||||||
|  |                         variant="brand" | ||||||
|  |                         @click=${this._updateConfiguration} | ||||||
|  |                       > | ||||||
|  |                         ${this.hass.localize( | ||||||
|  |                           "ui.panel.config.zha.configuration_page.update_button" | ||||||
|  |                         )} | ||||||
|  |                       </ha-button> | ||||||
|  |                     </div> | ||||||
|  |                   </ha-card>` | ||||||
|  |               ) | ||||||
|  |             : nothing} | ||||||
|  |         </div> | ||||||
|  |  | ||||||
|         <a href="/config/zha/add" slot="fab"> |         <a href="/config/zha/add" slot="fab"> | ||||||
|           <ha-fab |           <ha-fab | ||||||
| @@ -489,6 +498,10 @@ class ZHAConfigDashboard extends LitElement { | |||||||
|         .network-status .offline { |         .network-status .offline { | ||||||
|           color: var(--error-color, var(--error-color)); |           color: var(--error-color, var(--error-color)); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         .container { | ||||||
|  |           padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4); | ||||||
|  |         } | ||||||
|       `, |       `, | ||||||
|     ]; |     ]; | ||||||
|   } |   } | ||||||
|   | |||||||
| @@ -999,6 +999,7 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { | |||||||
|           display: flex; |           display: flex; | ||||||
|           gap: var(--ha-space-2); |           gap: var(--ha-space-2); | ||||||
|           margin-left: auto; |           margin-left: auto; | ||||||
|  |           flex-wrap: wrap; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|         .container { |         .container { | ||||||
|   | |||||||
| @@ -318,13 +318,13 @@ export class HaConfigLovelaceDashboards extends LitElement { | |||||||
|         }); |         }); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       if (this.hass.panels.security) { |       if (this.hass.panels.safety) { | ||||||
|         result.push({ |         result.push({ | ||||||
|           icon: "mdi:security", |           icon: "mdi:security", | ||||||
|           title: this.hass.localize("panel.security"), |           title: this.hass.localize("panel.safety"), | ||||||
|           show_in_sidebar: false, |           show_in_sidebar: false, | ||||||
|           mode: "storage", |           mode: "storage", | ||||||
|           url_path: "security", |           url_path: "safety", | ||||||
|           filename: "", |           filename: "", | ||||||
|           iconColor: "var(--blue-grey-color)", |           iconColor: "var(--blue-grey-color)", | ||||||
|           default: false, |           default: false, | ||||||
|   | |||||||
| @@ -1,10 +1,10 @@ | |||||||
|  | import type { ActionDetail } from "@material/mwc-list"; | ||||||
| import { | import { | ||||||
|   mdiDotsVertical, |   mdiDotsVertical, | ||||||
|   mdiDownload, |   mdiDownload, | ||||||
|   mdiFilterRemove, |   mdiFilterRemove, | ||||||
|   mdiImagePlus, |   mdiImagePlus, | ||||||
| } from "@mdi/js"; | } from "@mdi/js"; | ||||||
| import type { ActionDetail } from "@material/mwc-list"; |  | ||||||
| import { differenceInHours } from "date-fns"; | import { differenceInHours } from "date-fns"; | ||||||
| import type { | import type { | ||||||
|   HassServiceTarget, |   HassServiceTarget, | ||||||
| @@ -27,21 +27,21 @@ import { | |||||||
| import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base"; | import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base"; | ||||||
| import "../../components/chart/state-history-charts"; | import "../../components/chart/state-history-charts"; | ||||||
| import type { StateHistoryCharts } from "../../components/chart/state-history-charts"; | import type { StateHistoryCharts } from "../../components/chart/state-history-charts"; | ||||||
| import "../../components/ha-spinner"; | import "../../components/ha-button-menu"; | ||||||
| import "../../components/ha-date-range-picker"; | import "../../components/ha-date-range-picker"; | ||||||
| import "../../components/ha-icon-button"; | import "../../components/ha-icon-button"; | ||||||
| import "../../components/ha-button-menu"; |  | ||||||
| import "../../components/ha-list-item"; |  | ||||||
| import "../../components/ha-icon-button-arrow-prev"; | import "../../components/ha-icon-button-arrow-prev"; | ||||||
|  | import "../../components/ha-list-item"; | ||||||
| import "../../components/ha-menu-button"; | import "../../components/ha-menu-button"; | ||||||
|  | import "../../components/ha-spinner"; | ||||||
| import "../../components/ha-target-picker"; | import "../../components/ha-target-picker"; | ||||||
| import "../../components/ha-top-app-bar-fixed"; | import "../../components/ha-top-app-bar-fixed"; | ||||||
| import type { HistoryResult } from "../../data/history"; | import type { HistoryResult } from "../../data/history"; | ||||||
| import { | import { | ||||||
|   computeHistory, |   computeHistory, | ||||||
|   subscribeHistory, |  | ||||||
|   mergeHistoryResults, |  | ||||||
|   convertStatisticsToHistory, |   convertStatisticsToHistory, | ||||||
|  |   mergeHistoryResults, | ||||||
|  |   subscribeHistory, | ||||||
| } from "../../data/history"; | } from "../../data/history"; | ||||||
| import { fetchStatistics } from "../../data/recorder"; | import { fetchStatistics } from "../../data/recorder"; | ||||||
| import { resolveEntityIDs } from "../../data/selector"; | import { resolveEntityIDs } from "../../data/selector"; | ||||||
| @@ -182,6 +182,7 @@ class HaPanelHistory extends LitElement { | |||||||
|               .disabled=${this._isLoading} |               .disabled=${this._isLoading} | ||||||
|               add-on-top |               add-on-top | ||||||
|               @value-changed=${this._targetsChanged} |               @value-changed=${this._targetsChanged} | ||||||
|  |               compact | ||||||
|             ></ha-target-picker> |             ></ha-target-picker> | ||||||
|           </div> |           </div> | ||||||
|           ${this._isLoading |           ${this._isLoading | ||||||
| @@ -649,6 +650,10 @@ class HaPanelHistory extends LitElement { | |||||||
|           direction: var(--direction); |           direction: var(--direction); | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         ha-target-picker { | ||||||
|  |           flex: 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         @media all and (max-width: 1025px) { |         @media all and (max-width: 1025px) { | ||||||
|           .filters { |           .filters { | ||||||
|             flex-direction: column; |             flex-direction: column; | ||||||
|   | |||||||
| @@ -1,9 +1,11 @@ | |||||||
| import { mdiRefresh } from "@mdi/js"; | import { mdiRefresh } from "@mdi/js"; | ||||||
|  | import type { HassServiceTarget } from "home-assistant-js-websocket"; | ||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { css, html, LitElement } from "lit"; | import { css, html, LitElement } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import type { HassServiceTarget } from "home-assistant-js-websocket"; |  | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
|  | import { ensureArray } from "../../common/array/ensure-array"; | ||||||
|  | import { storage } from "../../common/decorators/storage"; | ||||||
| import { goBack, navigate } from "../../common/navigate"; | import { goBack, navigate } from "../../common/navigate"; | ||||||
| import { constructUrlCurrentPath } from "../../common/url/construct-url"; | import { constructUrlCurrentPath } from "../../common/url/construct-url"; | ||||||
| import { | import { | ||||||
| @@ -16,17 +18,15 @@ import "../../components/ha-date-range-picker"; | |||||||
| import "../../components/ha-icon-button"; | import "../../components/ha-icon-button"; | ||||||
| import "../../components/ha-icon-button-arrow-prev"; | import "../../components/ha-icon-button-arrow-prev"; | ||||||
| import "../../components/ha-menu-button"; | import "../../components/ha-menu-button"; | ||||||
| import "../../components/ha-top-app-bar-fixed"; |  | ||||||
| import "../../components/ha-target-picker"; | import "../../components/ha-target-picker"; | ||||||
|  | import "../../components/ha-top-app-bar-fixed"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; | ||||||
| import { filterLogbookCompatibleEntities } from "../../data/logbook"; | import { filterLogbookCompatibleEntities } from "../../data/logbook"; | ||||||
|  | import { resolveEntityIDs } from "../../data/selector"; | ||||||
|  | import { getSensorNumericDeviceClasses } from "../../data/sensor"; | ||||||
| import { haStyle } from "../../resources/styles"; | import { haStyle } from "../../resources/styles"; | ||||||
| import type { HomeAssistant } from "../../types"; | import type { HomeAssistant } from "../../types"; | ||||||
| import "./ha-logbook"; | import "./ha-logbook"; | ||||||
| import { storage } from "../../common/decorators/storage"; |  | ||||||
| import { ensureArray } from "../../common/array/ensure-array"; |  | ||||||
| import { resolveEntityIDs } from "../../data/selector"; |  | ||||||
| import { getSensorNumericDeviceClasses } from "../../data/sensor"; |  | ||||||
| import type { HaEntityPickerEntityFilterFunc } from "../../components/entity/ha-entity-picker"; |  | ||||||
|  |  | ||||||
| @customElement("ha-panel-logbook") | @customElement("ha-panel-logbook") | ||||||
| export class HaPanelLogbook extends LitElement { | export class HaPanelLogbook extends LitElement { | ||||||
| @@ -108,6 +108,7 @@ export class HaPanelLogbook extends LitElement { | |||||||
|               .value=${this._targetPickerValue} |               .value=${this._targetPickerValue} | ||||||
|               add-on-top |               add-on-top | ||||||
|               @value-changed=${this._targetsChanged} |               @value-changed=${this._targetsChanged} | ||||||
|  |               compact | ||||||
|             ></ha-target-picker> |             ></ha-target-picker> | ||||||
|           </div> |           </div> | ||||||
|  |  | ||||||
| @@ -363,6 +364,10 @@ export class HaPanelLogbook extends LitElement { | |||||||
|           max-width: 400px; |           max-width: 400px; | ||||||
|         } |         } | ||||||
|  |  | ||||||
|  |         ha-target-picker { | ||||||
|  |           flex: 1; | ||||||
|  |         } | ||||||
|  |  | ||||||
|         :host([narrow]) ha-entity-picker { |         :host([narrow]) ha-entity-picker { | ||||||
|           max-width: none; |           max-width: none; | ||||||
|           width: 100%; |           width: 100%; | ||||||
|   | |||||||
| @@ -9,9 +9,9 @@ import { computeCssColor } from "../../../common/color/compute-color"; | |||||||
| import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; | import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; | ||||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | import { computeDomain } from "../../../common/entity/compute_domain"; | ||||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import { stateActive } from "../../../common/entity/state_active"; | import { stateActive } from "../../../common/entity/state_active"; | ||||||
| import { stateColorCss } from "../../../common/entity/state_color"; | import { stateColorCss } from "../../../common/entity/state_color"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import "../../../components/ha-badge"; | import "../../../components/ha-badge"; | ||||||
| import "../../../components/ha-ripple"; | import "../../../components/ha-ripple"; | ||||||
| import "../../../components/ha-state-icon"; | import "../../../components/ha-state-icon"; | ||||||
| @@ -189,7 +189,11 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { | |||||||
|       </state-display> |       </state-display> | ||||||
|     `; |     `; | ||||||
|  |  | ||||||
|     const name = this._config.name || computeStateName(stateObj); |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const showState = this._config.show_state; |     const showState = this._config.show_state; | ||||||
|     const showName = this._config.show_name; |     const showName = this._config.show_name; | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display"; | ||||||
| import type { ActionConfig } from "../../../data/lovelace/config/action"; | import type { ActionConfig } from "../../../data/lovelace/config/action"; | ||||||
| import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; | import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; | ||||||
| import type { LegacyStateFilter } from "../common/evaluate-filter"; | import type { LegacyStateFilter } from "../common/evaluate-filter"; | ||||||
| @@ -31,7 +32,7 @@ export interface StateLabelBadgeConfig extends LovelaceBadgeConfig { | |||||||
| export interface EntityBadgeConfig extends LovelaceBadgeConfig { | export interface EntityBadgeConfig extends LovelaceBadgeConfig { | ||||||
|   type: "entity"; |   type: "entity"; | ||||||
|   entity?: string; |   entity?: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   icon?: string; |   icon?: string; | ||||||
|   color?: string; |   color?: string; | ||||||
|   show_name?: boolean; |   show_name?: boolean; | ||||||
|   | |||||||
| @@ -43,6 +43,8 @@ class HuiHistoryChartCardFeature | |||||||
|  |  | ||||||
|   @state() private _coordinates?: [number, number][]; |   @state() private _coordinates?: [number, number][]; | ||||||
|  |  | ||||||
|  |   @state() private _yAxisOrigin?: number; | ||||||
|  |  | ||||||
|   private _interval?: number; |   private _interval?: number; | ||||||
|  |  | ||||||
|   static getStubConfig(): TrendGraphCardFeatureConfig { |   static getStubConfig(): TrendGraphCardFeatureConfig { | ||||||
| @@ -105,7 +107,10 @@ class HuiHistoryChartCardFeature | |||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|     return html` |     return html` | ||||||
|       <hui-graph-base .coordinates=${this._coordinates}></hui-graph-base> |       <hui-graph-base | ||||||
|  |         .coordinates=${this._coordinates} | ||||||
|  |         .yAxisOrigin=${this._yAxisOrigin} | ||||||
|  |       ></hui-graph-base> | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -123,14 +128,15 @@ class HuiHistoryChartCardFeature | |||||||
|     return subscribeHistoryStatesTimeWindow( |     return subscribeHistoryStatesTimeWindow( | ||||||
|       this.hass!, |       this.hass!, | ||||||
|       (historyStates) => { |       (historyStates) => { | ||||||
|         this._coordinates = |         const { points, yAxisOrigin } = | ||||||
|           coordinatesMinimalResponseCompressedState( |           coordinatesMinimalResponseCompressedState( | ||||||
|             historyStates[this.context!.entity_id!], |             historyStates[this.context!.entity_id!], | ||||||
|             hourToShow, |             this.clientWidth, | ||||||
|             500, |             this.clientHeight, | ||||||
|             2, |             this.clientWidth / 5 // sample to 1 point per 5 pixels | ||||||
|             undefined |           ); | ||||||
|           ) || []; |         this._coordinates = points; | ||||||
|  |         this._yAxisOrigin = yAxisOrigin; | ||||||
|       }, |       }, | ||||||
|       hourToShow, |       hourToShow, | ||||||
|       [this.context!.entity_id!] |       [this.context!.entity_id!] | ||||||
|   | |||||||
| @@ -16,7 +16,6 @@ import { | |||||||
| import type { | import type { | ||||||
|   BarSeriesOption, |   BarSeriesOption, | ||||||
|   CallbackDataParams, |   CallbackDataParams, | ||||||
|   LineSeriesOption, |  | ||||||
|   TopLevelFormatterParams, |   TopLevelFormatterParams, | ||||||
| } from "echarts/types/dist/shared"; | } from "echarts/types/dist/shared"; | ||||||
| import type { FrontendLocaleData } from "../../../../../data/translation"; | import type { FrontendLocaleData } from "../../../../../data/translation"; | ||||||
| @@ -27,7 +26,7 @@ import { | |||||||
|   formatDateVeryShort, |   formatDateVeryShort, | ||||||
| } from "../../../../../common/datetime/format_date"; | } from "../../../../../common/datetime/format_date"; | ||||||
| import { formatTime } from "../../../../../common/datetime/format_time"; | import { formatTime } from "../../../../../common/datetime/format_time"; | ||||||
| import type { ECOption } from "../../../../../resources/echarts"; | import type { ECOption } from "../../../../../resources/echarts/echarts"; | ||||||
| import { filterXSS } from "../../../../../common/util/xss"; | import { filterXSS } from "../../../../../common/util/xss"; | ||||||
|  |  | ||||||
| export function getSuggestedMax(dayDifference: number, end: Date): number { | export function getSuggestedMax(dayDifference: number, end: Date): number { | ||||||
| @@ -171,10 +170,11 @@ function formatTooltip( | |||||||
|       compare |       compare | ||||||
|         ? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: ` |         ? `${(showCompareYear ? formatDateShort : formatDateVeryShort)(date, locale, config)}: ` | ||||||
|         : "" |         : "" | ||||||
|     }${formatTime(date, locale, config)}`; |     }${formatTime(date, locale, config)} – ${formatTime( | ||||||
|     if (params[0].componentSubType === "bar") { |       addHours(date, 1), | ||||||
|       period += ` – ${formatTime(addHours(date, 1), locale, config)}`; |       locale, | ||||||
|     } |       config | ||||||
|  |     )}`; | ||||||
|   } |   } | ||||||
|   const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`; |   const title = `<h4 style="text-align: center; margin: 0;">${period}</h4>`; | ||||||
|  |  | ||||||
| @@ -281,35 +281,6 @@ export function fillDataGapsAndRoundCaps(datasets: BarSeriesOption[]) { | |||||||
|   }); |   }); | ||||||
| } | } | ||||||
|  |  | ||||||
| export function fillLineGaps(datasets: LineSeriesOption[]) { |  | ||||||
|   const buckets = Array.from( |  | ||||||
|     new Set( |  | ||||||
|       datasets |  | ||||||
|         .map((dataset) => |  | ||||||
|           dataset.data!.map((datapoint) => Number(datapoint![0])) |  | ||||||
|         ) |  | ||||||
|         .flat() |  | ||||||
|     ) |  | ||||||
|   ).sort((a, b) => a - b); |  | ||||||
|   buckets.forEach((bucket, index) => { |  | ||||||
|     for (let i = datasets.length - 1; i >= 0; i--) { |  | ||||||
|       const dataPoint = datasets[i].data![index]; |  | ||||||
|       const item: any = |  | ||||||
|         dataPoint && typeof dataPoint === "object" && "value" in dataPoint |  | ||||||
|           ? dataPoint |  | ||||||
|           : { value: dataPoint }; |  | ||||||
|       const x = item.value?.[0]; |  | ||||||
|       if (x === undefined) { |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|       if (Number(x) !== bucket) { |  | ||||||
|         datasets[i].data?.splice(index, 0, [bucket, 0]); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|   }); |  | ||||||
|   return datasets; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export function getCompareTransform(start: Date, compareStart?: Date) { | export function getCompareTransform(start: Date, compareStart?: Date) { | ||||||
|   if (!compareStart) { |   if (!compareStart) { | ||||||
|     return (ts: Date) => ts; |     return (ts: Date) => ts; | ||||||
|   | |||||||
| @@ -36,7 +36,7 @@ import { | |||||||
|   getCompareTransform, |   getCompareTransform, | ||||||
| } from "./common/energy-chart-options"; | } from "./common/energy-chart-options"; | ||||||
| import { storage } from "../../../../common/decorators/storage"; | import { storage } from "../../../../common/decorators/storage"; | ||||||
| import type { ECOption } from "../../../../resources/echarts"; | import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||||
| import { formatNumber } from "../../../../common/number/format_number"; | import { formatNumber } from "../../../../common/number/format_number"; | ||||||
| import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base"; | import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,16 +2,22 @@ import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | |||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { css, html, LitElement, nothing } from "lit"; | import { css, html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
|  | import { mdiChartDonut, mdiChartBar } from "@mdi/js"; | ||||||
| import { classMap } from "lit/directives/class-map"; | import { classMap } from "lit/directives/class-map"; | ||||||
| import memoizeOne from "memoize-one"; | import memoizeOne from "memoize-one"; | ||||||
| import type { BarSeriesOption } from "echarts/charts"; | import type { BarSeriesOption, PieSeriesOption } from "echarts/charts"; | ||||||
|  | import { PieChart } from "echarts/charts"; | ||||||
| import type { ECElementEvent } from "echarts/types/dist/shared"; | import type { ECElementEvent } from "echarts/types/dist/shared"; | ||||||
| import { filterXSS } from "../../../../common/util/xss"; | import { filterXSS } from "../../../../common/util/xss"; | ||||||
| import { getGraphColorByIndex } from "../../../../common/color/colors"; | import { getGraphColorByIndex } from "../../../../common/color/colors"; | ||||||
| import { formatNumber } from "../../../../common/number/format_number"; | import { formatNumber } from "../../../../common/number/format_number"; | ||||||
| import "../../../../components/chart/ha-chart-base"; | import "../../../../components/chart/ha-chart-base"; | ||||||
| import type { EnergyData } from "../../../../data/energy"; | import type { EnergyData } from "../../../../data/energy"; | ||||||
| import { getEnergyDataCollection } from "../../../../data/energy"; | import { | ||||||
|  |   computeConsumptionData, | ||||||
|  |   getEnergyDataCollection, | ||||||
|  |   getSummedData, | ||||||
|  | } from "../../../../data/energy"; | ||||||
| import { | import { | ||||||
|   calculateStatisticSumGrowth, |   calculateStatisticSumGrowth, | ||||||
|   getStatisticLabel, |   getStatisticLabel, | ||||||
| @@ -22,10 +28,12 @@ import type { HomeAssistant } from "../../../../types"; | |||||||
| import type { LovelaceCard } from "../../types"; | import type { LovelaceCard } from "../../types"; | ||||||
| import type { EnergyDevicesGraphCardConfig } from "../types"; | import type { EnergyDevicesGraphCardConfig } from "../types"; | ||||||
| import { hasConfigChanged } from "../../common/has-changed"; | import { hasConfigChanged } from "../../common/has-changed"; | ||||||
| import type { ECOption } from "../../../../resources/echarts"; | import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||||
| import "../../../../components/ha-card"; | import "../../../../components/ha-card"; | ||||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | import { fireEvent } from "../../../../common/dom/fire_event"; | ||||||
| import { measureTextWidth } from "../../../../util/text"; | import { measureTextWidth } from "../../../../util/text"; | ||||||
|  | import "../../../../components/ha-icon-button"; | ||||||
|  | import { storage } from "../../../../common/decorators/storage"; | ||||||
|  |  | ||||||
| @customElement("hui-energy-devices-graph-card") | @customElement("hui-energy-devices-graph-card") | ||||||
| export class HuiEnergyDevicesGraphCard | export class HuiEnergyDevicesGraphCard | ||||||
| @@ -36,10 +44,20 @@ export class HuiEnergyDevicesGraphCard | |||||||
|  |  | ||||||
|   @state() private _config?: EnergyDevicesGraphCardConfig; |   @state() private _config?: EnergyDevicesGraphCardConfig; | ||||||
|  |  | ||||||
|   @state() private _chartData: BarSeriesOption[] = []; |   @state() private _chartData: (BarSeriesOption | PieSeriesOption)[] = []; | ||||||
|  |  | ||||||
|   @state() private _data?: EnergyData; |   @state() private _data?: EnergyData; | ||||||
|  |  | ||||||
|  |   @state() | ||||||
|  |   @storage({ | ||||||
|  |     key: "energy-devices-graph-chart-type", | ||||||
|  |     state: true, | ||||||
|  |     subscribe: false, | ||||||
|  |   }) | ||||||
|  |   private _chartType: "bar" | "pie" = "bar"; | ||||||
|  |  | ||||||
|  |   private _compoundStats: string[] = []; | ||||||
|  |  | ||||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; |   protected hassSubscribeRequiredHostProps = ["_config"]; | ||||||
|  |  | ||||||
|   public hassSubscribe(): UnsubscribeFunc[] { |   public hassSubscribe(): UnsubscribeFunc[] { | ||||||
| @@ -76,9 +94,16 @@ export class HuiEnergyDevicesGraphCard | |||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-card> |       <ha-card> | ||||||
|         ${this._config.title |         <div class="card-header"> | ||||||
|           ? html`<h1 class="card-header">${this._config.title}</h1>` |           <span>${this._config.title ? this._config.title : nothing}</span> | ||||||
|           : ""} |           <ha-icon-button | ||||||
|  |             .path=${this._chartType === "pie" ? mdiChartBar : mdiChartDonut} | ||||||
|  |             .label=${this.hass.localize( | ||||||
|  |               "ui.panel.lovelace.cards.energy.energy_devices_graph.change_chart_type" | ||||||
|  |             )} | ||||||
|  |             @click=${this._handleChartTypeChange} | ||||||
|  |           ></ha-icon-button> | ||||||
|  |         </div> | ||||||
|         <div |         <div | ||||||
|           class="content ${classMap({ |           class="content ${classMap({ | ||||||
|             "has-header": !!this._config.title, |             "has-header": !!this._config.title, | ||||||
| @@ -87,9 +112,10 @@ export class HuiEnergyDevicesGraphCard | |||||||
|           <ha-chart-base |           <ha-chart-base | ||||||
|             .hass=${this.hass} |             .hass=${this.hass} | ||||||
|             .data=${this._chartData} |             .data=${this._chartData} | ||||||
|             .options=${this._createOptions(this._chartData)} |             .options=${this._createOptions(this._chartData, this._chartType)} | ||||||
|             .height=${`${(this._chartData[0]?.data?.length || 0) * 28 + 50}px`} |             .height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`} | ||||||
|             @chart-click=${this._handleChartClick} |             @chart-click=${this._handleChartClick} | ||||||
|  |             .extraComponents=${[PieChart]} | ||||||
|           ></ha-chart-base> |           ></ha-chart-base> | ||||||
|         </div> |         </div> | ||||||
|       </ha-card> |       </ha-card> | ||||||
| @@ -97,71 +123,86 @@ export class HuiEnergyDevicesGraphCard | |||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _renderTooltip(params: any) { |   private _renderTooltip(params: any) { | ||||||
|     const deviceName = filterXSS(this._getDeviceName(params.value[1])); |     const deviceName = filterXSS(this._getDeviceName(params.name)); | ||||||
|     const title = `<h4 style="text-align: center; margin: 0;">${deviceName}</h4>`; |     const title = `<h4 style="text-align: center; margin: 0;">${deviceName}</h4>`; | ||||||
|     const value = `${formatNumber( |     const value = `${formatNumber( | ||||||
|       params.value[0] as number, |       params.value[0] as number, | ||||||
|       this.hass.locale, |       this.hass.locale, | ||||||
|       params.value[0] < 0.1 ? { maximumFractionDigits: 3 } : undefined |       params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined | ||||||
|     )} kWh`; |     )} kWh`; | ||||||
|     return `${title}${params.marker} ${params.seriesName}: ${value}`; |     return `${title}${params.marker} ${params.seriesName}: ${value}`; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => { |   private _createOptions = memoizeOne( | ||||||
|     const isMobile = window.matchMedia( |     ( | ||||||
|       "all and (max-width: 450px), all and (max-height: 500px)" |       data: (BarSeriesOption | PieSeriesOption)[], | ||||||
|     ).matches; |       chartType: "bar" | "pie" | ||||||
|     return { |     ): ECOption => { | ||||||
|       xAxis: { |       const options: ECOption = { | ||||||
|         type: "value", |         grid: { | ||||||
|         name: "kWh", |           top: 5, | ||||||
|       }, |           left: 5, | ||||||
|       yAxis: { |           right: 40, | ||||||
|         type: "category", |           bottom: 0, | ||||||
|         inverse: true, |           containLabel: true, | ||||||
|         triggerEvent: true, |  | ||||||
|         // take order from data |  | ||||||
|         data: data[0]?.data?.map((d: any) => d.value[1]), |  | ||||||
|         axisLabel: { |  | ||||||
|           formatter: this._getDeviceName.bind(this), |  | ||||||
|           overflow: "truncate", |  | ||||||
|           fontSize: 12, |  | ||||||
|           margin: 5, |  | ||||||
|           width: Math.min( |  | ||||||
|             isMobile ? 100 : 200, |  | ||||||
|             Math.max( |  | ||||||
|               ...(data[0]?.data?.map( |  | ||||||
|                 (d: any) => |  | ||||||
|                   measureTextWidth(this._getDeviceName(d.value[1]), 12) + 5 |  | ||||||
|               ) || []) |  | ||||||
|             ) |  | ||||||
|           ), |  | ||||||
|         }, |         }, | ||||||
|       }, |         tooltip: { | ||||||
|       grid: { |           show: true, | ||||||
|         top: 5, |           formatter: this._renderTooltip.bind(this), | ||||||
|         left: 5, |         }, | ||||||
|         right: 40, |         xAxis: { show: false }, | ||||||
|         bottom: 0, |         yAxis: { show: false }, | ||||||
|         containLabel: true, |       }; | ||||||
|       }, |       if (chartType === "bar") { | ||||||
|       tooltip: { |         const isMobile = window.matchMedia( | ||||||
|         show: true, |           "all and (max-width: 450px), all and (max-height: 500px)" | ||||||
|         formatter: this._renderTooltip.bind(this), |         ).matches; | ||||||
|       }, |         options.xAxis = { | ||||||
|     }; |           show: true, | ||||||
|   }); |           type: "value", | ||||||
|  |           name: "kWh", | ||||||
|  |         }; | ||||||
|  |         options.yAxis = { | ||||||
|  |           show: true, | ||||||
|  |           type: "category", | ||||||
|  |           inverse: true, | ||||||
|  |           triggerEvent: true, | ||||||
|  |           // take order from data | ||||||
|  |           data: data[0]?.data?.map((d: any) => d.name), | ||||||
|  |           axisLabel: { | ||||||
|  |             formatter: this._getDeviceName.bind(this), | ||||||
|  |             overflow: "truncate", | ||||||
|  |             fontSize: 12, | ||||||
|  |             margin: 5, | ||||||
|  |             width: Math.min( | ||||||
|  |               isMobile ? 100 : 200, | ||||||
|  |               Math.max( | ||||||
|  |                 ...(data[0]?.data?.map( | ||||||
|  |                   (d: any) => | ||||||
|  |                     measureTextWidth(this._getDeviceName(d.name), 12) + 5 | ||||||
|  |                 ) || []) | ||||||
|  |               ) | ||||||
|  |             ), | ||||||
|  |           }, | ||||||
|  |         }; | ||||||
|  |       } | ||||||
|  |       return options; | ||||||
|  |     } | ||||||
|  |   ); | ||||||
|  |  | ||||||
|   private _getDeviceName(statisticId: string): string { |   private _getDeviceName(statisticId: string): string { | ||||||
|  |     const suffix = this._compoundStats.includes(statisticId) | ||||||
|  |       ? ` (${this.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_graph.untracked")})` | ||||||
|  |       : ""; | ||||||
|     return ( |     return ( | ||||||
|       this._data?.prefs.device_consumption.find( |       (this._data?.prefs.device_consumption.find( | ||||||
|         (d) => d.stat_consumption === statisticId |         (d) => d.stat_consumption === statisticId | ||||||
|       )?.name || |       )?.name || | ||||||
|       getStatisticLabel( |         getStatisticLabel( | ||||||
|         this.hass, |           this.hass, | ||||||
|         statisticId, |           statisticId, | ||||||
|         this._data?.statsMetadata[statisticId] |           this._data?.statsMetadata[statisticId] | ||||||
|       ) |         )) + suffix | ||||||
|     ); |     ); | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -169,60 +210,105 @@ export class HuiEnergyDevicesGraphCard | |||||||
|     const data = energyData.stats; |     const data = energyData.stats; | ||||||
|     const compareData = energyData.statsCompare; |     const compareData = energyData.statsCompare; | ||||||
|  |  | ||||||
|     const chartData: NonNullable<BarSeriesOption["data"]> = []; |     const chartData: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> = | ||||||
|     const chartDataCompare: NonNullable<BarSeriesOption["data"]> = []; |       []; | ||||||
|  |     const chartDataCompare: NonNullable< | ||||||
|  |       (BarSeriesOption | PieSeriesOption)["data"] | ||||||
|  |     > = []; | ||||||
|  |  | ||||||
|     const datasets: BarSeriesOption[] = [ |     const datasets: (BarSeriesOption | PieSeriesOption)[] = [ | ||||||
|       { |       { | ||||||
|         type: "bar", |         type: this._chartType, | ||||||
|  |         radius: [compareData ? "50%" : "40%", "70%"], | ||||||
|  |         universalTransition: true, | ||||||
|         name: this.hass.localize( |         name: this.hass.localize( | ||||||
|           "ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage" |           "ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage" | ||||||
|         ), |         ), | ||||||
|         itemStyle: { |         itemStyle: { | ||||||
|           borderRadius: [0, 4, 4, 0], |           borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4, | ||||||
|         }, |         }, | ||||||
|         data: chartData, |         data: chartData, | ||||||
|         barWidth: compareData ? 10 : 20, |         barWidth: compareData ? 10 : 20, | ||||||
|         cursor: "default", |         cursor: "default", | ||||||
|       }, |         minShowLabelAngle: 15, | ||||||
|  |         label: | ||||||
|  |           this._chartType === "pie" | ||||||
|  |             ? { | ||||||
|  |                 formatter: ({ name }) => this._getDeviceName(name), | ||||||
|  |               } | ||||||
|  |             : undefined, | ||||||
|  |       } as BarSeriesOption | PieSeriesOption, | ||||||
|     ]; |     ]; | ||||||
|  |  | ||||||
|     if (compareData) { |     if (compareData) { | ||||||
|       datasets.push({ |       datasets.push({ | ||||||
|         type: "bar", |         type: this._chartType, | ||||||
|  |         radius: ["30%", "50%"], | ||||||
|  |         universalTransition: true, | ||||||
|         name: this.hass.localize( |         name: this.hass.localize( | ||||||
|           "ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage" |           "ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage" | ||||||
|         ), |         ), | ||||||
|         itemStyle: { |         itemStyle: { | ||||||
|           borderRadius: [0, 4, 4, 0], |           borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4, | ||||||
|         }, |         }, | ||||||
|         data: chartDataCompare, |         data: chartDataCompare, | ||||||
|         barWidth: 10, |         barWidth: 10, | ||||||
|         cursor: "default", |         cursor: "default", | ||||||
|       }); |         label: this._chartType === "pie" ? { show: false } : undefined, | ||||||
|  |         emphasis: | ||||||
|  |           this._chartType === "pie" | ||||||
|  |             ? { | ||||||
|  |                 focus: "series", | ||||||
|  |                 blurScope: "global", | ||||||
|  |               } | ||||||
|  |             : undefined, | ||||||
|  |       } as BarSeriesOption | PieSeriesOption); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const computedStyle = getComputedStyle(this); |     const computedStyle = getComputedStyle(this); | ||||||
|  |  | ||||||
|     const exclude = this._config?.hide_compound_stats |     this._compoundStats = energyData.prefs.device_consumption | ||||||
|       ? energyData.prefs.device_consumption |       .map((d) => d.included_in_stat) | ||||||
|           .map((d) => d.included_in_stat) |       .filter(Boolean) as string[]; | ||||||
|           .filter(Boolean) |  | ||||||
|       : []; |  | ||||||
|  |  | ||||||
|     energyData.prefs.device_consumption.forEach((device, id) => { |     const devices = energyData.prefs.device_consumption; | ||||||
|       if (exclude.includes(device.stat_consumption)) { |     const devicesTotals: Record<string, number> = {}; | ||||||
|         return; |     devices.forEach((device) => { | ||||||
|       } |       devicesTotals[device.stat_consumption] = | ||||||
|       const value = |  | ||||||
|         device.stat_consumption in data |         device.stat_consumption in data | ||||||
|           ? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0 |           ? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0 | ||||||
|           : 0; |           : 0; | ||||||
|       const color = getGraphColorByIndex(id, computedStyle); |     }); | ||||||
|  |     const devicesTotalsCompare: Record<string, number> = {}; | ||||||
|  |     if (compareData) { | ||||||
|  |       devices.forEach((device) => { | ||||||
|  |         devicesTotalsCompare[device.stat_consumption] = | ||||||
|  |           device.stat_consumption in compareData | ||||||
|  |             ? calculateStatisticSumGrowth( | ||||||
|  |                 compareData[device.stat_consumption] | ||||||
|  |               ) || 0 | ||||||
|  |             : 0; | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |     devices.forEach((device, idx) => { | ||||||
|  |       let value = devicesTotals[device.stat_consumption]; | ||||||
|  |       if (!this._config?.hide_compound_stats) { | ||||||
|  |         const childSum = devices.reduce((acc, d) => { | ||||||
|  |           if (d.included_in_stat === device.stat_consumption) { | ||||||
|  |             return acc + devicesTotals[d.stat_consumption]; | ||||||
|  |           } | ||||||
|  |           return acc; | ||||||
|  |         }, 0); | ||||||
|  |         value -= Math.min(value, childSum); | ||||||
|  |       } else if (this._compoundStats.includes(device.stat_consumption)) { | ||||||
|  |         return; | ||||||
|  |       } | ||||||
|  |       const color = getGraphColorByIndex(idx, computedStyle); | ||||||
|  |  | ||||||
|       chartData.push({ |       chartData.push({ | ||||||
|         id, |         id: device.stat_consumption, | ||||||
|         value: [value, device.stat_consumption], |         value: [value, device.stat_consumption] as any, | ||||||
|  |         name: device.stat_consumption, | ||||||
|         itemStyle: { |         itemStyle: { | ||||||
|           color: color + "7F", |           color: color + "7F", | ||||||
|           borderColor: color, |           borderColor: color, | ||||||
| @@ -230,16 +316,24 @@ export class HuiEnergyDevicesGraphCard | |||||||
|       }); |       }); | ||||||
|  |  | ||||||
|       if (compareData) { |       if (compareData) { | ||||||
|         const compareValue = |         let compareValue = | ||||||
|           device.stat_consumption in compareData |           device.stat_consumption in compareData | ||||||
|             ? calculateStatisticSumGrowth( |             ? calculateStatisticSumGrowth( | ||||||
|                 compareData[device.stat_consumption] |                 compareData[device.stat_consumption] | ||||||
|               ) || 0 |               ) || 0 | ||||||
|             : 0; |             : 0; | ||||||
|  |         const compareChildSum = devices.reduce((acc, d) => { | ||||||
|  |           if (d.included_in_stat === device.stat_consumption) { | ||||||
|  |             return acc + devicesTotalsCompare[d.stat_consumption]; | ||||||
|  |           } | ||||||
|  |           return acc; | ||||||
|  |         }, 0); | ||||||
|  |         compareValue -= Math.min(compareValue, compareChildSum); | ||||||
|  |  | ||||||
|         chartDataCompare.push({ |         chartDataCompare.push({ | ||||||
|           id, |           id: device.stat_consumption, | ||||||
|           value: [compareValue, device.stat_consumption], |           value: [compareValue, device.stat_consumption] as any, | ||||||
|  |           name: device.stat_consumption, | ||||||
|           itemStyle: { |           itemStyle: { | ||||||
|             color: color + "32", |             color: color + "32", | ||||||
|             borderColor: color + "7F", |             borderColor: color + "7F", | ||||||
| @@ -249,11 +343,62 @@ export class HuiEnergyDevicesGraphCard | |||||||
|     }); |     }); | ||||||
|  |  | ||||||
|     chartData.sort((a: any, b: any) => b.value[0] - a.value[0]); |     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; | ||||||
|  |     } | ||||||
|  |  | ||||||
|     chartData.length = Math.min( |     datasets.forEach((dataset) => { | ||||||
|       this._config?.max_devices || Infinity, |       dataset.data!.length = Math.min( | ||||||
|       chartData.length |         this._config?.max_devices || Infinity, | ||||||
|     ); |         dataset.data!.length | ||||||
|  |       ); | ||||||
|  |     }); | ||||||
|  |  | ||||||
|  |     if (this._chartType === "pie") { | ||||||
|  |       const { summedData } = getSummedData(energyData); | ||||||
|  |       const { consumption } = computeConsumptionData(summedData); | ||||||
|  |       const totalUsed = consumption.total.used_total; | ||||||
|  |       const showUntracked = | ||||||
|  |         "from_grid" in summedData || | ||||||
|  |         "solar" in summedData || | ||||||
|  |         "from_battery" in summedData; | ||||||
|  |       const untracked = showUntracked | ||||||
|  |         ? totalUsed - | ||||||
|  |           chartData.reduce((acc: number, d: any) => acc + d.value[0], 0) | ||||||
|  |         : 0; | ||||||
|  |       datasets.push({ | ||||||
|  |         type: "pie", | ||||||
|  |         radius: ["0%", compareData ? "30%" : "40%"], | ||||||
|  |         name: this.hass.localize( | ||||||
|  |           "ui.panel.lovelace.cards.energy.energy_devices_graph.total_energy_usage" | ||||||
|  |         ), | ||||||
|  |         data: [totalUsed], | ||||||
|  |         label: { | ||||||
|  |           show: true, | ||||||
|  |           position: "center", | ||||||
|  |           color: computedStyle.getPropertyValue("--secondary-text-color"), | ||||||
|  |           fontSize: computedStyle.getPropertyValue("--ha-font-size-l"), | ||||||
|  |           lineHeight: 24, | ||||||
|  |           fontWeight: "bold", | ||||||
|  |           formatter: `{a}\n${formatNumber(totalUsed, this.hass.locale)} kWh`, | ||||||
|  |         }, | ||||||
|  |         cursor: "default", | ||||||
|  |         itemStyle: { | ||||||
|  |           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) } | ||||||
|  |                 ) | ||||||
|  |               : "", | ||||||
|  |         }, | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|  |  | ||||||
|     this._chartData = datasets; |     this._chartData = datasets; | ||||||
|     await this.updateComplete; |     await this.updateComplete; | ||||||
| @@ -268,11 +413,26 @@ export class HuiEnergyDevicesGraphCard | |||||||
|       fireEvent(this, "hass-more-info", { |       fireEvent(this, "hass-more-info", { | ||||||
|         entityId: e.detail.value as string, |         entityId: e.detail.value as string, | ||||||
|       }); |       }); | ||||||
|  |     } else if ( | ||||||
|  |       e.detail.seriesType === "pie" && | ||||||
|  |       e.detail.event?.target?.type === "tspan" // label | ||||||
|  |     ) { | ||||||
|  |       fireEvent(this, "hass-more-info", { | ||||||
|  |         entityId: (e.detail.data as any).id as string, | ||||||
|  |       }); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _handleChartTypeChange(): void { | ||||||
|  |     this._chartType = this._chartType === "pie" ? "bar" : "pie"; | ||||||
|  |     this._getStatistics(this._data!); | ||||||
|  |   } | ||||||
|  |  | ||||||
|   static styles = css` |   static styles = css` | ||||||
|     .card-header { |     .card-header { | ||||||
|  |       display: flex; | ||||||
|  |       justify-content: space-between; | ||||||
|  |       align-items: center; | ||||||
|       padding-bottom: 0; |       padding-bottom: 0; | ||||||
|     } |     } | ||||||
|     .content { |     .content { | ||||||
| @@ -284,6 +444,11 @@ export class HuiEnergyDevicesGraphCard | |||||||
|     ha-chart-base { |     ha-chart-base { | ||||||
|       --chart-max-height: none; |       --chart-max-height: none; | ||||||
|     } |     } | ||||||
|  |     ha-icon-button { | ||||||
|  |       transform: rotate(90deg); | ||||||
|  |       color: var(--secondary-text-color); | ||||||
|  |       cursor: pointer; | ||||||
|  |     } | ||||||
|   `; |   `; | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -28,7 +28,7 @@ import { | |||||||
|   getCommonOptions, |   getCommonOptions, | ||||||
|   getCompareTransform, |   getCompareTransform, | ||||||
| } from "./common/energy-chart-options"; | } from "./common/energy-chart-options"; | ||||||
| import type { ECOption } from "../../../../resources/echarts"; | import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||||
| import "./common/hui-energy-graph-chip"; | import "./common/hui-energy-graph-chip"; | ||||||
| import "../../../../components/ha-tooltip"; | import "../../../../components/ha-tooltip"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -32,9 +32,9 @@ import { | |||||||
|   getCommonOptions, |   getCommonOptions, | ||||||
|   getCompareTransform, |   getCompareTransform, | ||||||
| } from "./common/energy-chart-options"; | } from "./common/energy-chart-options"; | ||||||
|  | import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||||
| import "./common/hui-energy-graph-chip"; | import "./common/hui-energy-graph-chip"; | ||||||
| import "../../../../components/ha-tooltip"; | import "../../../../components/ha-tooltip"; | ||||||
| import type { ECOption } from "../../../../resources/echarts"; |  | ||||||
|  |  | ||||||
| @customElement("hui-energy-solar-graph-card") | @customElement("hui-energy-solar-graph-card") | ||||||
| export class HuiEnergySolarGraphCard | export class HuiEnergySolarGraphCard | ||||||
|   | |||||||
| @@ -37,7 +37,7 @@ import { | |||||||
|   getCommonOptions, |   getCommonOptions, | ||||||
|   getCompareTransform, |   getCompareTransform, | ||||||
| } from "./common/energy-chart-options"; | } from "./common/energy-chart-options"; | ||||||
| import type { ECOption } from "../../../../resources/echarts"; | import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||||
|  |  | ||||||
| const colorPropertyMap = { | const colorPropertyMap = { | ||||||
|   to_grid: "--energy-grid-return-color", |   to_grid: "--energy-grid-return-color", | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ import { | |||||||
|   getCommonOptions, |   getCommonOptions, | ||||||
|   getCompareTransform, |   getCompareTransform, | ||||||
| } from "./common/energy-chart-options"; | } from "./common/energy-chart-options"; | ||||||
| import type { ECOption } from "../../../../resources/echarts"; | import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||||
| import { formatNumber } from "../../../../common/number/format_number"; | import { formatNumber } from "../../../../common/number/format_number"; | ||||||
| import "./common/hui-energy-graph-chip"; | import "./common/hui-energy-graph-chip"; | ||||||
| import "../../../../components/ha-tooltip"; | import "../../../../components/ha-tooltip"; | ||||||
|   | |||||||
| @@ -1,305 +0,0 @@ | |||||||
| import { endOfToday, isToday, startOfToday } from "date-fns"; |  | ||||||
| import type { HassConfig, UnsubscribeFunc } from "home-assistant-js-websocket"; |  | ||||||
| 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 memoizeOne from "memoize-one"; |  | ||||||
| import type { LineSeriesOption } from "echarts/charts"; |  | ||||||
| import { graphic } from "echarts"; |  | ||||||
| import "../../../../components/chart/ha-chart-base"; |  | ||||||
| import "../../../../components/ha-card"; |  | ||||||
| import type { EnergyData } from "../../../../data/energy"; |  | ||||||
| import { getEnergyDataCollection } from "../../../../data/energy"; |  | ||||||
| import type { StatisticValue } from "../../../../data/recorder"; |  | ||||||
| import type { FrontendLocaleData } from "../../../../data/translation"; |  | ||||||
| import { SubscribeMixin } from "../../../../mixins/subscribe-mixin"; |  | ||||||
| import type { HomeAssistant } from "../../../../types"; |  | ||||||
| import type { LovelaceCard } from "../../types"; |  | ||||||
| import type { PowerSourcesGraphCardConfig } from "../types"; |  | ||||||
| import { hasConfigChanged } from "../../common/has-changed"; |  | ||||||
| import { getCommonOptions, fillLineGaps } from "./common/energy-chart-options"; |  | ||||||
| import type { ECOption } from "../../../../resources/echarts"; |  | ||||||
| import { hex2rgb } from "../../../../common/color/convert-color"; |  | ||||||
|  |  | ||||||
| @customElement("hui-power-sources-graph-card") |  | ||||||
| export class HuiPowerSourcesGraphCard |  | ||||||
|   extends SubscribeMixin(LitElement) |  | ||||||
|   implements LovelaceCard |  | ||||||
| { |  | ||||||
|   @property({ attribute: false }) public hass!: HomeAssistant; |  | ||||||
|  |  | ||||||
|   @state() private _config?: PowerSourcesGraphCardConfig; |  | ||||||
|  |  | ||||||
|   @state() private _chartData: LineSeriesOption[] = []; |  | ||||||
|  |  | ||||||
|   @state() private _start = startOfToday(); |  | ||||||
|  |  | ||||||
|   @state() private _end = endOfToday(); |  | ||||||
|  |  | ||||||
|   @state() private _compareStart?: Date; |  | ||||||
|  |  | ||||||
|   @state() private _compareEnd?: Date; |  | ||||||
|  |  | ||||||
|   protected hassSubscribeRequiredHostProps = ["_config"]; |  | ||||||
|  |  | ||||||
|   public hassSubscribe(): UnsubscribeFunc[] { |  | ||||||
|     return [ |  | ||||||
|       getEnergyDataCollection(this.hass, { |  | ||||||
|         key: this._config?.collection_key, |  | ||||||
|       }).subscribe((data) => this._getStatistics(data)), |  | ||||||
|     ]; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public getCardSize(): Promise<number> | number { |  | ||||||
|     return 3; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   public setConfig(config: PowerSourcesGraphCardConfig): void { |  | ||||||
|     this._config = config; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected shouldUpdate(changedProps: PropertyValues): boolean { |  | ||||||
|     return ( |  | ||||||
|       hasConfigChanged(this, changedProps) || |  | ||||||
|       changedProps.size > 1 || |  | ||||||
|       !changedProps.has("hass") |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   protected render() { |  | ||||||
|     if (!this.hass || !this._config) { |  | ||||||
|       return nothing; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     return html` |  | ||||||
|       <ha-card> |  | ||||||
|         ${this._config.title |  | ||||||
|           ? html`<h1 class="card-header">${this._config.title}</h1>` |  | ||||||
|           : ""} |  | ||||||
|         <div |  | ||||||
|           class="content ${classMap({ |  | ||||||
|             "has-header": !!this._config.title, |  | ||||||
|           })}" |  | ||||||
|         > |  | ||||||
|           <ha-chart-base |  | ||||||
|             .hass=${this.hass} |  | ||||||
|             .data=${this._chartData} |  | ||||||
|             .options=${this._createOptions( |  | ||||||
|               this._start, |  | ||||||
|               this._end, |  | ||||||
|               this.hass.locale, |  | ||||||
|               this.hass.config, |  | ||||||
|               this._compareStart, |  | ||||||
|               this._compareEnd |  | ||||||
|             )} |  | ||||||
|           ></ha-chart-base> |  | ||||||
|           ${!this._chartData.some((dataset) => dataset.data!.length) |  | ||||||
|             ? html`<div class="no-data"> |  | ||||||
|                 ${isToday(this._start) |  | ||||||
|                   ? this.hass.localize("ui.panel.lovelace.cards.energy.no_data") |  | ||||||
|                   : this.hass.localize( |  | ||||||
|                       "ui.panel.lovelace.cards.energy.no_data_period" |  | ||||||
|                     )} |  | ||||||
|               </div>` |  | ||||||
|             : nothing} |  | ||||||
|         </div> |  | ||||||
|       </ha-card> |  | ||||||
|     `; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _createOptions = memoizeOne( |  | ||||||
|     ( |  | ||||||
|       start: Date, |  | ||||||
|       end: Date, |  | ||||||
|       locale: FrontendLocaleData, |  | ||||||
|       config: HassConfig, |  | ||||||
|       compareStart?: Date, |  | ||||||
|       compareEnd?: Date |  | ||||||
|     ): ECOption => |  | ||||||
|       getCommonOptions( |  | ||||||
|         start, |  | ||||||
|         end, |  | ||||||
|         locale, |  | ||||||
|         config, |  | ||||||
|         "kW", |  | ||||||
|         compareStart, |  | ||||||
|         compareEnd |  | ||||||
|       ) |  | ||||||
|   ); |  | ||||||
|  |  | ||||||
|   private async _getStatistics(energyData: EnergyData): Promise<void> { |  | ||||||
|     const datasets: LineSeriesOption[] = []; |  | ||||||
|  |  | ||||||
|     const statIds = { |  | ||||||
|       solar: { |  | ||||||
|         stats: [] as string[], |  | ||||||
|         color: "--energy-solar-color", |  | ||||||
|         name: this.hass.localize( |  | ||||||
|           "ui.panel.lovelace.cards.energy.power_graph.solar" |  | ||||||
|         ), |  | ||||||
|       }, |  | ||||||
|       grid: { |  | ||||||
|         stats: [] as string[], |  | ||||||
|         color: "--energy-grid-consumption-color", |  | ||||||
|         name: this.hass.localize( |  | ||||||
|           "ui.panel.lovelace.cards.energy.power_graph.grid" |  | ||||||
|         ), |  | ||||||
|       }, |  | ||||||
|       battery: { |  | ||||||
|         stats: [] as string[], |  | ||||||
|         color: "--energy-battery-out-color", |  | ||||||
|         name: this.hass.localize( |  | ||||||
|           "ui.panel.lovelace.cards.energy.power_graph.battery" |  | ||||||
|         ), |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     const computedStyles = getComputedStyle(this); |  | ||||||
|  |  | ||||||
|     for (const source of energyData.prefs.energy_sources) { |  | ||||||
|       if (source.type === "solar") { |  | ||||||
|         if (source.stat_power) { |  | ||||||
|           statIds.solar.stats.push(source.stat_power); |  | ||||||
|         } |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (source.type === "battery") { |  | ||||||
|         if (source.stat_power) { |  | ||||||
|           statIds.battery.stats.push(source.stat_power); |  | ||||||
|         } |  | ||||||
|         continue; |  | ||||||
|       } |  | ||||||
|  |  | ||||||
|       if (source.type === "grid" && source.power) { |  | ||||||
|         statIds.grid.stats.push(...source.power.map((p) => p.stat_power)); |  | ||||||
|       } |  | ||||||
|     } |  | ||||||
|     const commonSeriesOptions: LineSeriesOption = { |  | ||||||
|       type: "line", |  | ||||||
|       smooth: 0.4, |  | ||||||
|       smoothMonotone: "x", |  | ||||||
|       lineStyle: { |  | ||||||
|         width: 1, |  | ||||||
|       }, |  | ||||||
|     }; |  | ||||||
|  |  | ||||||
|     Object.keys(statIds).forEach((key, keyIndex) => { |  | ||||||
|       if (statIds[key].stats.length) { |  | ||||||
|         const colorHex = computedStyles.getPropertyValue(statIds[key].color); |  | ||||||
|         const rgb = hex2rgb(colorHex); |  | ||||||
|         const { positive, negative } = this._processData( |  | ||||||
|           statIds[key].stats.map((id: string) => energyData.stats[id] ?? []) |  | ||||||
|         ); |  | ||||||
|         datasets.push({ |  | ||||||
|           ...commonSeriesOptions, |  | ||||||
|           id: key, |  | ||||||
|           name: statIds[key].name, |  | ||||||
|           color: colorHex, |  | ||||||
|           stack: "positive", |  | ||||||
|           areaStyle: { |  | ||||||
|             color: new graphic.LinearGradient(0, 0, 0, 1, [ |  | ||||||
|               { |  | ||||||
|                 offset: 0, |  | ||||||
|                 color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`, |  | ||||||
|               }, |  | ||||||
|               { |  | ||||||
|                 offset: 1, |  | ||||||
|                 color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`, |  | ||||||
|               }, |  | ||||||
|             ]), |  | ||||||
|           }, |  | ||||||
|           data: positive, |  | ||||||
|           z: 3 - keyIndex, // draw in reverse order so 0 value lines are overwritten |  | ||||||
|         }); |  | ||||||
|         if (key !== "solar") { |  | ||||||
|           datasets.push({ |  | ||||||
|             ...commonSeriesOptions, |  | ||||||
|             id: `${key}-negative`, |  | ||||||
|             name: statIds[key].name, |  | ||||||
|             color: colorHex, |  | ||||||
|             stack: "negative", |  | ||||||
|             areaStyle: { |  | ||||||
|               color: new graphic.LinearGradient(0, 1, 0, 0, [ |  | ||||||
|                 { |  | ||||||
|                   offset: 0, |  | ||||||
|                   color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.75)`, |  | ||||||
|                 }, |  | ||||||
|                 { |  | ||||||
|                   offset: 1, |  | ||||||
|                   color: `rgba(${rgb[0]}, ${rgb[1]}, ${rgb[2]}, 0.25)`, |  | ||||||
|                 }, |  | ||||||
|               ]), |  | ||||||
|             }, |  | ||||||
|             data: negative, |  | ||||||
|             z: 4 - keyIndex, // draw in reverse order but above positive series |  | ||||||
|           }); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|     }); |  | ||||||
|  |  | ||||||
|     this._start = energyData.start; |  | ||||||
|     this._end = energyData.end || endOfToday(); |  | ||||||
|  |  | ||||||
|     this._chartData = fillLineGaps(datasets); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   private _processData(stats: StatisticValue[][]) { |  | ||||||
|     const data: Record<number, number[]> = {}; |  | ||||||
|     stats.forEach((statSet) => { |  | ||||||
|       statSet.forEach((point) => { |  | ||||||
|         if (point.mean == null) { |  | ||||||
|           return; |  | ||||||
|         } |  | ||||||
|         const x = (point.start + point.end) / 2; |  | ||||||
|         data[x] = [...(data[x] ?? []), point.mean]; |  | ||||||
|       }); |  | ||||||
|     }); |  | ||||||
|     const positive: [number, number][] = []; |  | ||||||
|     const negative: [number, number][] = []; |  | ||||||
|     Object.entries(data).forEach(([x, y]) => { |  | ||||||
|       const ts = Number(x); |  | ||||||
|       const meanY = y.reduce((a, b) => a + b, 0) / y.length; |  | ||||||
|       positive.push([ts, Math.max(0, meanY)]); |  | ||||||
|       negative.push([ts, Math.min(0, meanY)]); |  | ||||||
|     }); |  | ||||||
|     return { positive, negative }; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   static styles = css` |  | ||||||
|     ha-card { |  | ||||||
|       height: 100%; |  | ||||||
|     } |  | ||||||
|     .card-header { |  | ||||||
|       padding-bottom: 0; |  | ||||||
|     } |  | ||||||
|     .content { |  | ||||||
|       padding: var(--ha-space-4); |  | ||||||
|     } |  | ||||||
|     .has-header { |  | ||||||
|       padding-top: 0; |  | ||||||
|     } |  | ||||||
|     .no-data { |  | ||||||
|       position: absolute; |  | ||||||
|       height: 100%; |  | ||||||
|       top: 0; |  | ||||||
|       left: 0; |  | ||||||
|       right: 0; |  | ||||||
|       display: flex; |  | ||||||
|       justify-content: center; |  | ||||||
|       align-items: center; |  | ||||||
|       padding: 20%; |  | ||||||
|       margin-left: var(--ha-space-8); |  | ||||||
|       margin-inline-start: var(--ha-space-8); |  | ||||||
|       margin-inline-end: initial; |  | ||||||
|       box-sizing: border-box; |  | ||||||
|     } |  | ||||||
|   `; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| declare global { |  | ||||||
|   interface HTMLElementTagNameMap { |  | ||||||
|     "hui-power-sources-graph-card": HuiPowerSourcesGraphCard; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @@ -28,6 +28,7 @@ import { | |||||||
|   subscribeEntityRegistry, |   subscribeEntityRegistry, | ||||||
| } from "../../../data/entity_registry"; | } from "../../../data/entity_registry"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||||
| import type { LovelaceCard } from "../types"; | import type { LovelaceCard } from "../types"; | ||||||
| @@ -232,12 +233,16 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { | |||||||
|  |  | ||||||
|     const defaultCode = this._entry?.options?.alarm_control_panel?.default_code; |     const defaultCode = this._entry?.options?.alarm_control_panel?.default_code; | ||||||
|  |  | ||||||
|  |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-card> |       <ha-card> | ||||||
|         <h1 class="card-header"> |         <h1 class="card-header"> | ||||||
|           ${this._config.name || |           ${name} | ||||||
|           stateObj.attributes.friendly_name || |  | ||||||
|           stateLabel} |  | ||||||
|           <ha-assist-chip |           <ha-assist-chip | ||||||
|             filled |             filled | ||||||
|             style=${styleMap({ |             style=${styleMap({ | ||||||
|   | |||||||
| @@ -8,7 +8,6 @@ import { styleMap } from "lit/directives/style-map"; | |||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import { | import { | ||||||
|   stateColorBrightness, |   stateColorBrightness, | ||||||
|   stateColorCss, |   stateColorCss, | ||||||
| @@ -27,6 +26,7 @@ import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate"; | |||||||
| import { isUnavailableState } from "../../../data/entity"; | import { isUnavailableState } from "../../../data/entity"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { computeCardSize } from "../common/compute-card-size"; | import { computeCardSize } from "../common/compute-card-size"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||||
| @@ -125,7 +125,11 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { | |||||||
|       ? this._config.attribute in stateObj.attributes |       ? this._config.attribute in stateObj.attributes | ||||||
|       : !isUnavailableState(stateObj.state); |       : !isUnavailableState(stateObj.state); | ||||||
|  |  | ||||||
|     const name = this._config.name || computeStateName(stateObj); |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const colored = stateObj && this._getStateColor(stateObj, this._config); |     const colored = stateObj && this._getStateColor(stateObj, this._config); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -2,11 +2,10 @@ import type { HassEntity } from "home-assistant-js-websocket/dist/types"; | |||||||
| import type { PropertyValues } from "lit"; | import type { PropertyValues } from "lit"; | ||||||
| import { css, html, LitElement, nothing } from "lit"; | import { css, html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { ifDefined } from "lit/directives/if-defined"; |  | ||||||
| import { classMap } from "lit/directives/class-map"; | import { classMap } from "lit/directives/class-map"; | ||||||
|  | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { styleMap } from "lit/directives/style-map"; | import { styleMap } from "lit/directives/style-map"; | ||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import { isValidEntityId } from "../../../common/entity/valid_entity_id"; | import { isValidEntityId } from "../../../common/entity/valid_entity_id"; | ||||||
| import { getNumberFormatOptions } from "../../../common/number/format_number"; | import { getNumberFormatOptions } from "../../../common/number/format_number"; | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| @@ -15,6 +14,7 @@ import { UNAVAILABLE } from "../../../data/entity"; | |||||||
| import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | import { actionHandler } from "../common/directives/action-handler-directive"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { handleAction } from "../common/handle-action"; | import { handleAction } from "../common/handle-action"; | ||||||
| import { hasAction, hasAnyAction } from "../common/has-action"; | import { hasAction, hasAnyAction } from "../common/has-action"; | ||||||
| @@ -126,13 +126,19 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { | |||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const name = this._config.name ?? computeStateName(stateObj); |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     // Use `stateObj.state` as value to keep formatting (e.g trailing zeros) |     // Use `stateObj.state` as value to keep formatting (e.g trailing zeros) | ||||||
|     // for consistent value display across gauge, entity, entity-row, etc. |     // for consistent value display across gauge, entity, entity-row, etc. | ||||||
|     return html` |     return html` | ||||||
|       <ha-card |       <ha-card | ||||||
|         class=${classMap({ action: hasAnyAction(this._config) })} |         class=${classMap({ | ||||||
|  |           action: hasAnyAction(this._config), | ||||||
|  |         })} | ||||||
|         @action=${this._handleAction} |         @action=${this._handleAction} | ||||||
|         .actionHandler=${actionHandler({ |         .actionHandler=${actionHandler({ | ||||||
|           hasHold: hasAction(this._config.hold_action), |           hasHold: hasAction(this._config.hold_action), | ||||||
|   | |||||||
| @@ -33,7 +33,7 @@ import type { HomeSummaryCard } from "./types"; | |||||||
| const COLORS: Record<HomeSummary, string> = { | const COLORS: Record<HomeSummary, string> = { | ||||||
|   light: "amber", |   light: "amber", | ||||||
|   climate: "deep-orange", |   climate: "deep-orange", | ||||||
|   security: "blue-grey", |   safety: "blue-grey", | ||||||
|   media_players: "blue", |   media_players: "blue", | ||||||
| }; | }; | ||||||
|  |  | ||||||
| @@ -147,23 +147,20 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard { | |||||||
|           ? `${formattedMinTemp}°` |           ? `${formattedMinTemp}°` | ||||||
|           : `${formattedMinTemp} - ${formattedMaxTemp}°`; |           : `${formattedMinTemp} - ${formattedMaxTemp}°`; | ||||||
|       } |       } | ||||||
|       case "security": { |       case "safety": { | ||||||
|         // Alarm and lock status |         // Alarm and lock status | ||||||
|         const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) => |         const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) => | ||||||
|           generateEntityFilter(this.hass!, filter) |           generateEntityFilter(this.hass!, filter) | ||||||
|         ); |         ); | ||||||
|  |  | ||||||
|         const securityEntities = findEntities( |         const safetyEntities = findEntities(entitiesInsideArea, safetyFilters); | ||||||
|           entitiesInsideArea, |  | ||||||
|           securityFilters |  | ||||||
|         ); |  | ||||||
|  |  | ||||||
|         const locks = securityEntities.filter((entityId) => { |         const locks = safetyEntities.filter((entityId) => { | ||||||
|           const domain = computeDomain(entityId); |           const domain = computeDomain(entityId); | ||||||
|           return domain === "lock"; |           return domain === "lock"; | ||||||
|         }); |         }); | ||||||
|  |  | ||||||
|         const alarms = securityEntities.filter((entityId) => { |         const alarms = safetyEntities.filter((entityId) => { | ||||||
|           const domain = computeDomain(entityId); |           const domain = computeDomain(entityId); | ||||||
|           return domain === "alarm_control_panel"; |           return domain === "alarm_control_panel"; | ||||||
|         }); |         }); | ||||||
|   | |||||||
| @@ -6,7 +6,6 @@ import { customElement, property, state } from "lit/decorators"; | |||||||
| import { styleMap } from "lit/directives/style-map"; | import { styleMap } from "lit/directives/style-map"; | ||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import { stateColorCss } from "../../../common/entity/state_color"; | import { stateColorCss } from "../../../common/entity/state_color"; | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| import "../../../components/ha-icon-button"; | import "../../../components/ha-icon-button"; | ||||||
| @@ -15,6 +14,7 @@ import "../../../state-control/humidifier/ha-state-control-humidifier-humidity"; | |||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import "../card-features/hui-card-features"; | import "../card-features/hui-card-features"; | ||||||
| import type { LovelaceCardFeatureContext } from "../card-features/types"; | import type { LovelaceCardFeatureContext } from "../card-features/types"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||||
| import type { | import type { | ||||||
| @@ -133,7 +133,11 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { | |||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const name = this._config!.name || computeStateName(stateObj); |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const color = stateColorCss(stateObj); |     const color = stateColorCss(stateObj); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import { classMap } from "lit/directives/class-map"; | |||||||
| import { styleMap } from "lit/directives/style-map"; | import { styleMap } from "lit/directives/style-map"; | ||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import { stateColorBrightness } from "../../../common/entity/state_color"; | import { stateColorBrightness } from "../../../common/entity/state_color"; | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| import "../../../components/ha-icon-button"; | import "../../../components/ha-icon-button"; | ||||||
| @@ -18,6 +17,7 @@ import { lightSupportsBrightness } from "../../../data/light"; | |||||||
| import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | import { actionHandler } from "../common/directives/action-handler-directive"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { handleAction } from "../common/handle-action"; | import { handleAction } from "../common/handle-action"; | ||||||
| import { hasAction } from "../common/has-action"; | import { hasAction } from "../common/has-action"; | ||||||
| @@ -92,7 +92,11 @@ export class HuiLightCard extends LitElement implements LovelaceCard { | |||||||
|       ((stateObj.attributes.brightness || 0) / 255) * 100 |       ((stateObj.attributes.brightness || 0) / 255) * 100 | ||||||
|     ); |     ); | ||||||
|  |  | ||||||
|     const name = this._config.name ?? computeStateName(stateObj); |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-card> |       <ha-card> | ||||||
|   | |||||||
| @@ -12,7 +12,6 @@ import { classMap } from "lit/directives/class-map"; | |||||||
| import { styleMap } from "lit/directives/style-map"; | import { styleMap } from "lit/directives/style-map"; | ||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||||
| import { extractColors } from "../../../common/image/extract_color"; | import { extractColors } from "../../../common/image/extract_color"; | ||||||
| import { stateActive } from "../../../common/entity/state_active"; | import { stateActive } from "../../../common/entity/state_active"; | ||||||
| @@ -36,6 +35,7 @@ import { | |||||||
|   mediaPlayerPlayMedia, |   mediaPlayerPlayMedia, | ||||||
| } from "../../../data/media-player"; | } from "../../../data/media-player"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||||
| import "../components/hui-marquee"; | import "../components/hui-marquee"; | ||||||
| @@ -242,8 +242,11 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { | |||||||
|                 .hass=${this.hass} |                 .hass=${this.hass} | ||||||
|               ></ha-state-icon> |               ></ha-state-icon> | ||||||
|               <div> |               <div> | ||||||
|                 ${this._config!.name || |                 ${computeLovelaceEntityName( | ||||||
|                 computeStateName(this.hass!.states[this._config!.entity])} |                   this.hass, | ||||||
|  |                   this.hass!.states[this._config!.entity], | ||||||
|  |                   this._config.name | ||||||
|  |                 )} | ||||||
|               </div> |               </div> | ||||||
|             </div> |             </div> | ||||||
|             <div> |             <div> | ||||||
|   | |||||||
| @@ -126,7 +126,16 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard { | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let image: string | undefined = this._config.image; |     let image: string | undefined = | ||||||
|  |       (typeof this._config?.image === "object" && | ||||||
|  |         this._config.image.media_content_id) || | ||||||
|  |       (this._config.image as string | undefined); | ||||||
|  |  | ||||||
|  |     const darkModeImage: string | undefined = | ||||||
|  |       (typeof this._config?.dark_mode_image === "object" && | ||||||
|  |         this._config.dark_mode_image.media_content_id) || | ||||||
|  |       (this._config.dark_mode_image as string | undefined); | ||||||
|  |  | ||||||
|     if (this._config.image_entity) { |     if (this._config.image_entity) { | ||||||
|       const stateObj: ImageEntity | PersonEntity | undefined = |       const stateObj: ImageEntity | PersonEntity | undefined = | ||||||
|         this.hass.states[this._config.image_entity]; |         this.hass.states[this._config.image_entity]; | ||||||
| @@ -156,7 +165,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard { | |||||||
|             .entity=${this._config.entity} |             .entity=${this._config.entity} | ||||||
|             .aspectRatio=${this._config.aspect_ratio} |             .aspectRatio=${this._config.aspect_ratio} | ||||||
|             .darkModeFilter=${this._config.dark_mode_filter} |             .darkModeFilter=${this._config.dark_mode_filter} | ||||||
|             .darkModeImage=${this._config.dark_mode_image} |             .darkModeImage=${darkModeImage} | ||||||
|           ></hui-image> |           ></hui-image> | ||||||
|           ${this._elements} |           ${this._elements} | ||||||
|         </div> |         </div> | ||||||
|   | |||||||
| @@ -5,7 +5,6 @@ import { classMap } from "lit/directives/class-map"; | |||||||
| import { ifDefined } from "lit/directives/if-defined"; | import { ifDefined } from "lit/directives/if-defined"; | ||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | import { computeDomain } from "../../../common/entity/compute_domain"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| import type { CameraEntity } from "../../../data/camera"; | import type { CameraEntity } from "../../../data/camera"; | ||||||
| import type { ImageEntity } from "../../../data/image"; | import type { ImageEntity } from "../../../data/image"; | ||||||
| @@ -14,6 +13,7 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | |||||||
| import type { PersonEntity } from "../../../data/person"; | import type { PersonEntity } from "../../../data/person"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | import { actionHandler } from "../common/directives/action-handler-directive"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { handleAction } from "../common/handle-action"; | import { handleAction } from "../common/handle-action"; | ||||||
| import { hasAction } from "../common/has-action"; | import { hasAction } from "../common/has-action"; | ||||||
| @@ -126,7 +126,11 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard { | |||||||
|       `; |       `; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     const name = this._config.name || computeStateName(stateObj); |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|     const entityState = this.hass.formatEntityState(stateObj); |     const entityState = this.hass.formatEntityState(stateObj); | ||||||
|  |  | ||||||
|     let footer: TemplateResult | string = ""; |     let footer: TemplateResult | string = ""; | ||||||
| @@ -144,7 +148,10 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard { | |||||||
|     } |     } | ||||||
|  |  | ||||||
|     const domain: string = computeDomain(this._config.entity); |     const domain: string = computeDomain(this._config.entity); | ||||||
|     let image: string | undefined = this._config.image; |     let image: string | undefined = | ||||||
|  |       (typeof this._config?.image === "object" && | ||||||
|  |         this._config.image.media_content_id) || | ||||||
|  |       (this._config.image as string | undefined); | ||||||
|     if (!image) { |     if (!image) { | ||||||
|       switch (domain) { |       switch (domain) { | ||||||
|         case "image": |         case "image": | ||||||
|   | |||||||
| @@ -179,7 +179,10 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard { | |||||||
|       return nothing; |       return nothing; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     let image: string | undefined = this._config.image; |     let image: string | undefined = | ||||||
|  |       (typeof this._config?.image === "object" && | ||||||
|  |         this._config.image.media_content_id) || | ||||||
|  |       (this._config.image as string | undefined); | ||||||
|     if (this._config.image_entity) { |     if (this._config.image_entity) { | ||||||
|       const stateObj: ImageEntity | PersonEntity | undefined = |       const stateObj: ImageEntity | PersonEntity | undefined = | ||||||
|         this.hass.states[this._config.image_entity]; |         this.hass.states[this._config.image_entity]; | ||||||
|   | |||||||
| @@ -11,11 +11,11 @@ import { customElement, property, state } from "lit/decorators"; | |||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { batteryLevelIcon } from "../../../common/entity/battery_icon"; | import { batteryLevelIcon } from "../../../common/entity/battery_icon"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| import "../../../components/ha-svg-icon"; | import "../../../components/ha-svg-icon"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | import { actionHandler } from "../common/directives/action-handler-directive"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||||
| @@ -119,7 +119,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard { | |||||||
|           style="background-image:url(${stateObj.attributes.entity_picture})" |           style="background-image:url(${stateObj.attributes.entity_picture})" | ||||||
|         > |         > | ||||||
|           <div class="header"> |           <div class="header"> | ||||||
|             ${this._config.name || computeStateName(stateObj)} |             ${computeLovelaceEntityName(this.hass, stateObj, this._config.name)} | ||||||
|           </div> |           </div> | ||||||
|         </div> |         </div> | ||||||
|         <div class="content"> |         <div class="content"> | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import { styleMap } from "lit/directives/style-map"; | |||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { fireEvent } from "../../../common/dom/fire_event"; | import { fireEvent } from "../../../common/dom/fire_event"; | ||||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | import { computeDomain } from "../../../common/entity/compute_domain"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import { stateColorCss } from "../../../common/entity/state_color"; | import { stateColorCss } from "../../../common/entity/state_color"; | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| import "../../../components/ha-icon-button"; | import "../../../components/ha-icon-button"; | ||||||
| @@ -16,6 +15,7 @@ import "../../../state-control/water_heater/ha-state-control-water_heater-temper | |||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import "../card-features/hui-card-features"; | import "../card-features/hui-card-features"; | ||||||
| import type { LovelaceCardFeatureContext } from "../card-features/types"; | import type { LovelaceCardFeatureContext } from "../card-features/types"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||||
| import type { | import type { | ||||||
| @@ -132,7 +132,11 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { | |||||||
|     } |     } | ||||||
|     const domain = computeDomain(stateObj.entity_id); |     const domain = computeDomain(stateObj.entity_id); | ||||||
|  |  | ||||||
|     const name = this._config!.name || computeStateName(stateObj); |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     const color = stateColorCss(stateObj); |     const color = stateColorCss(stateObj); | ||||||
|  |  | ||||||
|   | |||||||
| @@ -9,7 +9,6 @@ import { computeCssColor } from "../../../common/color/compute-color"; | |||||||
| import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; | import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; | ||||||
| import { DOMAINS_TOGGLE } from "../../../common/const"; | import { DOMAINS_TOGGLE } from "../../../common/const"; | ||||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | import { computeDomain } from "../../../common/entity/compute_domain"; | ||||||
| import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display"; |  | ||||||
| import { stateActive } from "../../../common/entity/state_active"; | import { stateActive } from "../../../common/entity/state_active"; | ||||||
| import { stateColorCss } from "../../../common/entity/state_color"; | import { stateColorCss } from "../../../common/entity/state_color"; | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| @@ -26,6 +25,7 @@ import type { HomeAssistant } from "../../../types"; | |||||||
| import "../card-features/hui-card-features"; | import "../card-features/hui-card-features"; | ||||||
| import type { LovelaceCardFeatureContext } from "../card-features/types"; | import type { LovelaceCardFeatureContext } from "../card-features/types"; | ||||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | import { actionHandler } from "../common/directives/action-handler-directive"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { handleAction } from "../common/handle-action"; | import { handleAction } from "../common/handle-action"; | ||||||
| import { hasAction } from "../common/has-action"; | import { hasAction } from "../common/has-action"; | ||||||
| @@ -47,11 +47,6 @@ export const getEntityDefaultTileIconAction = (entityId: string) => { | |||||||
|   return supportsIconAction ? "toggle" : "none"; |   return supportsIconAction ? "toggle" : "none"; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const DEFAULT_NAME = [ |  | ||||||
|   { type: "device" }, |  | ||||||
|   { type: "entity" }, |  | ||||||
| ] satisfies EntityNameItem[]; |  | ||||||
|  |  | ||||||
| @customElement("hui-tile-card") | @customElement("hui-tile-card") | ||||||
| export class HuiTileCard extends LitElement implements LovelaceCard { | export class HuiTileCard extends LitElement implements LovelaceCard { | ||||||
|   public static async getConfigElement(): Promise<LovelaceCardEditor> { |   public static async getConfigElement(): Promise<LovelaceCardEditor> { | ||||||
| @@ -260,12 +255,11 @@ export class HuiTileCard extends LitElement implements LovelaceCard { | |||||||
|  |  | ||||||
|     const contentClasses = { vertical: Boolean(this._config.vertical) }; |     const contentClasses = { vertical: Boolean(this._config.vertical) }; | ||||||
|  |  | ||||||
|     const nameConfig = this._config.name; |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|     const nameDisplay = |       stateObj, | ||||||
|       typeof nameConfig === "string" |       this._config.name | ||||||
|         ? nameConfig |     ); | ||||||
|         : this.hass.formatEntityName(stateObj, nameConfig || DEFAULT_NAME); |  | ||||||
|  |  | ||||||
|     const active = stateActive(stateObj); |     const active = stateActive(stateObj); | ||||||
|     const color = this._computeStateColor(stateObj, this._config.color); |     const color = this._computeStateColor(stateObj, this._config.color); | ||||||
| @@ -278,7 +272,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { | |||||||
|             .stateObj=${stateObj} |             .stateObj=${stateObj} | ||||||
|             .hass=${this.hass} |             .hass=${this.hass} | ||||||
|             .content=${this._config.state_content} |             .content=${this._config.state_content} | ||||||
|             .name=${nameDisplay} |             .name=${name} | ||||||
|           > |           > | ||||||
|           </state-display> |           </state-display> | ||||||
|         `; |         `; | ||||||
| @@ -337,7 +331,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { | |||||||
|               ${renderTileBadge(stateObj, this.hass)} |               ${renderTileBadge(stateObj, this.hass)} | ||||||
|             </ha-tile-icon> |             </ha-tile-icon> | ||||||
|             <ha-tile-info id="info"> |             <ha-tile-info id="info"> | ||||||
|               <span slot="primary" class="primary">${nameDisplay}</span> |               <span slot="primary" class="primary">${name}</span> | ||||||
|               ${stateDisplay |               ${stateDisplay | ||||||
|                 ? html`<span slot="secondary">${stateDisplay}</span>` |                 ? html`<span slot="secondary">${stateDisplay}</span>` | ||||||
|                 : nothing} |                 : nothing} | ||||||
|   | |||||||
| @@ -7,7 +7,6 @@ import { classMap } from "lit/directives/class-map"; | |||||||
| import { formatDateWeekdayShort } from "../../../common/datetime/format_date"; | import { formatDateWeekdayShort } from "../../../common/datetime/format_date"; | ||||||
| import { formatTime } from "../../../common/datetime/format_time"; | import { formatTime } from "../../../common/datetime/format_time"; | ||||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; |  | ||||||
| import { isValidEntityId } from "../../../common/entity/valid_entity_id"; | import { isValidEntityId } from "../../../common/entity/valid_entity_id"; | ||||||
| import { formatNumber } from "../../../common/number/format_number"; | import { formatNumber } from "../../../common/number/format_number"; | ||||||
| import "../../../components/ha-card"; | import "../../../components/ha-card"; | ||||||
| @@ -27,6 +26,7 @@ import { | |||||||
| } from "../../../data/weather"; | } from "../../../data/weather"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | import { actionHandler } from "../common/directives/action-handler-directive"; | ||||||
|  | import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||||
| import { findEntities } from "../common/find-entities"; | import { findEntities } from "../common/find-entities"; | ||||||
| import { handleAction } from "../common/handle-action"; | import { handleAction } from "../common/handle-action"; | ||||||
| import { hasAction } from "../common/has-action"; | import { hasAction } from "../common/has-action"; | ||||||
| @@ -229,7 +229,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { | |||||||
|       return html` |       return html` | ||||||
|         <ha-card class="unavailable" @click=${this._handleAction}> |         <ha-card class="unavailable" @click=${this._handleAction}> | ||||||
|           ${this.hass.localize("ui.panel.lovelace.warning.entity_unavailable", { |           ${this.hass.localize("ui.panel.lovelace.warning.entity_unavailable", { | ||||||
|             entity: `${computeStateName(stateObj)} (${this._config.entity})`, |             entity: `${computeLovelaceEntityName(this.hass, stateObj, this._config.name)} (${this._config.entity})`, | ||||||
|           })} |           })} | ||||||
|         </ha-card> |         </ha-card> | ||||||
|       `; |       `; | ||||||
| @@ -260,7 +260,11 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { | |||||||
|     const dayNight = forecastData?.type === "twice_daily"; |     const dayNight = forecastData?.type === "twice_daily"; | ||||||
|  |  | ||||||
|     const weatherStateIcon = getWeatherStateIcon(stateObj.state, this); |     const weatherStateIcon = getWeatherStateIcon(stateObj.state, this); | ||||||
|     const name = this._config.name ?? computeStateName(stateObj); |     const name = computeLovelaceEntityName( | ||||||
|  |       this.hass, | ||||||
|  |       stateObj, | ||||||
|  |       this._config.name | ||||||
|  |     ); | ||||||
|  |  | ||||||
|     return html` |     return html` | ||||||
|       <ha-card |       <ha-card | ||||||
|   | |||||||
| @@ -40,7 +40,7 @@ export type AlarmPanelCardConfigState = | |||||||
|  |  | ||||||
| export interface AlarmPanelCardConfig extends LovelaceCardConfig { | export interface AlarmPanelCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   states?: AlarmPanelCardConfigState[]; |   states?: AlarmPanelCardConfigState[]; | ||||||
|   theme?: string; |   theme?: string; | ||||||
| } | } | ||||||
| @@ -63,6 +63,9 @@ export interface EmptyStateCardConfig extends LovelaceCardConfig { | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface EntityCardConfig extends LovelaceCardConfig { | export interface EntityCardConfig extends LovelaceCardConfig { | ||||||
|  |   entity: string; | ||||||
|  |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|  |   icon?: string; | ||||||
|   attribute?: string; |   attribute?: string; | ||||||
|   unit?: string; |   unit?: string; | ||||||
|   theme?: string; |   theme?: string; | ||||||
| @@ -227,11 +230,6 @@ export interface EnergySankeyCardConfig extends EnergyCardBaseConfig { | |||||||
|   group_by_area?: boolean; |   group_by_area?: boolean; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface PowerSourcesGraphCardConfig extends EnergyCardBaseConfig { |  | ||||||
|   type: "power-sources-graph"; |  | ||||||
|   title?: string; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export interface EntityFilterCardConfig extends LovelaceCardConfig { | export interface EntityFilterCardConfig extends LovelaceCardConfig { | ||||||
|   type: "entity-filter"; |   type: "entity-filter"; | ||||||
|   entities: (EntityFilterEntityConfig | string)[]; |   entities: (EntityFilterEntityConfig | string)[]; | ||||||
| @@ -263,7 +261,7 @@ export interface GaugeSegment { | |||||||
| export interface GaugeCardConfig extends LovelaceCardConfig { | export interface GaugeCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   attribute?: string; |   attribute?: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   unit?: string; |   unit?: string; | ||||||
|   min?: number; |   min?: number; | ||||||
|   max?: number; |   max?: number; | ||||||
| @@ -276,12 +274,14 @@ export interface GaugeCardConfig extends LovelaceCardConfig { | |||||||
|   double_tap_action?: ActionConfig; |   double_tap_action?: ActionConfig; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface ConfigEntity extends EntityConfig { | export interface ActionsConfig { | ||||||
|   tap_action?: ActionConfig; |   tap_action?: ActionConfig; | ||||||
|   hold_action?: ActionConfig; |   hold_action?: ActionConfig; | ||||||
|   double_tap_action?: ActionConfig; |   double_tap_action?: ActionConfig; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | export interface ConfigEntity extends EntityConfig, ActionsConfig {} | ||||||
|  |  | ||||||
| export interface PictureGlanceEntityConfig extends ConfigEntity { | export interface PictureGlanceEntityConfig extends ConfigEntity { | ||||||
|   show_state?: boolean; |   show_state?: boolean; | ||||||
|   attribute?: string; |   attribute?: string; | ||||||
| @@ -311,7 +311,7 @@ export interface GlanceCardConfig extends LovelaceCardConfig { | |||||||
| export interface HumidifierCardConfig extends LovelaceCardConfig { | export interface HumidifierCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   theme?: string; |   theme?: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   show_current_as_primary?: boolean; |   show_current_as_primary?: boolean; | ||||||
|   features?: LovelaceCardFeatureConfig[]; |   features?: LovelaceCardFeatureConfig[]; | ||||||
| } | } | ||||||
| @@ -327,7 +327,7 @@ export interface IframeCardConfig extends LovelaceCardConfig { | |||||||
|  |  | ||||||
| export interface LightCardConfig extends LovelaceCardConfig { | export interface LightCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   theme?: string; |   theme?: string; | ||||||
|   icon?: string; |   icon?: string; | ||||||
|   tap_action?: ActionConfig; |   tap_action?: ActionConfig; | ||||||
| @@ -399,6 +399,7 @@ export interface ClockCardConfig extends LovelaceCardConfig { | |||||||
|  |  | ||||||
| export interface MediaControlCardConfig extends LovelaceCardConfig { | export interface MediaControlCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|  |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   theme?: string; |   theme?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -458,7 +459,7 @@ export interface PictureCardConfig extends LovelaceCardConfig { | |||||||
|  |  | ||||||
| export interface PictureElementsCardConfig extends LovelaceCardConfig { | export interface PictureElementsCardConfig extends LovelaceCardConfig { | ||||||
|   title?: string; |   title?: string; | ||||||
|   image?: string; |   image?: string | MediaSelectorValue; | ||||||
|   image_entity?: string; |   image_entity?: string; | ||||||
|   camera_image?: string; |   camera_image?: string; | ||||||
|   camera_view?: HuiImage["cameraView"]; |   camera_view?: HuiImage["cameraView"]; | ||||||
| @@ -468,14 +469,14 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig { | |||||||
|   entity?: string; |   entity?: string; | ||||||
|   elements: LovelaceElementConfig[]; |   elements: LovelaceElementConfig[]; | ||||||
|   theme?: string; |   theme?: string; | ||||||
|   dark_mode_image?: string; |   dark_mode_image?: string | MediaSelectorValue; | ||||||
|   dark_mode_filter?: string; |   dark_mode_filter?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface PictureEntityCardConfig extends LovelaceCardConfig { | export interface PictureEntityCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   image?: string; |   image?: string | MediaSelectorValue; | ||||||
|   camera_image?: string; |   camera_image?: string; | ||||||
|   camera_view?: HuiImage["cameraView"]; |   camera_view?: HuiImage["cameraView"]; | ||||||
|   state_image?: Record<string, unknown>; |   state_image?: Record<string, unknown>; | ||||||
| @@ -493,7 +494,7 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig { | |||||||
| export interface PictureGlanceCardConfig extends LovelaceCardConfig { | export interface PictureGlanceCardConfig extends LovelaceCardConfig { | ||||||
|   entities: (string | PictureGlanceEntityConfig)[]; |   entities: (string | PictureGlanceEntityConfig)[]; | ||||||
|   title?: string; |   title?: string; | ||||||
|   image?: string; |   image?: string | MediaSelectorValue; | ||||||
|   image_entity?: string; |   image_entity?: string; | ||||||
|   camera_image?: string; |   camera_image?: string; | ||||||
|   camera_view?: HuiImage["cameraView"]; |   camera_view?: HuiImage["cameraView"]; | ||||||
| @@ -514,14 +515,14 @@ export interface PlantAttributeTarget extends EventTarget { | |||||||
| } | } | ||||||
|  |  | ||||||
| export interface PlantStatusCardConfig extends LovelaceCardConfig { | export interface PlantStatusCardConfig extends LovelaceCardConfig { | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   entity: string; |   entity: string; | ||||||
|   theme?: string; |   theme?: string; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface SensorCardConfig extends LovelaceCardConfig { | export interface SensorCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   icon?: string; |   icon?: string; | ||||||
|   graph?: string; |   graph?: string; | ||||||
|   unit?: string; |   unit?: string; | ||||||
| @@ -557,14 +558,14 @@ export interface GridCardConfig extends StackCardConfig { | |||||||
| export interface ThermostatCardConfig extends LovelaceCardConfig { | export interface ThermostatCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   theme?: string; |   theme?: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   show_current_as_primary?: boolean; |   show_current_as_primary?: boolean; | ||||||
|   features?: LovelaceCardFeatureConfig[]; |   features?: LovelaceCardFeatureConfig[]; | ||||||
| } | } | ||||||
|  |  | ||||||
| export interface WeatherForecastCardConfig extends LovelaceCardConfig { | export interface WeatherForecastCardConfig extends LovelaceCardConfig { | ||||||
|   entity: string; |   entity: string; | ||||||
|   name?: string; |   name?: string | EntityNameItem | EntityNameItem[]; | ||||||
|   show_current?: boolean; |   show_current?: boolean; | ||||||
|   show_forecast?: boolean; |   show_forecast?: boolean; | ||||||
|   forecast_type?: ForecastType; |   forecast_type?: ForecastType; | ||||||
|   | |||||||
| @@ -0,0 +1,23 @@ | |||||||
|  | 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"; | ||||||
|  |  | ||||||
|  | /** | ||||||
|  |  * 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) | ||||||
|  |  * @returns The computed entity name | ||||||
|  |  */ | ||||||
|  | export const computeLovelaceEntityName = ( | ||||||
|  |   hass: HomeAssistant, | ||||||
|  |   stateObj: HassEntity, | ||||||
|  |   nameConfig: string | EntityNameItem | EntityNameItem[] | undefined | ||||||
|  | ): string => | ||||||
|  |   typeof nameConfig === "string" | ||||||
|  |     ? nameConfig | ||||||
|  |     : hass.formatEntityName(stateObj, nameConfig || DEFAULT_ENTITY_NAME); | ||||||
| @@ -1,134 +1,85 @@ | |||||||
| import { strokeWidth } from "../../../../data/graph"; | import { downSampleLineData } from "../../../../components/chart/down-sample"; | ||||||
| import type { EntityHistoryState } from "../../../../data/history"; | import type { EntityHistoryState } from "../../../../data/history"; | ||||||
|  |  | ||||||
| const average = (items: any[]): number => |  | ||||||
|   items.reduce((sum, entry) => sum + parseFloat(entry.state), 0) / items.length; |  | ||||||
|  |  | ||||||
| const lastValue = (items: any[]): number => |  | ||||||
|   parseFloat(items[items.length - 1].state) || 0; |  | ||||||
|  |  | ||||||
| const calcPoints = ( | const calcPoints = ( | ||||||
|   history: any, |   history: [number, number][], | ||||||
|   hours: number, |  | ||||||
|   width: number, |   width: number, | ||||||
|   detail: number, |   height: number, | ||||||
|   min: number, |   limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } | ||||||
|   max: number | ) => { | ||||||
| ): [number, number][] => { |   let yAxisOrigin = height; | ||||||
|   const coords = [] as [number, number][]; |   let minY = limits?.minY ?? history[0][1]; | ||||||
|   const height = 80; |   let maxY = limits?.maxY ?? history[0][1]; | ||||||
|   let yRatio = (max - min) / height; |   const minX = limits?.minX ?? history[0][0]; | ||||||
|   yRatio = yRatio !== 0 ? yRatio : height; |   const maxX = limits?.maxX ?? history[history.length - 1][0]; | ||||||
|   let xRatio = width / (hours - (detail === 1 ? 1 : 0)); |   history.forEach(([_, stateValue]) => { | ||||||
|   xRatio = isFinite(xRatio) ? xRatio : width; |     if (stateValue < minY) { | ||||||
|  |       minY = stateValue; | ||||||
|   let first = history.filter(Boolean)[0]; |     } else if (stateValue > maxY) { | ||||||
|   if (detail > 1) { |       maxY = stateValue; | ||||||
|     first = first.filter(Boolean)[0]; |  | ||||||
|   } |  | ||||||
|   let last = [average(first), lastValue(first)]; |  | ||||||
|  |  | ||||||
|   const getY = (value: number): number => |  | ||||||
|     height + strokeWidth / 2 - (value - min) / yRatio; |  | ||||||
|  |  | ||||||
|   const getCoords = (item: any[], i: number, offset = 0, depth = 1) => { |  | ||||||
|     if (depth > 1 && item) { |  | ||||||
|       return item.forEach((subItem, index) => |  | ||||||
|         getCoords(subItem, i, index, depth - 1) |  | ||||||
|       ); |  | ||||||
|     } |     } | ||||||
|  |   }); | ||||||
|     const x = xRatio * (i + offset / 6); |   const rangeY = maxY - minY || minY * 0.1; | ||||||
|  |   if (maxY < 0) { | ||||||
|     if (item) { |     // all values are negative | ||||||
|       last = [average(item), lastValue(item)]; |     // add margin | ||||||
|     } |     maxY += rangeY * 0.1; | ||||||
|     const y = getY(item ? last[0] : last[1]); |     maxY = Math.min(0, maxY); | ||||||
|     return coords.push([x, y]); |     yAxisOrigin = 0; | ||||||
|   }; |   } else if (minY < 0) { | ||||||
|  |     // some values are negative | ||||||
|   for (let i = 0; i < history.length; i += 1) { |     yAxisOrigin = (maxY / (maxY - minY || 1)) * height; | ||||||
|     getCoords(history[i], i, 0, detail); |   } else { | ||||||
|  |     // all values are positive | ||||||
|  |     // add margin | ||||||
|  |     minY -= rangeY * 0.1; | ||||||
|  |     minY = Math.max(0, minY); | ||||||
|   } |   } | ||||||
|  |   const yDenom = maxY - minY || 1; | ||||||
|   coords.push([width, getY(last[1])]); |   const xDenom = maxX - minX || 1; | ||||||
|   return coords; |   const points: [number, number][] = history.map((point) => { | ||||||
|  |     const x = ((point[0] - minX) / xDenom) * width; | ||||||
|  |     const y = height - ((point[1] - minY) / yDenom) * height; | ||||||
|  |     return [x, y]; | ||||||
|  |   }); | ||||||
|  |   points.push([width, points[points.length - 1][1]]); | ||||||
|  |   return { points, yAxisOrigin }; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| export const coordinates = ( | export const coordinates = ( | ||||||
|   history: any, |   history: [number, number][], | ||||||
|   hours: number, |  | ||||||
|   width: number, |   width: number, | ||||||
|   detail: number, |   height: number, | ||||||
|   limits?: { min?: number; max?: number } |   maxDetails: number, | ||||||
| ): [number, number][] | undefined => { |   limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } | ||||||
|   history.forEach((item) => { | ) => { | ||||||
|     item.state = Number(item.state); |   history = history.filter((item) => !Number.isNaN(item[1])); | ||||||
|   }); |  | ||||||
|   history = history.filter((item) => !Number.isNaN(item.state)); |  | ||||||
|  |  | ||||||
|   const min = |   const sampledData: [number, number][] = downSampleLineData( | ||||||
|     limits?.min !== undefined |     history, | ||||||
|       ? limits.min |     maxDetails, | ||||||
|       : Math.min(...history.map((item) => item.state)); |     limits?.minX, | ||||||
|   const max = |     limits?.maxX | ||||||
|     limits?.max !== undefined |   ); | ||||||
|       ? limits.max |   return calcPoints(sampledData, width, height, limits); | ||||||
|       : Math.max(...history.map((item) => item.state)); |  | ||||||
|   const now = new Date().getTime(); |  | ||||||
|  |  | ||||||
|   const reduce = (res, item, point) => { |  | ||||||
|     const age = now - new Date(item.last_changed).getTime(); |  | ||||||
|  |  | ||||||
|     let key = Math.abs(age / (1000 * 3600) - hours); |  | ||||||
|     if (point) { |  | ||||||
|       key = (key - Math.floor(key)) * 60; |  | ||||||
|       key = Number((Math.round(key / 10) * 10).toString()[0]); |  | ||||||
|     } else { |  | ||||||
|       key = Math.floor(key); |  | ||||||
|     } |  | ||||||
|     if (!res[key]) { |  | ||||||
|       res[key] = []; |  | ||||||
|     } |  | ||||||
|     res[key].push(item); |  | ||||||
|     return res; |  | ||||||
|   }; |  | ||||||
|  |  | ||||||
|   history = history.reduce((res, item) => reduce(res, item, false), []); |  | ||||||
|   if (detail > 1) { |  | ||||||
|     history = history.map((entry) => |  | ||||||
|       entry.reduce((res, item) => reduce(res, item, true), []) |  | ||||||
|     ); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   if (!history.length) { |  | ||||||
|     return undefined; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   return calcPoints(history, hours, width, detail, min, max); |  | ||||||
| }; | }; | ||||||
|  |  | ||||||
| interface NumericEntityHistoryState { |  | ||||||
|   state: number; |  | ||||||
|   last_changed: number; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| export const coordinatesMinimalResponseCompressedState = ( | export const coordinatesMinimalResponseCompressedState = ( | ||||||
|   history: EntityHistoryState[], |   history: EntityHistoryState[] | undefined, | ||||||
|   hours: number, |  | ||||||
|   width: number, |   width: number, | ||||||
|   detail: number, |   height: number, | ||||||
|   limits?: { min?: number; max?: number } |   maxDetails: number, | ||||||
| ): [number, number][] | undefined => { |   limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } | ||||||
|   if (!history) { | ) => { | ||||||
|     return undefined; |   if (!history?.length) { | ||||||
|  |     return { points: [], yAxisOrigin: 0 }; | ||||||
|   } |   } | ||||||
|   const numericHistory: NumericEntityHistoryState[] = history.map((item) => ({ |   const mappedHistory: [number, number][] = history.map((item) => [ | ||||||
|     state: Number(item.s), |  | ||||||
|     // With minimal response and compressed state, we don't have last_changed, |     // With minimal response and compressed state, we don't have last_changed, | ||||||
|     // so we use last_updated since its always the same as last_changed since |     // so we use last_updated since its always the same as last_changed since | ||||||
|     // we already filtered out states that are the same. |     // we already filtered out states that are the same. | ||||||
|     last_changed: item.lu * 1000, |     item.lu * 1000, | ||||||
|   })); |     Number(item.s), | ||||||
|   return coordinates(numericHistory, hours, width, detail, limits); |   ]); | ||||||
|  |   return coordinates(mappedHistory, width, height, maxDetails, limits); | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -1,11 +1,11 @@ | |||||||
| import type { ActionConfig } from "../../../data/lovelace/config/action"; | import type { ActionConfig } from "../../../data/lovelace/config/action"; | ||||||
| import type { ConfigEntity } from "../cards/types"; | import type { ActionsConfig } from "../cards/types"; | ||||||
|  |  | ||||||
| export function hasAction(config?: ActionConfig): boolean { | export function hasAction(config?: ActionConfig): boolean { | ||||||
|   return config !== undefined && config.action !== "none"; |   return config !== undefined && config.action !== "none"; | ||||||
| } | } | ||||||
|  |  | ||||||
| export function hasAnyAction(config: ConfigEntity): boolean { | export function hasAnyAction(config: ActionsConfig): boolean { | ||||||
|   return ( |   return ( | ||||||
|     !config.tap_action || |     !config.tap_action || | ||||||
|     hasAction(config.tap_action) || |     hasAction(config.tap_action) || | ||||||
|   | |||||||
| @@ -6,12 +6,10 @@ import { fireEvent } from "../../../common/dom/fire_event"; | |||||||
| import { entityUseDeviceName } from "../../../common/entity/compute_entity_name"; | import { entityUseDeviceName } from "../../../common/entity/compute_entity_name"; | ||||||
| import { computeRTL } from "../../../common/util/compute_rtl"; | import { computeRTL } from "../../../common/util/compute_rtl"; | ||||||
| import "../../../components/entity/ha-entity-picker"; | import "../../../components/entity/ha-entity-picker"; | ||||||
| import type { | import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker"; | ||||||
|   HaEntityPicker, |  | ||||||
|   HaEntityPickerEntityFilterFunc, |  | ||||||
| } from "../../../components/entity/ha-entity-picker"; |  | ||||||
| import "../../../components/ha-icon-button"; | import "../../../components/ha-icon-button"; | ||||||
| import "../../../components/ha-sortable"; | import "../../../components/ha-sortable"; | ||||||
|  | import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
| import type { EntityConfig } from "../entity-rows/types"; | import type { EntityConfig } from "../entity-rows/types"; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -6,20 +6,26 @@ import { getPath } from "../common/graph/get-path"; | |||||||
|  |  | ||||||
| @customElement("hui-graph-base") | @customElement("hui-graph-base") | ||||||
| export class HuiGraphBase extends LitElement { | export class HuiGraphBase extends LitElement { | ||||||
|   @property() public coordinates?: any; |   @property({ attribute: false }) public coordinates?: number[][]; | ||||||
|  |  | ||||||
|  |   @property({ attribute: "y-axis-origin", type: Number }) | ||||||
|  |   public yAxisOrigin?: number; | ||||||
|  |  | ||||||
|   @state() private _path?: string; |   @state() private _path?: string; | ||||||
|  |  | ||||||
|   protected render(): TemplateResult { |   protected render(): TemplateResult { | ||||||
|  |     const width = this.clientWidth || 500; | ||||||
|  |     const height = this.clientHeight || width / 5; | ||||||
|  |     const yAxisOrigin = this.yAxisOrigin ?? height; | ||||||
|     return html` |     return html` | ||||||
|       ${this._path |       ${this._path | ||||||
|         ? svg`<svg width="100%" height="100%" viewBox="0 0 500 100" preserveAspectRatio="none"> |         ? svg`<svg width="100%" height="100%" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none"> | ||||||
|           <g> |           <g> | ||||||
|             <mask id="fill"> |             <mask id="fill"> | ||||||
|               <path |               <path | ||||||
|                 class='fill' |                 class='fill' | ||||||
|                 fill='white' |                 fill='white' | ||||||
|                 d="${this._path} L 500, 100 L 0, 100 z" |                 d="${this._path} L ${width}, ${yAxisOrigin} L 0, ${yAxisOrigin} z" | ||||||
|               /> |               /> | ||||||
|             </mask> |             </mask> | ||||||
|             <rect height="100%" width="100%" id="fill-rect" fill="var(--accent-color)" mask="url(#fill)"></rect> |             <rect height="100%" width="100%" id="fill-rect" fill="var(--accent-color)" mask="url(#fill)"></rect> | ||||||
| @@ -38,7 +44,7 @@ export class HuiGraphBase extends LitElement { | |||||||
|             <rect height="100%" width="100%" id="rect" fill="var(--accent-color)" mask="url(#line)"></rect> |             <rect height="100%" width="100%" id="rect" fill="var(--accent-color)" mask="url(#line)"></rect> | ||||||
|           </g> |           </g> | ||||||
|         </svg>` |         </svg>` | ||||||
|         : svg`<svg width="100%" height="100%" viewBox="0 0 500 100"></svg>`} |         : svg`<svg width="100%" height="100%" viewBox="0 0 ${width} ${height}"></svg>`} | ||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -15,6 +15,10 @@ import { UNAVAILABLE } from "../../../data/entity"; | |||||||
| import type { ImageEntity } from "../../../data/image"; | import type { ImageEntity } from "../../../data/image"; | ||||||
| import { computeImageUrl } from "../../../data/image"; | import { computeImageUrl } from "../../../data/image"; | ||||||
| import type { HomeAssistant } from "../../../types"; | import type { HomeAssistant } from "../../../types"; | ||||||
|  | import { | ||||||
|  |   isMediaSourceContentId, | ||||||
|  |   resolveMediaSource, | ||||||
|  | } from "../../../data/media_source"; | ||||||
|  |  | ||||||
| const UPDATE_INTERVAL = 10000; | const UPDATE_INTERVAL = 10000; | ||||||
| const DEFAULT_FILTER = "grayscale(100%)"; | const DEFAULT_FILTER = "grayscale(100%)"; | ||||||
| @@ -67,6 +71,12 @@ export class HuiImage extends LitElement { | |||||||
|  |  | ||||||
|   @state() private _loadedImageSrc?: string; |   @state() private _loadedImageSrc?: string; | ||||||
|  |  | ||||||
|  |   @state() private _resolvedImageSrc?: string; | ||||||
|  |  | ||||||
|  |   @state() private _resolvedDarkModeImageSrc?: string; | ||||||
|  |  | ||||||
|  |   @state() private _resolvedStateImages: Record<string, string> = {}; | ||||||
|  |  | ||||||
|   @state() private _lastImageHeight?: number; |   @state() private _lastImageHeight?: number; | ||||||
|  |  | ||||||
|   private _intersectionObserver?: IntersectionObserver; |   private _intersectionObserver?: IntersectionObserver; | ||||||
| @@ -130,6 +140,46 @@ export class HuiImage extends LitElement { | |||||||
|     if (this._loadState === LoadState.Loading && !this.cameraImage) { |     if (this._loadState === LoadState.Loading && !this.cameraImage) { | ||||||
|       this._loadState = LoadState.Loaded; |       this._loadState = LoadState.Loaded; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|  |     const firstHass = changedProps.has("hass") && !changedProps.get("hass"); | ||||||
|  |     if (this.hass && (changedProps.has("image") || firstHass)) { | ||||||
|  |       if (this.image && isMediaSourceContentId(this.image)) { | ||||||
|  |         resolveMediaSource(this.hass, this.image).then((result) => { | ||||||
|  |           this._resolvedImageSrc = result.url; | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         this._resolvedImageSrc = this.image; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (this.hass && (changedProps.has("darkModeImage") || firstHass)) { | ||||||
|  |       if (this.darkModeImage && isMediaSourceContentId(this.darkModeImage)) { | ||||||
|  |         resolveMediaSource(this.hass, this.darkModeImage).then((result) => { | ||||||
|  |           this._resolvedDarkModeImageSrc = result.url; | ||||||
|  |         }); | ||||||
|  |       } else { | ||||||
|  |         this._resolvedDarkModeImageSrc = this.darkModeImage; | ||||||
|  |       } | ||||||
|  |     } | ||||||
|  |     if (changedProps.has("stateImage") || firstHass) { | ||||||
|  |       this._resolvedStateImages = {}; | ||||||
|  |       Object.entries(this.stateImage || {}).forEach((entry) => { | ||||||
|  |         const key = entry[0] as string; | ||||||
|  |         const value = entry[1] as any; | ||||||
|  |         const image = | ||||||
|  |           (typeof value === "object" && value.media_content_id) || | ||||||
|  |           (value as string | undefined); | ||||||
|  |         if (isMediaSourceContentId(image)) { | ||||||
|  |           resolveMediaSource(this.hass!, image).then((result) => { | ||||||
|  |             this._resolvedStateImages = { | ||||||
|  |               ...this._resolvedStateImages, | ||||||
|  |               [key]: result.url, | ||||||
|  |             }; | ||||||
|  |           }); | ||||||
|  |         } else { | ||||||
|  |           this._resolvedStateImages![key] = image; | ||||||
|  |         } | ||||||
|  |       }); | ||||||
|  |     } | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   protected render() { |   protected render() { | ||||||
| @@ -155,20 +205,20 @@ export class HuiImage extends LitElement { | |||||||
|         imageSrc = this._cameraImageSrc; |         imageSrc = this._cameraImageSrc; | ||||||
|       } |       } | ||||||
|     } else if (this.stateImage) { |     } else if (this.stateImage) { | ||||||
|       const stateImage = this.stateImage[entityState]; |       const stateImage = this._resolvedStateImages[entityState]; | ||||||
|  |  | ||||||
|       if (stateImage) { |       if (stateImage) { | ||||||
|         imageSrc = stateImage; |         imageSrc = stateImage; | ||||||
|       } else { |       } else { | ||||||
|         imageSrc = this.image; |         imageSrc = this._resolvedImageSrc; | ||||||
|         imageFallback = true; |         imageFallback = true; | ||||||
|       } |       } | ||||||
|     } else if (this.darkModeImage && this.hass.themes.darkMode) { |     } else if (this.darkModeImage && this.hass.themes.darkMode) { | ||||||
|       imageSrc = this.darkModeImage; |       imageSrc = this._resolvedDarkModeImageSrc; | ||||||
|     } else if (stateObj && computeDomain(stateObj.entity_id) === "image") { |     } else if (stateObj && computeDomain(stateObj.entity_id) === "image") { | ||||||
|       imageSrc = computeImageUrl(stateObj as ImageEntity); |       imageSrc = computeImageUrl(stateObj as ImageEntity); | ||||||
|     } else { |     } else { | ||||||
|       imageSrc = this.image; |       imageSrc = this._resolvedImageSrc; | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     if (imageSrc) { |     if (imageSrc) { | ||||||
|   | |||||||
| @@ -66,8 +66,6 @@ const LAZY_LOAD_TYPES = { | |||||||
|   "energy-usage-graph": () => |   "energy-usage-graph": () => | ||||||
|     import("../cards/energy/hui-energy-usage-graph-card"), |     import("../cards/energy/hui-energy-usage-graph-card"), | ||||||
|   "energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"), |   "energy-sankey": () => import("../cards/energy/hui-energy-sankey-card"), | ||||||
|   "power-sources-graph": () => |  | ||||||
|     import("../cards/energy/hui-power-sources-graph-card"), |  | ||||||
|   "entity-filter": () => import("../cards/hui-entity-filter-card"), |   "entity-filter": () => import("../cards/hui-entity-filter-card"), | ||||||
|   error: () => import("../cards/hui-error-card"), |   error: () => import("../cards/hui-error-card"), | ||||||
|   "home-summary": () => import("../cards/hui-home-summary-card"), |   "home-summary": () => import("../cards/hui-home-summary-card"), | ||||||
|   | |||||||
| @@ -2,7 +2,15 @@ import memoizeOne from "memoize-one"; | |||||||
| import { mdiGestureTap } from "@mdi/js"; | import { mdiGestureTap } from "@mdi/js"; | ||||||
| import { html, LitElement, nothing } from "lit"; | import { html, LitElement, nothing } from "lit"; | ||||||
| import { customElement, property, state } from "lit/decorators"; | import { customElement, property, state } from "lit/decorators"; | ||||||
| import { any, assert, literal, object, optional, string } from "superstruct"; | import { | ||||||
|  |   any, | ||||||
|  |   assert, | ||||||
|  |   literal, | ||||||
|  |   object, | ||||||
|  |   optional, | ||||||
|  |   string, | ||||||
|  |   union, | ||||||
|  | } from "superstruct"; | ||||||
| import type { LocalizeFunc } from "../../../../../common/translations/localize"; | import type { LocalizeFunc } from "../../../../../common/translations/localize"; | ||||||
| import { fireEvent } from "../../../../../common/dom/fire_event"; | import { fireEvent } from "../../../../../common/dom/fire_event"; | ||||||
| import "../../../../../components/ha-form/ha-form"; | import "../../../../../components/ha-form/ha-form"; | ||||||
| @@ -15,7 +23,7 @@ import { actionConfigStruct } from "../../structs/action-struct"; | |||||||
| const imageElementConfigStruct = object({ | const imageElementConfigStruct = object({ | ||||||
|   type: literal("image"), |   type: literal("image"), | ||||||
|   entity: optional(string()), |   entity: optional(string()), | ||||||
|   image: optional(string()), |   image: optional(union([string(), object()])), | ||||||
|   style: optional(any()), |   style: optional(any()), | ||||||
|   title: optional(string()), |   title: optional(string()), | ||||||
|   tap_action: optional(actionConfigStruct), |   tap_action: optional(actionConfigStruct), | ||||||
| @@ -87,7 +95,20 @@ export class HuiImageElementEditor | |||||||
|             }, |             }, | ||||||
|           ], |           ], | ||||||
|         }, |         }, | ||||||
|         { name: "image", selector: { image: {} } }, |         { | ||||||
|  |           name: "image", | ||||||
|  |           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" | ||||||
|  |               ), | ||||||
|  |             }, | ||||||
|  |           }, | ||||||
|  |         }, | ||||||
|         { name: "camera_image", selector: { entity: { domain: "camera" } } }, |         { name: "camera_image", selector: { entity: { domain: "camera" } } }, | ||||||
|         { |         { | ||||||
|           name: "camera_view", |           name: "camera_view", | ||||||
| @@ -119,7 +140,7 @@ export class HuiImageElementEditor | |||||||
|     return html` |     return html` | ||||||
|       <ha-form |       <ha-form | ||||||
|         .hass=${this.hass} |         .hass=${this.hass} | ||||||
|         .data=${this._config} |         .data=${this._processData(this._config)} | ||||||
|         .schema=${this._schema(this.hass.localize)} |         .schema=${this._schema(this.hass.localize)} | ||||||
|         .computeLabel=${this._computeLabelCallback} |         .computeLabel=${this._computeLabelCallback} | ||||||
|         @value-changed=${this._valueChanged} |         @value-changed=${this._valueChanged} | ||||||
| @@ -127,6 +148,13 @@ export class HuiImageElementEditor | |||||||
|     `; |     `; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   private _processData = memoizeOne((config: ImageElementConfig) => ({ | ||||||
|  |     ...config, | ||||||
|  |     ...(typeof config.image === "string" | ||||||
|  |       ? { image: { media_content_id: config.image } } | ||||||
|  |       : {}), | ||||||
|  |   })); | ||||||
|  |  | ||||||
|   private _valueChanged(ev: CustomEvent): void { |   private _valueChanged(ev: CustomEvent): void { | ||||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); |     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||||
|   } |   } | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user