mirror of
				https://github.com/home-assistant/frontend.git
				synced 2025-10-25 03:29:41 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			copilot/fi
			...
			copilot/ad
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| ![copilot-swe-agent[bot]](/assets/img/avatar_default.png)  | 09aaa0d55f | ||
| ![copilot-swe-agent[bot]](/assets/img/avatar_default.png)  | baa049ea7a | ||
| ![copilot-swe-agent[bot]](/assets/img/avatar_default.png)  | e16037a712 | 
| @@ -46,9 +46,22 @@ class HassioIngressView extends LitElement { | ||||
|  | ||||
|   private _fetchDataTimeout?: number; | ||||
|  | ||||
|   private _messageListener = (ev: MessageEvent) => { | ||||
|     if (this._addon?.webui_ha_aware && ev.data?.type === "toggle-sidebar") { | ||||
|       this._toggleMenu(); | ||||
|     } | ||||
|   }; | ||||
|  | ||||
|   public connectedCallback() { | ||||
|     super.connectedCallback(); | ||||
|     window.addEventListener("message", this._messageListener); | ||||
|   } | ||||
|  | ||||
|   public disconnectedCallback() { | ||||
|     super.disconnectedCallback(); | ||||
|  | ||||
|     window.removeEventListener("message", this._messageListener); | ||||
|  | ||||
|     if (this._sessionKeepAlive) { | ||||
|       clearInterval(this._sessionKeepAlive); | ||||
|       this._sessionKeepAlive = undefined; | ||||
| @@ -83,17 +96,25 @@ class HassioIngressView extends LitElement { | ||||
|       </hass-subpage>`; | ||||
|     } | ||||
|  | ||||
|     return html`${this.narrow || this.hass.dockedSidebar === "always_hidden" | ||||
|       ? html`<div class="header"> | ||||
|             <ha-icon-button | ||||
|               .label=${this.hass.localize("ui.sidebar.sidebar_toggle")} | ||||
|               .path=${mdiMenu} | ||||
|               @click=${this._toggleMenu} | ||||
|             ></ha-icon-button> | ||||
|             <div class="main-title">${this._addon.name}</div> | ||||
|           </div> | ||||
|           ${iframe}` | ||||
|       : iframe}`; | ||||
|     // If webui_ha_aware is true, or if narrow or sidebar is always hidden, | ||||
|     // don't render the header and just render the iframe | ||||
|     if ( | ||||
|       this._addon.webui_ha_aware || | ||||
|       this.narrow || | ||||
|       this.hass.dockedSidebar === "always_hidden" | ||||
|     ) { | ||||
|       return iframe; | ||||
|     } | ||||
|  | ||||
|     return html`<div class="header"> | ||||
|         <ha-icon-button | ||||
|           .label=${this.hass.localize("ui.sidebar.sidebar_toggle")} | ||||
|           .path=${mdiMenu} | ||||
|           @click=${this._toggleMenu} | ||||
|         ></ha-icon-button> | ||||
|         <div class="main-title">${this._addon.name}</div> | ||||
|       </div> | ||||
|       ${iframe}`; | ||||
|   } | ||||
|  | ||||
|   protected async firstUpdated(): Promise<void> { | ||||
|   | ||||
| @@ -157,7 +157,7 @@ | ||||
|     "@octokit/auth-oauth-device": "8.0.2", | ||||
|     "@octokit/plugin-retry": "8.0.2", | ||||
|     "@octokit/rest": "22.0.0", | ||||
|     "@rsdoctor/rspack-plugin": "1.3.2", | ||||
|     "@rsdoctor/rspack-plugin": "1.3.1", | ||||
|     "@rspack/core": "1.5.8", | ||||
|     "@rspack/dev-server": "1.1.4", | ||||
|     "@types/babel__plugin-transform-runtime": "7.9.5", | ||||
| @@ -167,7 +167,7 @@ | ||||
|     "@types/culori": "4.0.1", | ||||
|     "@types/html-minifier-terser": "7.0.2", | ||||
|     "@types/js-yaml": "4.0.9", | ||||
|     "@types/leaflet": "1.9.21", | ||||
|     "@types/leaflet": "1.9.20", | ||||
|     "@types/leaflet-draw": "1.0.13", | ||||
|     "@types/leaflet.markercluster": "1.5.6", | ||||
|     "@types/lodash.merge": "4.6.9", | ||||
| @@ -203,7 +203,7 @@ | ||||
|     "husky": "9.1.7", | ||||
|     "jsdom": "27.0.0", | ||||
|     "jszip": "3.10.1", | ||||
|     "lint-staged": "16.2.4", | ||||
|     "lint-staged": "16.2.3", | ||||
|     "lit-analyzer": "2.0.3", | ||||
|     "lodash.merge": "4.6.2", | ||||
|     "lodash.template": "4.5.0", | ||||
|   | ||||
| @@ -9,11 +9,6 @@ import { getEntityContext } from "./context/get_entity_context"; | ||||
|  | ||||
| const DEFAULT_SEPARATOR = " "; | ||||
|  | ||||
| export const DEFAULT_ENTITY_NAME = [ | ||||
|   { type: "device" }, | ||||
|   { type: "entity" }, | ||||
| ] satisfies EntityNameItem[]; | ||||
|  | ||||
| export type EntityNameItem = | ||||
|   | { | ||||
|       type: "entity" | "device" | "area" | "floor"; | ||||
| @@ -29,14 +24,14 @@ export interface EntityNameOptions { | ||||
|  | ||||
| export const computeEntityNameDisplay = ( | ||||
|   stateObj: HassEntity, | ||||
|   name: EntityNameItem | EntityNameItem[] | undefined, | ||||
|   name: EntityNameItem | EntityNameItem[], | ||||
|   entities: HomeAssistant["entities"], | ||||
|   devices: HomeAssistant["devices"], | ||||
|   areas: HomeAssistant["areas"], | ||||
|   floors: HomeAssistant["floors"], | ||||
|   options?: EntityNameOptions | ||||
| ) => { | ||||
|   let items = ensureArray(name || DEFAULT_ENTITY_NAME); | ||||
|   let items = ensureArray(name); | ||||
|  | ||||
|   const separator = options?.separator ?? DEFAULT_SEPARATOR; | ||||
|  | ||||
|   | ||||
| @@ -8,10 +8,10 @@ interface AreaContext { | ||||
| } | ||||
| export const getAreaContext = ( | ||||
|   area: AreaRegistryEntry, | ||||
|   hassFloors: HomeAssistant["floors"] | ||||
|   hass: HomeAssistant | ||||
| ): AreaContext => { | ||||
|   const floorId = area.floor_id; | ||||
|   const floor = floorId ? hassFloors[floorId] : undefined; | ||||
|   const floor = floorId ? hass.floors[floorId] : undefined; | ||||
|  | ||||
|   return { | ||||
|     area: area, | ||||
|   | ||||
| @@ -1,22 +1,21 @@ | ||||
| import type { LineSeriesOption } from "echarts"; | ||||
|  | ||||
| export function downSampleLineData< | ||||
|   T extends [number, number] | NonNullable<LineSeriesOption["data"]>[number], | ||||
| >( | ||||
|   data: T[] | undefined, | ||||
|   maxDetails: number, | ||||
| export function downSampleLineData( | ||||
|   data: LineSeriesOption["data"], | ||||
|   chartWidth: number, | ||||
|   minX?: number, | ||||
|   maxX?: number | ||||
| ): T[] { | ||||
|   if (!data) { | ||||
|     return []; | ||||
| ) { | ||||
|   if (!data || data.length < 10) { | ||||
|     return data; | ||||
|   } | ||||
|   if (data.length <= maxDetails) { | ||||
|   const width = chartWidth * window.devicePixelRatio; | ||||
|   if (data.length <= width) { | ||||
|     return data; | ||||
|   } | ||||
|   const min = minX ?? getPointData(data[0]!)[0]; | ||||
|   const max = maxX ?? getPointData(data[data.length - 1]!)[0]; | ||||
|   const step = Math.ceil((max - min) / Math.floor(maxDetails)); | ||||
|   const step = Math.floor((max - min) / width); | ||||
|   const frames = new Map< | ||||
|     number, | ||||
|     { | ||||
| @@ -48,7 +47,7 @@ export function downSampleLineData< | ||||
|   } | ||||
|  | ||||
|   // Convert frames back to points | ||||
|   const result: T[] = []; | ||||
|   const result: typeof data = []; | ||||
|   for (const [_i, frame] of frames) { | ||||
|     // Use min/max points to preserve visual accuracy | ||||
|     // 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 { themesContext } from "../../data/context"; | ||||
| import type { Themes } from "../../data/ws-themes"; | ||||
| import type { ECOption } from "../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../resources/echarts"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { isMac } from "../../util/is_mac"; | ||||
| import "../chips/ha-assist-chip"; | ||||
| @@ -346,7 +346,7 @@ export class HaChartBase extends LitElement { | ||||
|       if (this.chart) { | ||||
|         this.chart.dispose(); | ||||
|       } | ||||
|       const echarts = (await import("../../resources/echarts/echarts")).default; | ||||
|       const echarts = (await import("../../resources/echarts")).default; | ||||
|  | ||||
|       if (this.extraComponents?.length) { | ||||
|         echarts.use(this.extraComponents); | ||||
| @@ -805,7 +805,7 @@ export class HaChartBase extends LitElement { | ||||
|             sampling: undefined, | ||||
|             data: downSampleLineData( | ||||
|               data as LineSeriesOption["data"], | ||||
|               this.clientWidth * window.devicePixelRatio, | ||||
|               this.clientWidth, | ||||
|               minX, | ||||
|               maxX | ||||
|             ), | ||||
|   | ||||
| @@ -6,7 +6,7 @@ import type { TopLevelFormatterParams } from "echarts/types/dist/shared"; | ||||
| import { mdiFormatTextVariant, mdiGoogleCirclesGroup } from "@mdi/js"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { listenMediaQuery } from "../../common/dom/media_query"; | ||||
| import type { ECOption } from "../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../resources/echarts"; | ||||
| import "./ha-chart-base"; | ||||
| import type { HaChartBase } from "./ha-chart-base"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
|   | ||||
| @@ -1,13 +1,13 @@ | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { LitElement, html, css } from "lit"; | ||||
| import type { EChartsType } from "echarts/core"; | ||||
| import type { CallbackDataParams } from "echarts/types/dist/shared"; | ||||
| import type { SankeySeriesOption } from "echarts/types/dist/echarts"; | ||||
| import type { CallbackDataParams } from "echarts/types/src/util/types"; | ||||
| import { SankeyChart } from "echarts/charts"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { ResizeController } from "@lit-labs/observers/resize-controller"; | ||||
| import SankeyChart from "../../resources/echarts/components/sankey/install"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import type { ECOption } from "../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../resources/echarts"; | ||||
| import { measureTextWidth } from "../../util/text"; | ||||
| import { filterXSS } from "../../common/util/xss"; | ||||
| import "./ha-chart-base"; | ||||
| @@ -39,7 +39,7 @@ type ProcessedLink = Link & { | ||||
|  | ||||
| const OVERFLOW_MARGIN = 5; | ||||
| const FONT_SIZE = 12; | ||||
| const NODE_GAP = 6; | ||||
| const NODE_GAP = 8; | ||||
| const LABEL_DISTANCE = 5; | ||||
|  | ||||
| @customElement("ha-sankey-chart") | ||||
| @@ -164,7 +164,6 @@ export class HaSankeyChart extends LitElement { | ||||
|       lineStyle: { | ||||
|         color: "gradient", | ||||
|         opacity: 0.4, | ||||
|         curveness: 0.5, | ||||
|       }, | ||||
|       layoutIterations: 0, | ||||
|       label: { | ||||
|   | ||||
| @@ -11,7 +11,7 @@ import { computeRTL } from "../../common/util/compute_rtl"; | ||||
| import type { LineChartEntity, LineChartState } from "../../data/history"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | ||||
| import type { ECOption } from "../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../resources/echarts"; | ||||
| import { formatDateTimeWithSeconds } from "../../common/datetime/format_date_time"; | ||||
| import { | ||||
|   getNumberFormatOptions, | ||||
|   | ||||
| @@ -15,8 +15,8 @@ import type { TimelineEntity } from "../../data/history"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { MIN_TIME_BETWEEN_UPDATES } from "./ha-chart-base"; | ||||
| import { computeTimelineColor } from "./timeline-color"; | ||||
| import type { ECOption } from "../../resources/echarts/echarts"; | ||||
| import echarts from "../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../resources/echarts"; | ||||
| import echarts from "../../resources/echarts"; | ||||
| import { luminosity } from "../../common/color/rgb"; | ||||
| import { hex2rgb } from "../../common/color/convert-color"; | ||||
| import { measureTextWidth } from "../../util/text"; | ||||
|   | ||||
| @@ -29,7 +29,7 @@ import { | ||||
|   getStatisticMetadata, | ||||
|   statisticsHaveType, | ||||
| } from "../../data/recorder"; | ||||
| import type { ECOption } from "../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../resources/echarts"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import type { CustomLegendOption } from "./ha-chart-base"; | ||||
| import "./ha-chart-base"; | ||||
|   | ||||
| @@ -5,18 +5,24 @@ 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 } from "../../common/entity/compute_device_name"; | ||||
| import { | ||||
|   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 { getConfigEntries, type ConfigEntry } from "../../data/config_entries"; | ||||
| import { | ||||
|   getDevices, | ||||
|   type DevicePickerItem, | ||||
|   getDeviceEntityDisplayLookup, | ||||
|   type DeviceEntityDisplayLookup, | ||||
|   type DeviceRegistryEntry, | ||||
| } from "../../data/device_registry"; | ||||
| import { domainToName } from "../../data/integration"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| import { brandsUrl } from "../../util/brands-url"; | ||||
| import "../ha-generic-picker"; | ||||
| import type { HaGenericPicker } from "../ha-generic-picker"; | ||||
| import type { PickerComboBoxItem } from "../ha-picker-combo-box"; | ||||
|  | ||||
| export type HaDevicePickerDeviceFilterFunc = ( | ||||
|   device: DeviceRegistryEntry | ||||
| @@ -24,6 +30,11 @@ export type HaDevicePickerDeviceFilterFunc = ( | ||||
|  | ||||
| export type HaDevicePickerEntityFilterFunc = (entity: HassEntity) => boolean; | ||||
|  | ||||
| interface DevicePickerItem extends PickerComboBoxItem { | ||||
|   domain?: string; | ||||
|   domain_name?: string; | ||||
| } | ||||
|  | ||||
| @customElement("ha-device-picker") | ||||
| export class HaDevicePicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -93,8 +104,6 @@ export class HaDevicePicker extends LitElement { | ||||
|  | ||||
|   @state() private _configEntryLookup: Record<string, ConfigEntry> = {}; | ||||
|  | ||||
|   private _getDevicesMemoized = memoizeOne(getDevices); | ||||
|  | ||||
|   protected firstUpdated(_changedProperties: PropertyValues): void { | ||||
|     super.firstUpdated(_changedProperties); | ||||
|     this._loadConfigEntries(); | ||||
| @@ -108,18 +117,162 @@ export class HaDevicePicker extends LitElement { | ||||
|   } | ||||
|  | ||||
|   private _getItems = () => | ||||
|     this._getDevicesMemoized( | ||||
|       this.hass, | ||||
|     this._getDevices( | ||||
|       this.hass.devices, | ||||
|       this.hass.entities, | ||||
|       this._configEntryLookup, | ||||
|       this.includeDomains, | ||||
|       this.excludeDomains, | ||||
|       this.includeDeviceClasses, | ||||
|       this.deviceFilter, | ||||
|       this.entityFilter, | ||||
|       this.excludeDevices, | ||||
|       this.value | ||||
|       this.excludeDevices | ||||
|     ); | ||||
|  | ||||
|   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( | ||||
|     (configEntriesLookup: Record<string, ConfigEntry>) => (value: string) => { | ||||
|       const deviceId = value; | ||||
|   | ||||
| @@ -7,7 +7,7 @@ import { isValidEntityId } from "../../common/entity/valid_entity_id"; | ||||
| import type { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||
| import "../ha-sortable"; | ||||
| import "./ha-entity-picker"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "./ha-entity-picker"; | ||||
|  | ||||
| @customElement("ha-entities-picker") | ||||
| class HaEntitiesPicker extends LitElement { | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import type { PropertyValues } from "lit"; | ||||
| import { LitElement, html, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| @@ -7,6 +8,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||
| import "../ha-combo-box"; | ||||
| import type { HaComboBox } from "../ha-combo-box"; | ||||
|  | ||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | ||||
|  | ||||
| interface AttributeOption { | ||||
|   value: string; | ||||
|   label: string; | ||||
|   | ||||
| @@ -25,7 +25,6 @@ import "../ha-sortable"; | ||||
| interface EntityNameOption { | ||||
|   primary: string; | ||||
|   secondary?: string; | ||||
|   field_label: string; | ||||
|   value: string; | ||||
| } | ||||
|  | ||||
| @@ -42,23 +41,6 @@ const KNOWN_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") | ||||
| export class HaEntityNamePicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -139,23 +121,13 @@ export class HaEntityNamePicker extends LitElement { | ||||
|       return { | ||||
|         primary, | ||||
|         secondary, | ||||
|         field_label: primary, | ||||
|         value: formatOptionValue({ type: name }), | ||||
|         value: name, | ||||
|       }; | ||||
|     }); | ||||
|  | ||||
|     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) => { | ||||
|     if (item.type === "text") { | ||||
|       return `"${item.text}"`; | ||||
| @@ -242,7 +214,7 @@ export class HaEntityNamePicker extends LitElement { | ||||
|             allow-custom-value | ||||
|             item-id-path="value" | ||||
|             item-value-path="value" | ||||
|             item-label-path="field_label" | ||||
|             item-label-path="primary" | ||||
|             .renderer=${rowRenderer} | ||||
|             @opened-changed=${this._openedChanged} | ||||
|             @value-changed=${this._comboBoxValueChanged} | ||||
| @@ -314,13 +286,14 @@ export class HaEntityNamePicker extends LitElement { | ||||
|       const initialItem = | ||||
|         this._editIndex != null ? this._value[this._editIndex] : undefined; | ||||
|  | ||||
|       const initialValue = initialItem ? formatOptionValue(initialItem) : ""; | ||||
|       const initialValue = initialItem | ||||
|         ? initialItem.type === "text" | ||||
|           ? initialItem.text | ||||
|           : initialItem.type | ||||
|         : ""; | ||||
|  | ||||
|       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.setInputValue(initialValue); | ||||
|     } else { | ||||
| @@ -353,7 +326,11 @@ export class HaEntityNamePicker extends LitElement { | ||||
|     const currentItem = | ||||
|       this._editIndex != null ? this._value[this._editIndex] : undefined; | ||||
|  | ||||
|     const currentValue = currentItem ? formatOptionValue(currentItem) : ""; | ||||
|     const currentValue = currentItem | ||||
|       ? currentItem.type === "text" | ||||
|         ? currentItem.text | ||||
|         : currentItem.type | ||||
|       : ""; | ||||
|  | ||||
|     this._comboBox.filteredItems = this._filterSelectedOptions( | ||||
|       options, | ||||
| @@ -375,7 +352,6 @@ export class HaEntityNamePicker extends LitElement { | ||||
|     const fuse = new Fuse(this._comboBox.filteredItems, fuseOptions); | ||||
|     const filteredItems = fuse.search(filter).map((result) => result.item); | ||||
|  | ||||
|     filteredItems.push(this._customNameOption(input)); | ||||
|     this._comboBox.filteredItems = filteredItems; | ||||
|   } | ||||
|  | ||||
| @@ -409,7 +385,9 @@ export class HaEntityNamePicker extends LitElement { | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     const item: EntityNameItem = parseOptionValue(value); | ||||
|     const item: EntityNameItem = KNOWN_TYPES.has(value as any) | ||||
|       ? { type: value as EntityNameType } | ||||
|       : { type: "text", text: value }; | ||||
|  | ||||
|     const newValue = [...this._value]; | ||||
|  | ||||
|   | ||||
| @@ -1,17 +1,15 @@ | ||||
| import { mdiPlus, mdiShape } from "@mdi/js"; | ||||
| 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 { customElement, property, query } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| 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 { isValidEntityId } from "../../common/entity/valid_entity_id"; | ||||
| 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 { | ||||
|   isHelperDomain, | ||||
| @@ -22,11 +20,21 @@ import type { HomeAssistant } from "../../types"; | ||||
| import "../ha-combo-box-item"; | ||||
| import "../ha-generic-picker"; | ||||
| import type { HaGenericPicker } from "../ha-generic-picker"; | ||||
| import type { PickerComboBoxSearchFn } from "../ha-picker-combo-box"; | ||||
| import type { | ||||
|   PickerComboBoxItem, | ||||
|   PickerComboBoxSearchFn, | ||||
| } from "../ha-picker-combo-box"; | ||||
| import type { PickerValueRenderer } from "../ha-picker-field"; | ||||
| import "../ha-svg-icon"; | ||||
| import "./state-badge"; | ||||
|  | ||||
| interface EntityComboBoxItem extends PickerComboBoxItem { | ||||
|   domain_name?: string; | ||||
|   stateObj?: HassEntity; | ||||
| } | ||||
|  | ||||
| export type HaEntityPickerEntityFilterFunc = (entity: HassEntity) => boolean; | ||||
|  | ||||
| const CREATE_ID = "___create-new-entity___"; | ||||
|  | ||||
| @customElement("ha-entity-picker") | ||||
| @@ -247,10 +255,8 @@ export class HaEntityPicker extends LitElement { | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|   private _getEntitiesMemoized = memoizeOne(getEntities); | ||||
|  | ||||
|   private _getItems = () => | ||||
|     this._getEntitiesMemoized( | ||||
|     this._getEntities( | ||||
|       this.hass, | ||||
|       this.includeDomains, | ||||
|       this.excludeDomains, | ||||
| @@ -258,10 +264,128 @@ export class HaEntityPicker extends LitElement { | ||||
|       this.includeDeviceClasses, | ||||
|       this.includeUnitOfMeasurement, | ||||
|       this.includeEntities, | ||||
|       this.excludeEntities, | ||||
|       this.value | ||||
|       this.excludeEntities | ||||
|     ); | ||||
|  | ||||
|   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() { | ||||
|     const placeholder = | ||||
|       this.placeholder ?? | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import type { PropertyValues } from "lit"; | ||||
| import { LitElement, html, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| @@ -8,6 +9,8 @@ import type { HomeAssistant, ValueChangedEvent } from "../../types"; | ||||
| import "../ha-combo-box"; | ||||
| import type { HaComboBox } from "../ha-combo-box"; | ||||
|  | ||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | ||||
|  | ||||
| interface StateOption { | ||||
|   value: string; | ||||
|   label: string; | ||||
|   | ||||
| @@ -8,13 +8,21 @@ import { styleMap } from "lit/directives/style-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { fireEvent } from "../common/dom/fire_event"; | ||||
| 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 { 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 { | ||||
|   getAreasAndFloors, | ||||
|   type AreaFloorValue, | ||||
|   type FloorComboBoxItem, | ||||
| } from "../data/area_floor"; | ||||
|   getFloorAreaLookup, | ||||
|   type FloorRegistryEntry, | ||||
| } from "../data/floor_registry"; | ||||
| import type { HomeAssistant, ValueChangedEvent } from "../types"; | ||||
| import type { HaDevicePickerDeviceFilterFunc } from "./device/ha-device-picker"; | ||||
| import "./ha-combo-box-item"; | ||||
| @@ -22,12 +30,24 @@ import "./ha-floor-icon"; | ||||
| import "./ha-generic-picker"; | ||||
| import type { HaGenericPicker } from "./ha-generic-picker"; | ||||
| import "./ha-icon-button"; | ||||
| import type { PickerComboBoxItem } from "./ha-picker-combo-box"; | ||||
| import type { PickerValueRenderer } from "./ha-picker-field"; | ||||
| import "./ha-svg-icon"; | ||||
| import "./ha-tree-indicator"; | ||||
|  | ||||
| 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") | ||||
| export class HaAreaFloorPicker extends LitElement { | ||||
|   @property({ attribute: false }) public hass!: HomeAssistant; | ||||
| @@ -134,6 +154,243 @@ 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> = ( | ||||
|     item, | ||||
|     { index }, | ||||
| @@ -188,16 +445,12 @@ export class HaAreaFloorPicker extends LitElement { | ||||
|     `; | ||||
|   }; | ||||
|  | ||||
|   private _getAreasAndFloorsMemoized = memoizeOne(getAreasAndFloors); | ||||
|  | ||||
|   private _getItems = () => | ||||
|     this._getAreasAndFloorsMemoized( | ||||
|       this.hass.states, | ||||
|     this._getAreasAndFloors( | ||||
|       this.hass.floors, | ||||
|       this.hass.areas, | ||||
|       this.hass.devices, | ||||
|       this.hass.entities, | ||||
|       this._formatValue, | ||||
|       this.includeDomains, | ||||
|       this.excludeDomains, | ||||
|       this.includeDeviceClasses, | ||||
|   | ||||
| @@ -107,7 +107,7 @@ export class HaAreaPicker extends LitElement { | ||||
|           `; | ||||
|         } | ||||
|  | ||||
|         const { floor } = getAreaContext(area, this.hass.floors); | ||||
|         const { floor } = getAreaContext(area, this.hass); | ||||
|  | ||||
|         const areaName = area ? computeAreaName(area) : undefined; | ||||
|         const floorName = floor ? computeFloorName(floor) : undefined; | ||||
| @@ -279,7 +279,7 @@ export class HaAreaPicker extends LitElement { | ||||
|       } | ||||
|  | ||||
|       const items = outputAreas.map<PickerComboBoxItem>((area) => { | ||||
|         const { floor } = getAreaContext(area, this.hass.floors); | ||||
|         const { floor } = getAreaContext(area, this.hass); | ||||
|         const floorName = floor ? computeFloorName(floor) : undefined; | ||||
|         const areaName = computeAreaName(area); | ||||
|         return { | ||||
|   | ||||
| @@ -44,7 +44,7 @@ export class HaAreasDisplayEditor extends LitElement { | ||||
|     ); | ||||
|  | ||||
|     const items: DisplayItem[] = areas.map((area) => { | ||||
|       const { floor } = getAreaContext(area, this.hass.floors); | ||||
|       const { floor } = getAreaContext(area, this.hass!); | ||||
|       return { | ||||
|         value: area.area_id, | ||||
|         label: area.name, | ||||
|   | ||||
| @@ -138,7 +138,7 @@ export class HaAreasFloorsDisplayEditor extends LitElement { | ||||
|       ); | ||||
|       const groupedItems: Record<string, DisplayItem[]> = areas.reduce( | ||||
|         (acc, area) => { | ||||
|           const { floor } = getAreaContext(area, this.hass.floors); | ||||
|           const { floor } = getAreaContext(area, this.hass!); | ||||
|           const floorId = floor?.floor_id ?? UNASSIGNED_FLOOR; | ||||
|  | ||||
|           if (!acc[floorId]) { | ||||
|   | ||||
| @@ -1,5 +1,5 @@ | ||||
| import "@home-assistant/webawesome/dist/components/drawer/drawer"; | ||||
| import { css, html, LitElement, type PropertyValues } from "lit"; | ||||
| import "@home-assistant/webawesome/dist/components/drawer/drawer"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
|  | ||||
| export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; | ||||
| @@ -8,9 +8,6 @@ export const BOTTOM_SHEET_ANIMATION_DURATION_MS = 300; | ||||
| export class HaBottomSheet extends LitElement { | ||||
|   @property({ type: Boolean }) public open = false; | ||||
|  | ||||
|   @property({ type: Boolean, reflect: true, attribute: "flexcontent" }) | ||||
|   public flexContent = false; | ||||
|  | ||||
|   @state() private _drawerOpen = false; | ||||
|  | ||||
|   private _handleAfterHide() { | ||||
| @@ -44,19 +41,16 @@ export class HaBottomSheet extends LitElement { | ||||
|  | ||||
|   static styles = css` | ||||
|     wa-drawer { | ||||
|       --wa-color-surface-raised: transparent; | ||||
|       --wa-color-surface-raised: var( | ||||
|         --ha-bottom-sheet-surface-background, | ||||
|         var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)), | ||||
|       ); | ||||
|       --spacing: 0; | ||||
|       --size: var(--ha-bottom-sheet-height, auto); | ||||
|       --size: auto; | ||||
|       --show-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms; | ||||
|       --hide-duration: ${BOTTOM_SHEET_ANIMATION_DURATION_MS}ms; | ||||
|     } | ||||
|     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( | ||||
|         --ha-bottom-sheet-border-radius, | ||||
|         var(--ha-dialog-border-radius, var(--ha-border-radius-2xl)) | ||||
| @@ -65,19 +59,10 @@ export class HaBottomSheet extends LitElement { | ||||
|         --ha-bottom-sheet-border-radius, | ||||
|         var(--ha-dialog-border-radius, var(--ha-border-radius-2xl)) | ||||
|       ); | ||||
|       background-color: var( | ||||
|         --ha-bottom-sheet-surface-background, | ||||
|         var(--ha-dialog-surface-background, var(--mdc-theme-surface, #fff)), | ||||
|       ); | ||||
|       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; | ||||
|       max-height: 90vh; | ||||
|       padding-bottom: var(--safe-area-inset-bottom); | ||||
|       padding-left: var(--safe-area-inset-left); | ||||
|       padding-right: var(--safe-area-inset-right); | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|   | ||||
| @@ -86,8 +86,7 @@ export class HaCameraStream extends LitElement { | ||||
|     const streams = this._streams( | ||||
|       this._capabilities?.frontend_stream_types, | ||||
|       this._hlsStreams, | ||||
|       this._webRtcStreams, | ||||
|       this.muted | ||||
|       this._webRtcStreams | ||||
|     ); | ||||
|     return html`${repeat( | ||||
|       streams, | ||||
| @@ -191,8 +190,7 @@ export class HaCameraStream extends LitElement { | ||||
|     ( | ||||
|       supportedTypes?: StreamType[], | ||||
|       hlsStreams?: { hasAudio: boolean; hasVideo: boolean }, | ||||
|       webRtcStreams?: { hasAudio: boolean; hasVideo: boolean }, | ||||
|       muted?: boolean | ||||
|       webRtcStreams?: { hasAudio: boolean; hasVideo: boolean } | ||||
|     ): Stream[] => { | ||||
|       if (__DEMO__) { | ||||
|         return [{ type: MJPEG_STREAM, visible: true }]; | ||||
| @@ -222,10 +220,9 @@ export class HaCameraStream extends LitElement { | ||||
|         if ( | ||||
|           hlsStreams.hasVideo && | ||||
|           hlsStreams.hasAudio && | ||||
|           !webRtcStreams.hasAudio && | ||||
|           !muted | ||||
|           !webRtcStreams.hasAudio | ||||
|         ) { | ||||
|           // webRTC stream is missing audio and audio is not muted, use HLS | ||||
|           // webRTC stream is missing audio, use HLS | ||||
|           return [{ type: STREAM_TYPE_HLS, visible: true }]; | ||||
|         } | ||||
|         if (webRtcStreams.hasVideo) { | ||||
|   | ||||
| @@ -49,7 +49,6 @@ export class HaExpansionPanel extends LitElement { | ||||
|           tabindex=${this.noCollapse ? -1 : 0} | ||||
|           aria-expanded=${this.expanded} | ||||
|           aria-controls="sect1" | ||||
|           part="summary" | ||||
|         > | ||||
|           ${this.leftChevron ? chevronIcon : nothing} | ||||
|           <slot name="leading-icon"></slot> | ||||
| @@ -171,11 +170,6 @@ export class HaExpansionPanel extends LitElement { | ||||
|       margin-left: 8px; | ||||
|       margin-inline-start: 8px; | ||||
|       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, | ||||
|   | ||||
| @@ -79,7 +79,6 @@ export class HaGenericPicker extends LitElement { | ||||
|         ${!this._opened | ||||
|           ? html` | ||||
|               <ha-picker-field | ||||
|                 id="picker" | ||||
|                 type="button" | ||||
|                 compact | ||||
|                 aria-label=${ifDefined(this.label)} | ||||
|   | ||||
| @@ -5,10 +5,16 @@ import { LitElement, html } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| 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 { | ||||
|   createLabelRegistryEntry, | ||||
|   getLabels, | ||||
|   subscribeLabelRegistry, | ||||
| } from "../data/label_registry"; | ||||
| import { showAlertDialog } from "../dialogs/generic/show-dialog-box"; | ||||
| @@ -131,22 +137,201 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { | ||||
|       } | ||||
|   ); | ||||
|  | ||||
|   private _getLabelsMemoized = memoizeOne(getLabels); | ||||
|   private _getLabels = memoizeOne( | ||||
|     ( | ||||
|       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, | ||||
|           }, | ||||
|         ]; | ||||
|       } | ||||
|  | ||||
|   private _getItems = () => { | ||||
|     if (!this._labels || this._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); | ||||
|       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.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; | ||||
|     } | ||||
|   ); | ||||
|  | ||||
|     return this._getLabelsMemoized( | ||||
|       this.hass, | ||||
|   private _getItems = () => | ||||
|     this._getLabels( | ||||
|       this._labels, | ||||
|       this.hass.areas, | ||||
|       this.hass.devices, | ||||
|       this.hass.entities, | ||||
|       this.includeDomains, | ||||
|       this.excludeDomains, | ||||
|       this.includeDeviceClasses, | ||||
| @@ -154,7 +339,6 @@ export class HaLabelPicker extends SubscribeMixin(LitElement) { | ||||
|       this.entityFilter, | ||||
|       this.excludeLabels | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   private _allLabelNames = memoizeOne((labels?: LabelRegistryEntry[]) => { | ||||
|     if (!labels) { | ||||
|   | ||||
| @@ -107,15 +107,14 @@ export class HaMediaSelector extends LitElement { | ||||
|         supportsFeature(stateObj, MediaPlayerEntityFeature.BROWSE_MEDIA)); | ||||
|  | ||||
|     if (this.selector.media?.image_upload && !this.value) { | ||||
|       return html`${this.label ? html`<label>${this.label}</label>` : nothing} | ||||
|         <ha-picture-upload | ||||
|           .hass=${this.hass} | ||||
|           .value=${null} | ||||
|           .contentIdHelper=${this.selector.media?.content_id_helper} | ||||
|           select-media | ||||
|           full-media | ||||
|           @media-picked=${this._pictureUploadMediaPicked} | ||||
|         ></ha-picture-upload>`; | ||||
|       return html`<ha-picture-upload | ||||
|         .hass=${this.hass} | ||||
|         .value=${null} | ||||
|         .contentIdHelper=${this.selector.media?.content_id_helper} | ||||
|         select-media | ||||
|         full-media | ||||
|         @media-picked=${this._pictureUploadMediaPicked} | ||||
|       ></ha-picture-upload>`; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
| @@ -142,7 +141,6 @@ export class HaMediaSelector extends LitElement { | ||||
|           `} | ||||
|       ${!supportsBrowse | ||||
|         ? html` | ||||
|             ${this.label ? html`<label>${this.label}</label>` : nothing} | ||||
|             <ha-alert> | ||||
|               ${this.hass.localize( | ||||
|                 "ui.components.selectors.media.browse_not_supported" | ||||
| @@ -156,8 +154,7 @@ export class HaMediaSelector extends LitElement { | ||||
|               .computeHelper=${this._computeHelperCallback} | ||||
|             ></ha-form> | ||||
|           ` | ||||
|         : html`${this.label ? html`<label>${this.label}</label>` : nothing} | ||||
|             <ha-card | ||||
|         : html`<ha-card | ||||
|               outlined | ||||
|               tabindex="0" | ||||
|               role="button" | ||||
|   | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -321,10 +321,6 @@ class HaWebRtcPlayer extends LitElement { | ||||
|     if (!this._remoteStream) { | ||||
|       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); | ||||
|     if (!this.hasUpdated) { | ||||
|       await this.updateComplete; | ||||
|   | ||||
| @@ -1,104 +0,0 @@ | ||||
| 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; | ||||
|   } | ||||
| } | ||||
| @@ -1,28 +0,0 @@ | ||||
| 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, | ||||
|   }); | ||||
| @@ -1,105 +0,0 @@ | ||||
| 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; | ||||
|   } | ||||
| } | ||||
| @@ -1,690 +0,0 @@ | ||||
| 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; | ||||
|   } | ||||
| } | ||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							| @@ -1,354 +0,0 @@ | ||||
| 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; | ||||
|   } | ||||
| } | ||||
| @@ -1,259 +0,0 @@ | ||||
| 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,7 +79,6 @@ export interface DataEntryFlowStepAbort { | ||||
|   reason: string; | ||||
|   description_placeholders?: Record<string, string>; | ||||
|   translation_domain?: string; | ||||
|   next_flow?: [FlowType, string]; // [flow_type, flow_id] | ||||
| } | ||||
|  | ||||
| export interface DataEntryFlowStepProgress { | ||||
|   | ||||
| @@ -1,20 +1,12 @@ | ||||
| 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 { getDeviceContext } from "../common/entity/context/get_device_context"; | ||||
| 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 { ConfigEntry } from "./config_entries"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "./entity"; | ||||
| import type { | ||||
|   EntityRegistryDisplayEntry, | ||||
|   EntityRegistryEntry, | ||||
| } from "./entity_registry"; | ||||
| import type { EntitySources } from "./entity_sources"; | ||||
| import { domainToName } from "./integration"; | ||||
| import type { RegistryEntry } from "./registry"; | ||||
|  | ||||
| export { | ||||
| @@ -171,147 +163,3 @@ export const getDeviceIntegrationLookup = ( | ||||
|   } | ||||
|   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; | ||||
| }; | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { arrayLiteralIncludes } from "../common/array/literal-includes"; | ||||
|  | ||||
| export const UNAVAILABLE = "unavailable"; | ||||
| @@ -11,5 +10,3 @@ export const OFF_STATES = [UNAVAILABLE, UNKNOWN, OFF] as const; | ||||
|  | ||||
| export const isUnavailableState = arrayLiteralIncludes(UNAVAILABLE_STATES); | ||||
| export const isOffState = arrayLiteralIncludes(OFF_STATES); | ||||
|  | ||||
| export type HaEntityPickerEntityFilterFunc = (entityId: HassEntity) => boolean; | ||||
|   | ||||
| @@ -1,17 +1,12 @@ | ||||
| import type { Connection, HassEntity } from "home-assistant-js-websocket"; | ||||
| import type { Connection } from "home-assistant-js-websocket"; | ||||
| import { createCollection } from "home-assistant-js-websocket"; | ||||
| import type { Store } from "home-assistant-js-websocket/dist/store"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| 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 { caseInsensitiveStringCompare } from "../common/string/compare"; | ||||
| import { computeRTL } from "../common/util/compute_rtl"; | ||||
| import { debounce } from "../common/util/debounce"; | ||||
| import type { PickerComboBoxItem } from "../components/ha-picker-combo-box"; | ||||
| import type { HomeAssistant } from "../types"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "./entity"; | ||||
| import { domainToName } from "./integration"; | ||||
| import type { LightColor } from "./light"; | ||||
| import type { RegistryEntry } from "./registry"; | ||||
|  | ||||
| @@ -329,122 +324,3 @@ export const getAutomaticEntityIds = ( | ||||
|     type: "config/entity_registry/get_automatic_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; | ||||
| }; | ||||
|   | ||||
| @@ -112,6 +112,7 @@ export interface HassioAddonDetails extends HassioAddonInfo { | ||||
|   translations: Record<string, AddonTranslations>; | ||||
|   watchdog: null | boolean; | ||||
|   webui: null | string; | ||||
|   webui_ha_aware?: boolean; | ||||
| } | ||||
|  | ||||
| export interface HassioAddonsInfo { | ||||
|   | ||||
| @@ -1,20 +1,9 @@ | ||||
| import { mdiLabel } from "@mdi/js"; | ||||
| import type { Connection } from "home-assistant-js-websocket"; | ||||
| import { createCollection } from "home-assistant-js-websocket"; | ||||
| import type { Store } from "home-assistant-js-websocket/dist/store"; | ||||
| import { computeDomain } from "../common/entity/compute_domain"; | ||||
| import { stringCompare } from "../common/string/compare"; | ||||
| 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 { | ||||
|   getDeviceEntityDisplayLookup, | ||||
|   type DeviceEntityDisplayLookup, | ||||
|   type DeviceRegistryEntry, | ||||
| } from "./device_registry"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "./entity"; | ||||
| import type { EntityRegistryDisplayEntry } from "./entity_registry"; | ||||
| import type { RegistryEntry } from "./registry"; | ||||
|  | ||||
| export interface LabelRegistryEntry extends RegistryEntry { | ||||
| @@ -99,178 +88,3 @@ export const deleteLabelRegistryEntry = ( | ||||
|     type: "config/label_registry/delete", | ||||
|     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; | ||||
| }; | ||||
|   | ||||
| @@ -1,164 +0,0 @@ | ||||
| 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,10 +472,7 @@ class DataEntryFlowDialog extends LitElement { | ||||
|     this._step = undefined; | ||||
|     await this.updateComplete; | ||||
|     this._step = _step; | ||||
|     if ( | ||||
|       (_step.type === "create_entry" || _step.type === "abort") && | ||||
|       _step.next_flow | ||||
|     ) { | ||||
|     if (_step.type === "create_entry" && _step.next_flow) { | ||||
|       // skip device rename if there is a chained flow | ||||
|       this._step = undefined; | ||||
|       this._handler = undefined; | ||||
| @@ -489,36 +486,32 @@ class DataEntryFlowDialog extends LitElement { | ||||
|           carryOverDevices: this._devices( | ||||
|             this._params!.flowConfig.showDevices, | ||||
|             Object.values(this.hass.devices), | ||||
|             _step.type === "create_entry" ? _step.result?.entry_id : undefined, | ||||
|             _step.result?.entry_id, | ||||
|             this._params!.carryOverDevices | ||||
|           ).map((device) => device.id), | ||||
|           dialogClosedCallback: this._params!.dialogClosedCallback, | ||||
|         }); | ||||
|       } else if (_step.next_flow[0] === "options_flow") { | ||||
|         if (_step.type === "create_entry") { | ||||
|           showOptionsFlowDialog( | ||||
|             this._params!.dialogParentElement!, | ||||
|             _step.result!, | ||||
|             { | ||||
|               continueFlowId: _step.next_flow[1], | ||||
|               navigateToResult: this._params!.navigateToResult, | ||||
|               dialogClosedCallback: this._params!.dialogClosedCallback, | ||||
|             } | ||||
|           ); | ||||
|         } | ||||
|         showOptionsFlowDialog( | ||||
|           this._params!.dialogParentElement!, | ||||
|           _step.result!, | ||||
|           { | ||||
|             continueFlowId: _step.next_flow[1], | ||||
|             navigateToResult: this._params!.navigateToResult, | ||||
|             dialogClosedCallback: this._params!.dialogClosedCallback, | ||||
|           } | ||||
|         ); | ||||
|       } else if (_step.next_flow[0] === "config_subentries_flow") { | ||||
|         if (_step.type === "create_entry") { | ||||
|           showSubConfigFlowDialog( | ||||
|             this._params!.dialogParentElement!, | ||||
|             _step.result!, | ||||
|             _step.next_flow[0], | ||||
|             { | ||||
|               continueFlowId: _step.next_flow[1], | ||||
|               navigateToResult: this._params!.navigateToResult, | ||||
|               dialogClosedCallback: this._params!.dialogClosedCallback, | ||||
|             } | ||||
|           ); | ||||
|         } | ||||
|         showSubConfigFlowDialog( | ||||
|           this._params!.dialogParentElement!, | ||||
|           _step.result!, | ||||
|           _step.next_flow[0], | ||||
|           { | ||||
|             continueFlowId: _step.next_flow[1], | ||||
|             navigateToResult: this._params!.navigateToResult, | ||||
|             dialogClosedCallback: this._params!.dialogClosedCallback, | ||||
|           } | ||||
|         ); | ||||
|       } else { | ||||
|         this.closeDialog(); | ||||
|         showAlertDialog(this._params!.dialogParentElement!, { | ||||
|   | ||||
| @@ -77,80 +77,84 @@ class MoreInfoMediaPlayer extends LitElement { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     if (!stateActive(this.stateObj)) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     const supportsMute = supportsFeature( | ||||
|       this.stateObj, | ||||
|       MediaPlayerEntityFeature.VOLUME_MUTE | ||||
|     ); | ||||
|     const supportsSliding = supportsFeature( | ||||
|     const supportsSet = supportsFeature( | ||||
|       this.stateObj, | ||||
|       MediaPlayerEntityFeature.VOLUME_SET | ||||
|     ); | ||||
|  | ||||
|     return html`${(supportsFeature( | ||||
|       this.stateObj!, | ||||
|       MediaPlayerEntityFeature.VOLUME_SET | ||||
|     ) || | ||||
|       supportsFeature(this.stateObj!, MediaPlayerEntityFeature.VOLUME_STEP)) && | ||||
|     stateActive(this.stateObj!) | ||||
|       ? html` | ||||
|           <div class="volume"> | ||||
|             ${supportsMute | ||||
|               ? html` | ||||
|                   <ha-icon-button | ||||
|                     .path=${this.stateObj.attributes.is_volume_muted | ||||
|                       ? mdiVolumeOff | ||||
|                       : mdiVolumeHigh} | ||||
|                     .label=${this.hass.localize( | ||||
|                       `ui.card.media_player.${ | ||||
|                         this.stateObj.attributes.is_volume_muted | ||||
|                           ? "media_volume_unmute" | ||||
|                           : "media_volume_mute" | ||||
|                       }` | ||||
|                     )} | ||||
|                     @click=${this._toggleMute} | ||||
|                   ></ha-icon-button> | ||||
|                 ` | ||||
|               : ""} | ||||
|             ${supportsFeature( | ||||
|               this.stateObj, | ||||
|               MediaPlayerEntityFeature.VOLUME_STEP | ||||
|             ) && !supportsSliding | ||||
|               ? html` | ||||
|                   <ha-icon-button | ||||
|                     action="volume_down" | ||||
|                     .path=${mdiVolumeMinus} | ||||
|                     .label=${this.hass.localize( | ||||
|                       "ui.card.media_player.media_volume_down" | ||||
|                     )} | ||||
|                     @click=${this._handleClick} | ||||
|                   ></ha-icon-button> | ||||
|                   <ha-icon-button | ||||
|                     action="volume_up" | ||||
|                     .path=${mdiVolumePlus} | ||||
|                     .label=${this.hass.localize( | ||||
|                       "ui.card.media_player.media_volume_up" | ||||
|                     )} | ||||
|                     @click=${this._handleClick} | ||||
|                   ></ha-icon-button> | ||||
|                 ` | ||||
|               : nothing} | ||||
|             ${supportsSliding | ||||
|               ? html` | ||||
|                   ${!supportsMute | ||||
|                     ? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>` | ||||
|                     : nothing} | ||||
|                   <ha-slider | ||||
|                     labeled | ||||
|                     id="input" | ||||
|                     .value=${Number(this.stateObj.attributes.volume_level) * | ||||
|                     100} | ||||
|                     @change=${this._selectedValueChanged} | ||||
|                   ></ha-slider> | ||||
|                 ` | ||||
|               : nothing} | ||||
|           </div> | ||||
|         ` | ||||
|       : nothing}`; | ||||
|     const supportsStep = supportsFeature( | ||||
|       this.stateObj, | ||||
|       MediaPlayerEntityFeature.VOLUME_STEP | ||||
|     ); | ||||
|  | ||||
|     if (!supportsMute && !supportsSet && !supportsStep) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <div class="volume"> | ||||
|         ${supportsMute | ||||
|           ? html` | ||||
|               <ha-icon-button | ||||
|                 .path=${this.stateObj.attributes.is_volume_muted | ||||
|                   ? mdiVolumeOff | ||||
|                   : mdiVolumeHigh} | ||||
|                 .label=${this.hass.localize( | ||||
|                   `ui.card.media_player.${ | ||||
|                     this.stateObj.attributes.is_volume_muted | ||||
|                       ? "media_volume_unmute" | ||||
|                       : "media_volume_mute" | ||||
|                   }` | ||||
|                 )} | ||||
|                 @click=${this._toggleMute} | ||||
|               ></ha-icon-button> | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${supportsStep | ||||
|           ? html` <ha-icon-button | ||||
|               action="volume_down" | ||||
|               .path=${mdiVolumeMinus} | ||||
|               .label=${this.hass.localize( | ||||
|                 "ui.card.media_player.media_volume_down" | ||||
|               )} | ||||
|               @click=${this._handleClick} | ||||
|             ></ha-icon-button>` | ||||
|           : nothing} | ||||
|         ${supportsSet | ||||
|           ? html` | ||||
|               ${!supportsMute && !supportsStep | ||||
|                 ? html`<ha-svg-icon .path=${mdiVolumeHigh}></ha-svg-icon>` | ||||
|                 : nothing} | ||||
|               <ha-slider | ||||
|                 labeled | ||||
|                 id="input" | ||||
|                 .value=${Number(this.stateObj.attributes.volume_level) * 100} | ||||
|                 @change=${this._selectedValueChanged} | ||||
|               ></ha-slider> | ||||
|             ` | ||||
|           : nothing} | ||||
|         ${supportsStep | ||||
|           ? html` | ||||
|               <ha-icon-button | ||||
|                 action="volume_up" | ||||
|                 .path=${mdiVolumePlus} | ||||
|                 .label=${this.hass.localize( | ||||
|                   "ui.card.media_player.media_volume_up" | ||||
|                 )} | ||||
|                 @click=${this._handleClick} | ||||
|               ></ha-icon-button> | ||||
|             ` | ||||
|           : nothing} | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   protected _renderSourceControl() { | ||||
|   | ||||
| @@ -1,10 +1,14 @@ | ||||
| import { mdiClose } from "@mdi/js"; | ||||
| import type { UnsubscribeFunc } from "home-assistant-js-websocket"; | ||||
| import type { CSSResultGroup } from "lit"; | ||||
| import { LitElement, css, html } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { LitElement, css, html, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { fireEvent } from "../../common/dom/fire_event"; | ||||
| import "../../components/ha-alert"; | ||||
| import "../../components/ha-wa-dialog"; | ||||
| import "../../components/ha-dialog-header"; | ||||
| import "../../components/ha-icon-button"; | ||||
| import "../../components/ha-md-dialog"; | ||||
| import type { HaMdDialog } from "../../components/ha-md-dialog"; | ||||
| import "../../components/ha-spinner"; | ||||
| import { | ||||
|   subscribeBackupEvents, | ||||
| @@ -33,6 +37,8 @@ class DialogRestartWait extends LitElement { | ||||
|  | ||||
|   private _backupEventsSubscription?: Promise<UnsubscribeFunc>; | ||||
|  | ||||
|   @query("ha-md-dialog") private _dialog?: HaMdDialog; | ||||
|  | ||||
|   public async showDialog(params: RestartWaitDialogParams): Promise<void> { | ||||
|     this._open = true; | ||||
|     this._loadBackupState(); | ||||
| @@ -43,11 +49,9 @@ class DialogRestartWait extends LitElement { | ||||
|     this._actionOnIdle = params.action; | ||||
|   } | ||||
|  | ||||
|   public closeDialog(): void { | ||||
|     this._open = false; | ||||
|   } | ||||
|  | ||||
|   private _dialogClosed(): void { | ||||
|     this._open = false; | ||||
|  | ||||
|     if (this._backupEventsSubscription) { | ||||
|       this._backupEventsSubscription.then((unsub) => { | ||||
|         unsub(); | ||||
| @@ -58,6 +62,10 @@ class DialogRestartWait extends LitElement { | ||||
|     fireEvent(this, "dialog-closed", { dialog: this.localName }); | ||||
|   } | ||||
|  | ||||
|   public closeDialog(): void { | ||||
|     this._dialog?.close(); | ||||
|   } | ||||
|  | ||||
|   private _getWaitMessage() { | ||||
|     switch (this._backupState) { | ||||
|       case "create_backup": | ||||
| @@ -72,17 +80,28 @@ class DialogRestartWait extends LitElement { | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this._open) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     const waitMessage = this._getWaitMessage(); | ||||
|  | ||||
|     return html` | ||||
|       <ha-wa-dialog | ||||
|         .hass=${this.hass} | ||||
|         .open=${this._open} | ||||
|         .headerTitle=${this._title} | ||||
|         width="medium" | ||||
|       <ha-md-dialog | ||||
|         open | ||||
|         @closed=${this._dialogClosed} | ||||
|         .disableCancelAction=${true} | ||||
|       > | ||||
|         <div class="content"> | ||||
|         <ha-dialog-header slot="headline"> | ||||
|           <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 | ||||
|             ? html`<ha-alert alert-type="error" | ||||
|                 >${this.hass.localize("ui.dialogs.restart.error_backup_state", { | ||||
| @@ -94,7 +113,7 @@ class DialogRestartWait extends LitElement { | ||||
|                 ${waitMessage} | ||||
|               `} | ||||
|         </div> | ||||
|       </ha-wa-dialog> | ||||
|       </ha-md-dialog> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -120,9 +139,15 @@ class DialogRestartWait extends LitElement { | ||||
|       haStyle, | ||||
|       haStyleDialog, | ||||
|       css` | ||||
|         ha-wa-dialog { | ||||
|         ha-md-dialog { | ||||
|           --dialog-content-padding: 0; | ||||
|         } | ||||
|         @media all and (min-width: 550px) { | ||||
|           ha-md-dialog { | ||||
|             min-width: 500px; | ||||
|             max-width: 500px; | ||||
|           } | ||||
|         } | ||||
|         .content { | ||||
|           display: flex; | ||||
|           flex-direction: column; | ||||
|   | ||||
| @@ -33,7 +33,7 @@ const COMPONENTS = { | ||||
|   "media-browser": () => | ||||
|     import("../panels/media-browser/ha-panel-media-browser"), | ||||
|   light: () => import("../panels/light/ha-panel-light"), | ||||
|   safety: () => import("../panels/safety/ha-panel-safety"), | ||||
|   security: () => import("../panels/security/ha-panel-security"), | ||||
|   climate: () => import("../panels/climate/ha-panel-climate"), | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -1,6 +1,5 @@ | ||||
| import { css, html, LitElement, nothing, type PropertyValues } from "lit"; | ||||
| import { css, html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { tinykeys } from "tinykeys"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeRTL } from "../../../common/util/compute_rtl"; | ||||
| import "../../../components/ha-resizable-bottom-sheet"; | ||||
| @@ -45,27 +44,11 @@ export default class HaAutomationSidebar extends LitElement { | ||||
|   @query("ha-resizable-bottom-sheet") | ||||
|   private _bottomSheetElement?: HaResizableBottomSheet; | ||||
|  | ||||
|   @query(".handle") | ||||
|   private _handleElement?: HTMLDivElement; | ||||
|  | ||||
|   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() { | ||||
|     super.disconnectedCallback(); | ||||
|     this._unregisterResizeHandlers(); | ||||
|     this._tinykeysUnsub?.(); | ||||
|   } | ||||
|  | ||||
|   private _renderContent() { | ||||
| @@ -187,9 +170,6 @@ export default class HaAutomationSidebar extends LitElement { | ||||
|         class="handle ${this._resizing ? "resizing" : ""}" | ||||
|         @mousedown=${this._handleMouseDown} | ||||
|         @touchstart=${this._handleMouseDown} | ||||
|         @focus=${this._startKeyboardResizing} | ||||
|         @blur=${this._stopKeyboardResizing} | ||||
|         tabindex="0" | ||||
|       > | ||||
|         <div class="indicator ${this._resizing ? "" : "hidden"}"></div> | ||||
|       </div> | ||||
| @@ -308,44 +288,6 @@ export default class HaAutomationSidebar extends LitElement { | ||||
|     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` | ||||
|     :host { | ||||
|       z-index: 6; | ||||
| @@ -400,10 +342,6 @@ export default class HaAutomationSidebar extends LitElement { | ||||
|       transform: scale3d(0, 1, 1); | ||||
|       opacity: 0; | ||||
|     } | ||||
|  | ||||
|     .handle:focus-visible { | ||||
|       outline: none; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import { consume } from "@lit/context"; | ||||
| import { | ||||
|   mdiAppleKeyboardCommand, | ||||
|   mdiContentCopy, | ||||
| @@ -14,7 +13,6 @@ import { | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, query, state } from "lit/decorators"; | ||||
| import { keyed } from "lit/directives/keyed"; | ||||
| import { capitalizeFirstLetter } from "../../../../common/string/capitalize-first-letter"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { handleStructError } from "../../../../common/structs/handle-errors"; | ||||
| import type { LocalizeKeys } from "../../../../common/translations/localize"; | ||||
| @@ -22,16 +20,7 @@ import "../../../../components/ha-md-divider"; | ||||
| import "../../../../components/ha-md-menu-item"; | ||||
| import { ACTION_BUILDING_BLOCKS } from "../../../../data/action"; | ||||
| import type { ActionSidebarConfig } from "../../../../data/automation"; | ||||
| import { | ||||
|   floorsContext, | ||||
|   fullEntitiesContext, | ||||
|   labelsContext, | ||||
| } from "../../../../data/context"; | ||||
| import type { EntityRegistryEntry } from "../../../../data/entity_registry"; | ||||
| import type { FloorRegistryEntry } from "../../../../data/floor_registry"; | ||||
| import type { LabelRegistryEntry } from "../../../../data/label_registry"; | ||||
| import type { RepeatAction } from "../../../../data/script"; | ||||
| import { describeAction } from "../../../../data/script_i18n"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { isMac } from "../../../../util/is_mac"; | ||||
| import type HaAutomationConditionEditor from "../action/ha-automation-action-editor"; | ||||
| @@ -59,18 +48,6 @@ export default class HaAutomationSidebarAction extends LitElement { | ||||
|  | ||||
|   @state() private _warnings?: string[]; | ||||
|  | ||||
|   @state() | ||||
|   @consume({ context: fullEntitiesContext, subscribe: true }) | ||||
|   _entityReg!: EntityRegistryEntry[]; | ||||
|  | ||||
|   @state() | ||||
|   @consume({ context: labelsContext, subscribe: true }) | ||||
|   _labelReg!: LabelRegistryEntry[]; | ||||
|  | ||||
|   @state() | ||||
|   @consume({ context: floorsContext, subscribe: true }) | ||||
|   _floorReg!: Record<string, FloorRegistryEntry>; | ||||
|  | ||||
|   @query(".sidebar-editor") | ||||
|   public editor?: HaAutomationConditionEditor; | ||||
|  | ||||
| @@ -101,20 +78,15 @@ export default class HaAutomationSidebarAction extends LitElement { | ||||
|  | ||||
|     const isBuildingBlock = ACTION_BUILDING_BLOCKS.includes(type || ""); | ||||
|  | ||||
|     const title = capitalizeFirstLetter( | ||||
|       describeAction( | ||||
|         this.hass, | ||||
|         this._entityReg, | ||||
|         this._labelReg, | ||||
|         this._floorReg, | ||||
|         actionConfig | ||||
|       ) | ||||
|     ); | ||||
|  | ||||
|     const subtitle = this.hass.localize( | ||||
|       "ui.panel.config.automation.editor.actions.action" | ||||
|     ); | ||||
|  | ||||
|     const title = | ||||
|       this.hass.localize( | ||||
|         `ui.panel.config.automation.editor.actions.type.${type}.label` as LocalizeKeys | ||||
|       ) || type; | ||||
|  | ||||
|     const description = isBuildingBlock | ||||
|       ? this.hass.localize( | ||||
|           `ui.panel.config.automation.editor.actions.type.${type}.description.picker` as LocalizeKeys | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { | ||||
|   mdiChartBox, | ||||
|   mdiCog, | ||||
|   mdiFolder, | ||||
|   mdiInformation, | ||||
|   mdiPlayBoxMultiple, | ||||
|   mdiPuzzle, | ||||
| } from "@mdi/js"; | ||||
| @@ -12,7 +11,6 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { isComponentLoaded } from "../../../../../common/config/is_component_loaded"; | ||||
| import { fireEvent } from "../../../../../common/dom/fire_event"; | ||||
| import "../../../../../components/ha-alert"; | ||||
| import "../../../../../components/ha-button"; | ||||
| import "../../../../../components/ha-expansion-panel"; | ||||
| import "../../../../../components/ha-md-list"; | ||||
| @@ -20,15 +18,10 @@ import "../../../../../components/ha-md-list-item"; | ||||
| import "../../../../../components/ha-md-select"; | ||||
| import type { HaMdSelect } from "../../../../../components/ha-md-select"; | ||||
| import "../../../../../components/ha-md-select-option"; | ||||
| import "../../../../../components/ha-spinner"; | ||||
| import "../../../../../components/ha-switch"; | ||||
| import type { HaSwitch } from "../../../../../components/ha-switch"; | ||||
| import "../../../../../components/ha-tooltip"; | ||||
| 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 { bytesToString } from "../../../../../util/bytes-to-string"; | ||||
| import "../ha-backup-addons-picker"; | ||||
| import type { BackupAddonItem } from "../ha-backup-addons-picker"; | ||||
| import { getRecorderInfo } from "../../../../../data/recorder"; | ||||
| @@ -85,14 +78,11 @@ class HaBackupConfigData extends LitElement { | ||||
|  | ||||
|   @state() private _showDbOption = true; | ||||
|  | ||||
|   @state() private _storageInfo?: HostDisksUsage | null; | ||||
|  | ||||
|   protected firstUpdated(changedProperties: PropertyValues): void { | ||||
|     super.firstUpdated(changedProperties); | ||||
|     this._checkDbOption(); | ||||
|     if (isComponentLoaded(this.hass, "hassio")) { | ||||
|       this._fetchAddons(); | ||||
|       this._fetchStorageInfo(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -124,68 +114,10 @@ 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 { | ||||
|     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( | ||||
|     (value: BackupConfigData | undefined, showAddon: boolean): FormData => { | ||||
|       if (!value) { | ||||
| @@ -239,7 +171,6 @@ class HaBackupConfigData extends LitElement { | ||||
|     const isHassio = isComponentLoaded(this.hass, "hassio"); | ||||
|  | ||||
|     return html` | ||||
|       ${this._renderSizeEstimate()} | ||||
|       <ha-md-list> | ||||
|         <ha-md-list-item> | ||||
|           <ha-svg-icon slot="start" .path=${mdiCog}></ha-svg-icon> | ||||
| @@ -450,103 +381,7 @@ 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` | ||||
|     .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 { | ||||
|       background: none; | ||||
|       --md-list-item-leading-space: 0; | ||||
|   | ||||
| @@ -31,7 +31,7 @@ import { showOptionsFlowDialog } from "../../../dialogs/config-flow/show-dialog- | ||||
| import { showRestartDialog } from "../../../dialogs/restart/show-dialog-restart"; | ||||
| import "../../../layouts/hass-subpage"; | ||||
| import { SubscribeMixin } from "../../../mixins/subscribe-mixin"; | ||||
| import type { ECOption } from "../../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../../resources/echarts"; | ||||
| import { haStyle } from "../../../resources/styles"; | ||||
| import { DefaultPrimaryColor } from "../../../resources/theme/color/color.globals"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
|   | ||||
| @@ -2,7 +2,6 @@ import { | ||||
|   mdiAlertCircle, | ||||
|   mdiChevronDown, | ||||
|   mdiCogOutline, | ||||
|   mdiContentCopy, | ||||
|   mdiDelete, | ||||
|   mdiDevices, | ||||
|   mdiDotsVertical, | ||||
| @@ -72,8 +71,6 @@ import { | ||||
| import "./ha-config-entry-device-row"; | ||||
| import { renderConfigEntryError } from "./ha-config-integration-page"; | ||||
| import "./ha-config-sub-entry-row"; | ||||
| import { copyToClipboard } from "../../../common/util/copy-clipboard"; | ||||
| import { showToast } from "../../../util/toast"; | ||||
|  | ||||
| @customElement("ha-config-entry-row") | ||||
| class HaConfigEntryRow extends LitElement { | ||||
| @@ -318,13 +315,6 @@ class HaConfigEntryRow extends LitElement { | ||||
|             )} | ||||
|           </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( | ||||
|             (flowType) => | ||||
|               html`<ha-md-menu-item | ||||
| @@ -633,15 +623,6 @@ 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() { | ||||
|     const newName = await showPromptDialog(this, { | ||||
|       title: this.hass.localize("ui.panel.config.integrations.rename_dialog"), | ||||
|   | ||||
| @@ -110,200 +110,191 @@ class ZHAConfigDashboard extends LitElement { | ||||
|         back-path="/config/integrations" | ||||
|         has-fab | ||||
|       > | ||||
|         <div class="container"> | ||||
|           <ha-card class="content network-status"> | ||||
|             ${this._error | ||||
|               ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||
|               : nothing} | ||||
|             <div class="card-content"> | ||||
|               <div class="heading"> | ||||
|                 <div class="icon"> | ||||
|                   <ha-svg-icon | ||||
|                     .path=${deviceOnline ? mdiCheckCircle : mdiAlertCircle} | ||||
|                     class=${deviceOnline ? "online" : "offline"} | ||||
|                   ></ha-svg-icon> | ||||
|                 </div> | ||||
|                 <div class="details"> | ||||
|                   ZHA | ||||
|         <ha-card class="content network-status"> | ||||
|           ${this._error | ||||
|             ? html`<ha-alert alert-type="error">${this._error}</ha-alert>` | ||||
|             : nothing} | ||||
|           <div class="card-content"> | ||||
|             <div class="heading"> | ||||
|               <div class="icon"> | ||||
|                 <ha-svg-icon | ||||
|                   .path=${deviceOnline ? mdiCheckCircle : mdiAlertCircle} | ||||
|                   class=${deviceOnline ? "online" : "offline"} | ||||
|                 ></ha-svg-icon> | ||||
|               </div> | ||||
|               <div class="details"> | ||||
|                 ZHA | ||||
|                 ${this.hass.localize( | ||||
|                   "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( | ||||
|                     "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( | ||||
|                       "ui.panel.config.zha.configuration_page.devices", | ||||
|                       { count: this._totalDevices } | ||||
|                     )} | ||||
|                   </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> | ||||
|                     "ui.panel.config.zha.configuration_page.devices", | ||||
|                     { count: this._totalDevices } | ||||
|                   )} | ||||
|                 </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> | ||||
|             ${this.configEntryId | ||||
|               ? html`<div class="card-actions"> | ||||
|                   <ha-button | ||||
|                     href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`} | ||||
|                     appearance="plain" | ||||
|                     size="small" | ||||
|           </div> | ||||
|           ${this.configEntryId | ||||
|             ? html`<div class="card-actions"> | ||||
|                 <ha-button | ||||
|                   href=${`/config/devices/dashboard?historyBack=1&config_entry=${this.configEntryId}`} | ||||
|                   appearance="plain" | ||||
|                   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 | ||||
|                   > | ||||
|                     ${this.hass.localize( | ||||
|                       "ui.panel.config.devices.caption" | ||||
|                     )}</ha-button | ||||
|                 </ha-settings-row> | ||||
|  | ||||
|                 <ha-settings-row> | ||||
|                   <span slot="heading" | ||||
|                     >${this._networkSettings.settings.network_info | ||||
|                       .extended_pan_id}</span | ||||
|                   > | ||||
|                   <ha-button | ||||
|                     appearance="plain" | ||||
|                     size="small" | ||||
|                     href=${`/config/entities/dashboard?historyBack=1&config_entry=${this.configEntryId}`} | ||||
|                   <span slot="description">Extended PAN ID</span> | ||||
|                 </ha-settings-row> | ||||
|  | ||||
|                 <ha-settings-row> | ||||
|                   <span slot="description">Channel</span> | ||||
|                   <span slot="heading" | ||||
|                     >${this._networkSettings.settings.network_info | ||||
|                       .channel}</span | ||||
|                   > | ||||
|                     ${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> | ||||
|  | ||||
|                   <ha-settings-row> | ||||
|                     <span slot="heading" | ||||
|                       >${this._networkSettings.settings.network_info | ||||
|                         .extended_pan_id}</span | ||||
|                     > | ||||
|                     <span slot="description">Extended PAN ID</span> | ||||
|                   </ha-settings-row> | ||||
|  | ||||
|                   <ha-settings-row> | ||||
|                     <span slot="description">Channel</span> | ||||
|                     <span slot="heading" | ||||
|                       >${this._networkSettings.settings.network_info | ||||
|                         .channel}</span | ||||
|                     > | ||||
|  | ||||
|                     <ha-icon-button | ||||
|                       .label=${this.hass.localize( | ||||
|                         "ui.panel.config.zha.configuration_page.change_channel" | ||||
|                       )} | ||||
|                       .path=${mdiPencil} | ||||
|                       @click=${this._showChannelMigrationDialog} | ||||
|                     > | ||||
|                     </ha-icon-button> | ||||
|                   </ha-settings-row> | ||||
|  | ||||
|                   <ha-settings-row> | ||||
|                     <span slot="description">Coordinator IEEE</span> | ||||
|                     <span slot="heading" | ||||
|                       >${this._networkSettings.settings.node_info.ieee}</span | ||||
|                     > | ||||
|                   </ha-settings-row> | ||||
|  | ||||
|                   <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` | ||||
|                   <ha-icon-button | ||||
|                     .label=${this.hass.localize( | ||||
|                       "ui.panel.config.zha.configuration_page.change_channel" | ||||
|                     )} | ||||
|                     .path=${mdiPencil} | ||||
|                     @click=${this._showChannelMigrationDialog} | ||||
|                   > | ||||
|                     <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> | ||||
|                   </ha-icon-button> | ||||
|                 </ha-settings-row> | ||||
|  | ||||
|                 <ha-settings-row> | ||||
|                   <span slot="description">Coordinator IEEE</span> | ||||
|                   <span slot="heading" | ||||
|                     >${this._networkSettings.settings.node_info.ieee}</span | ||||
|                   > | ||||
|                 </ha-settings-row> | ||||
|  | ||||
|                 <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> | ||||
|                     ` | ||||
|                   : ""} | ||||
|               </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> | ||||
|                   </div> | ||||
|                 </ha-card>` | ||||
|             ) | ||||
|           : ""} | ||||
|         <ha-card> | ||||
|           <div class="card-actions"> | ||||
|             <ha-button @click=${this._updateConfiguration}> | ||||
|               ${this.hass.localize( | ||||
|                 "ui.panel.config.zha.configuration_page.update_button" | ||||
|               )} | ||||
|             </ha-button> | ||||
|           </div> | ||||
|         </ha-card> | ||||
|  | ||||
|         <a href="/config/zha/add" slot="fab"> | ||||
|           <ha-fab | ||||
| @@ -498,10 +489,6 @@ class ZHAConfigDashboard extends LitElement { | ||||
|         .network-status .offline { | ||||
|           color: var(--error-color, var(--error-color)); | ||||
|         } | ||||
|  | ||||
|         .container { | ||||
|           padding: var(--ha-space-2) var(--ha-space-4) var(--ha-space-4); | ||||
|         } | ||||
|       `, | ||||
|     ]; | ||||
|   } | ||||
|   | ||||
| @@ -999,7 +999,6 @@ class ZWaveJSConfigDashboard extends SubscribeMixin(LitElement) { | ||||
|           display: flex; | ||||
|           gap: var(--ha-space-2); | ||||
|           margin-left: auto; | ||||
|           flex-wrap: wrap; | ||||
|         } | ||||
|  | ||||
|         .container { | ||||
|   | ||||
| @@ -318,13 +318,13 @@ export class HaConfigLovelaceDashboards extends LitElement { | ||||
|         }); | ||||
|       } | ||||
|  | ||||
|       if (this.hass.panels.safety) { | ||||
|       if (this.hass.panels.security) { | ||||
|         result.push({ | ||||
|           icon: "mdi:security", | ||||
|           title: this.hass.localize("panel.safety"), | ||||
|           title: this.hass.localize("panel.security"), | ||||
|           show_in_sidebar: false, | ||||
|           mode: "storage", | ||||
|           url_path: "safety", | ||||
|           url_path: "security", | ||||
|           filename: "", | ||||
|           iconColor: "var(--blue-grey-color)", | ||||
|           default: false, | ||||
|   | ||||
| @@ -1,10 +1,10 @@ | ||||
| import type { ActionDetail } from "@material/mwc-list"; | ||||
| import { | ||||
|   mdiDotsVertical, | ||||
|   mdiDownload, | ||||
|   mdiFilterRemove, | ||||
|   mdiImagePlus, | ||||
| } from "@mdi/js"; | ||||
| import type { ActionDetail } from "@material/mwc-list"; | ||||
| import { differenceInHours } from "date-fns"; | ||||
| import type { | ||||
|   HassServiceTarget, | ||||
| @@ -27,21 +27,21 @@ import { | ||||
| import { MIN_TIME_BETWEEN_UPDATES } from "../../components/chart/ha-chart-base"; | ||||
| import "../../components/chart/state-history-charts"; | ||||
| import type { StateHistoryCharts } from "../../components/chart/state-history-charts"; | ||||
| import "../../components/ha-button-menu"; | ||||
| import "../../components/ha-spinner"; | ||||
| import "../../components/ha-date-range-picker"; | ||||
| import "../../components/ha-icon-button"; | ||||
| import "../../components/ha-icon-button-arrow-prev"; | ||||
| import "../../components/ha-button-menu"; | ||||
| import "../../components/ha-list-item"; | ||||
| import "../../components/ha-icon-button-arrow-prev"; | ||||
| import "../../components/ha-menu-button"; | ||||
| import "../../components/ha-spinner"; | ||||
| import "../../components/ha-target-picker"; | ||||
| import "../../components/ha-top-app-bar-fixed"; | ||||
| import type { HistoryResult } from "../../data/history"; | ||||
| import { | ||||
|   computeHistory, | ||||
|   convertStatisticsToHistory, | ||||
|   mergeHistoryResults, | ||||
|   subscribeHistory, | ||||
|   mergeHistoryResults, | ||||
|   convertStatisticsToHistory, | ||||
| } from "../../data/history"; | ||||
| import { fetchStatistics } from "../../data/recorder"; | ||||
| import { resolveEntityIDs } from "../../data/selector"; | ||||
| @@ -182,7 +182,6 @@ class HaPanelHistory extends LitElement { | ||||
|               .disabled=${this._isLoading} | ||||
|               add-on-top | ||||
|               @value-changed=${this._targetsChanged} | ||||
|               compact | ||||
|             ></ha-target-picker> | ||||
|           </div> | ||||
|           ${this._isLoading | ||||
| @@ -650,10 +649,6 @@ class HaPanelHistory extends LitElement { | ||||
|           direction: var(--direction); | ||||
|         } | ||||
|  | ||||
|         ha-target-picker { | ||||
|           flex: 1; | ||||
|         } | ||||
|  | ||||
|         @media all and (max-width: 1025px) { | ||||
|           .filters { | ||||
|             flex-direction: column; | ||||
|   | ||||
| @@ -1,11 +1,9 @@ | ||||
| import { mdiRefresh } from "@mdi/js"; | ||||
| import type { HassServiceTarget } from "home-assistant-js-websocket"; | ||||
| import type { PropertyValues } from "lit"; | ||||
| import { css, html, LitElement } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import type { HassServiceTarget } from "home-assistant-js-websocket"; | ||||
| 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 { constructUrlCurrentPath } from "../../common/url/construct-url"; | ||||
| import { | ||||
| @@ -18,15 +16,17 @@ import "../../components/ha-date-range-picker"; | ||||
| import "../../components/ha-icon-button"; | ||||
| import "../../components/ha-icon-button-arrow-prev"; | ||||
| import "../../components/ha-menu-button"; | ||||
| import "../../components/ha-target-picker"; | ||||
| import "../../components/ha-top-app-bar-fixed"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "../../data/entity"; | ||||
| import "../../components/ha-target-picker"; | ||||
| import { filterLogbookCompatibleEntities } from "../../data/logbook"; | ||||
| import { resolveEntityIDs } from "../../data/selector"; | ||||
| import { getSensorNumericDeviceClasses } from "../../data/sensor"; | ||||
| import { haStyle } from "../../resources/styles"; | ||||
| import type { HomeAssistant } from "../../types"; | ||||
| 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") | ||||
| export class HaPanelLogbook extends LitElement { | ||||
| @@ -108,7 +108,6 @@ export class HaPanelLogbook extends LitElement { | ||||
|               .value=${this._targetPickerValue} | ||||
|               add-on-top | ||||
|               @value-changed=${this._targetsChanged} | ||||
|               compact | ||||
|             ></ha-target-picker> | ||||
|           </div> | ||||
|  | ||||
| @@ -364,10 +363,6 @@ export class HaPanelLogbook extends LitElement { | ||||
|           max-width: 400px; | ||||
|         } | ||||
|  | ||||
|         ha-target-picker { | ||||
|           flex: 1; | ||||
|         } | ||||
|  | ||||
|         :host([narrow]) ha-entity-picker { | ||||
|           max-width: none; | ||||
|           width: 100%; | ||||
|   | ||||
| @@ -9,9 +9,9 @@ import { computeCssColor } from "../../../common/color/compute-color"; | ||||
| import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; | ||||
| import { computeDomain } from "../../../common/entity/compute_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 { stateColorCss } from "../../../common/entity/state_color"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import "../../../components/ha-badge"; | ||||
| import "../../../components/ha-ripple"; | ||||
| import "../../../components/ha-state-icon"; | ||||
| @@ -189,11 +189,7 @@ export class HuiEntityBadge extends LitElement implements LovelaceBadge { | ||||
|       </state-display> | ||||
|     `; | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const name = this._config.name || computeStateName(stateObj); | ||||
|  | ||||
|     const showState = this._config.show_state; | ||||
|     const showName = this._config.show_name; | ||||
|   | ||||
| @@ -1,4 +1,3 @@ | ||||
| import type { EntityNameItem } from "../../../common/entity/compute_entity_name_display"; | ||||
| import type { ActionConfig } from "../../../data/lovelace/config/action"; | ||||
| import type { LovelaceBadgeConfig } from "../../../data/lovelace/config/badge"; | ||||
| import type { LegacyStateFilter } from "../common/evaluate-filter"; | ||||
| @@ -32,7 +31,7 @@ export interface StateLabelBadgeConfig extends LovelaceBadgeConfig { | ||||
| export interface EntityBadgeConfig extends LovelaceBadgeConfig { | ||||
|   type: "entity"; | ||||
|   entity?: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   icon?: string; | ||||
|   color?: string; | ||||
|   show_name?: boolean; | ||||
|   | ||||
| @@ -43,8 +43,6 @@ class HuiHistoryChartCardFeature | ||||
|  | ||||
|   @state() private _coordinates?: [number, number][]; | ||||
|  | ||||
|   @state() private _yAxisOrigin?: number; | ||||
|  | ||||
|   private _interval?: number; | ||||
|  | ||||
|   static getStubConfig(): TrendGraphCardFeatureConfig { | ||||
| @@ -107,10 +105,7 @@ class HuiHistoryChartCardFeature | ||||
|       `; | ||||
|     } | ||||
|     return html` | ||||
|       <hui-graph-base | ||||
|         .coordinates=${this._coordinates} | ||||
|         .yAxisOrigin=${this._yAxisOrigin} | ||||
|       ></hui-graph-base> | ||||
|       <hui-graph-base .coordinates=${this._coordinates}></hui-graph-base> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
| @@ -128,15 +123,14 @@ class HuiHistoryChartCardFeature | ||||
|     return subscribeHistoryStatesTimeWindow( | ||||
|       this.hass!, | ||||
|       (historyStates) => { | ||||
|         const { points, yAxisOrigin } = | ||||
|         this._coordinates = | ||||
|           coordinatesMinimalResponseCompressedState( | ||||
|             historyStates[this.context!.entity_id!], | ||||
|             this.clientWidth, | ||||
|             this.clientHeight, | ||||
|             this.clientWidth / 5 // sample to 1 point per 5 pixels | ||||
|           ); | ||||
|         this._coordinates = points; | ||||
|         this._yAxisOrigin = yAxisOrigin; | ||||
|             hourToShow, | ||||
|             500, | ||||
|             2, | ||||
|             undefined | ||||
|           ) || []; | ||||
|       }, | ||||
|       hourToShow, | ||||
|       [this.context!.entity_id!] | ||||
|   | ||||
| @@ -26,7 +26,7 @@ import { | ||||
|   formatDateVeryShort, | ||||
| } from "../../../../../common/datetime/format_date"; | ||||
| import { formatTime } from "../../../../../common/datetime/format_time"; | ||||
| import type { ECOption } from "../../../../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../../../../resources/echarts"; | ||||
| import { filterXSS } from "../../../../../common/util/xss"; | ||||
|  | ||||
| export function getSuggestedMax(dayDifference: number, end: Date): number { | ||||
|   | ||||
| @@ -36,7 +36,7 @@ import { | ||||
|   getCompareTransform, | ||||
| } from "./common/energy-chart-options"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
| import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../../../resources/echarts"; | ||||
| import { formatNumber } from "../../../../common/number/format_number"; | ||||
| import type { CustomLegendOption } from "../../../../components/chart/ha-chart-base"; | ||||
|  | ||||
|   | ||||
| @@ -2,22 +2,16 @@ import type { 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 { mdiChartDonut, mdiChartBar } from "@mdi/js"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import type { BarSeriesOption, PieSeriesOption } from "echarts/charts"; | ||||
| import { PieChart } from "echarts/charts"; | ||||
| import type { BarSeriesOption } from "echarts/charts"; | ||||
| import type { ECElementEvent } from "echarts/types/dist/shared"; | ||||
| import { filterXSS } from "../../../../common/util/xss"; | ||||
| import { getGraphColorByIndex } from "../../../../common/color/colors"; | ||||
| import { formatNumber } from "../../../../common/number/format_number"; | ||||
| import "../../../../components/chart/ha-chart-base"; | ||||
| import type { EnergyData } from "../../../../data/energy"; | ||||
| import { | ||||
|   computeConsumptionData, | ||||
|   getEnergyDataCollection, | ||||
|   getSummedData, | ||||
| } from "../../../../data/energy"; | ||||
| import { getEnergyDataCollection } from "../../../../data/energy"; | ||||
| import { | ||||
|   calculateStatisticSumGrowth, | ||||
|   getStatisticLabel, | ||||
| @@ -28,12 +22,10 @@ import type { HomeAssistant } from "../../../../types"; | ||||
| import type { LovelaceCard } from "../../types"; | ||||
| import type { EnergyDevicesGraphCardConfig } from "../types"; | ||||
| import { hasConfigChanged } from "../../common/has-changed"; | ||||
| import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../../../resources/echarts"; | ||||
| import "../../../../components/ha-card"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { measureTextWidth } from "../../../../util/text"; | ||||
| import "../../../../components/ha-icon-button"; | ||||
| import { storage } from "../../../../common/decorators/storage"; | ||||
|  | ||||
| @customElement("hui-energy-devices-graph-card") | ||||
| export class HuiEnergyDevicesGraphCard | ||||
| @@ -44,20 +36,10 @@ export class HuiEnergyDevicesGraphCard | ||||
|  | ||||
|   @state() private _config?: EnergyDevicesGraphCardConfig; | ||||
|  | ||||
|   @state() private _chartData: (BarSeriesOption | PieSeriesOption)[] = []; | ||||
|   @state() private _chartData: BarSeriesOption[] = []; | ||||
|  | ||||
|   @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"]; | ||||
|  | ||||
|   public hassSubscribe(): UnsubscribeFunc[] { | ||||
| @@ -94,16 +76,9 @@ export class HuiEnergyDevicesGraphCard | ||||
|  | ||||
|     return html` | ||||
|       <ha-card> | ||||
|         <div class="card-header"> | ||||
|           <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> | ||||
|         ${this._config.title | ||||
|           ? html`<h1 class="card-header">${this._config.title}</h1>` | ||||
|           : ""} | ||||
|         <div | ||||
|           class="content ${classMap({ | ||||
|             "has-header": !!this._config.title, | ||||
| @@ -112,10 +87,9 @@ export class HuiEnergyDevicesGraphCard | ||||
|           <ha-chart-base | ||||
|             .hass=${this.hass} | ||||
|             .data=${this._chartData} | ||||
|             .options=${this._createOptions(this._chartData, this._chartType)} | ||||
|             .height=${`${Math.max(300, (this._chartData[0]?.data?.length || 0) * 28 + 50)}px`} | ||||
|             .options=${this._createOptions(this._chartData)} | ||||
|             .height=${`${(this._chartData[0]?.data?.length || 0) * 28 + 50}px`} | ||||
|             @chart-click=${this._handleChartClick} | ||||
|             .extraComponents=${[PieChart]} | ||||
|           ></ha-chart-base> | ||||
|         </div> | ||||
|       </ha-card> | ||||
| @@ -123,86 +97,71 @@ export class HuiEnergyDevicesGraphCard | ||||
|   } | ||||
|  | ||||
|   private _renderTooltip(params: any) { | ||||
|     const deviceName = filterXSS(this._getDeviceName(params.name)); | ||||
|     const deviceName = filterXSS(this._getDeviceName(params.value[1])); | ||||
|     const title = `<h4 style="text-align: center; margin: 0;">${deviceName}</h4>`; | ||||
|     const value = `${formatNumber( | ||||
|       params.value[0] as number, | ||||
|       this.hass.locale, | ||||
|       params.value < 0.1 ? { maximumFractionDigits: 3 } : undefined | ||||
|       params.value[0] < 0.1 ? { maximumFractionDigits: 3 } : undefined | ||||
|     )} kWh`; | ||||
|     return `${title}${params.marker} ${params.seriesName}: ${value}`; | ||||
|   } | ||||
|  | ||||
|   private _createOptions = memoizeOne( | ||||
|     ( | ||||
|       data: (BarSeriesOption | PieSeriesOption)[], | ||||
|       chartType: "bar" | "pie" | ||||
|     ): ECOption => { | ||||
|       const options: ECOption = { | ||||
|         grid: { | ||||
|           top: 5, | ||||
|           left: 5, | ||||
|           right: 40, | ||||
|           bottom: 0, | ||||
|           containLabel: true, | ||||
|   private _createOptions = memoizeOne((data: BarSeriesOption[]): ECOption => { | ||||
|     const isMobile = window.matchMedia( | ||||
|       "all and (max-width: 450px), all and (max-height: 500px)" | ||||
|     ).matches; | ||||
|     return { | ||||
|       xAxis: { | ||||
|         type: "value", | ||||
|         name: "kWh", | ||||
|       }, | ||||
|       yAxis: { | ||||
|         type: "category", | ||||
|         inverse: 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: { | ||||
|           show: true, | ||||
|           formatter: this._renderTooltip.bind(this), | ||||
|         }, | ||||
|         xAxis: { show: false }, | ||||
|         yAxis: { show: false }, | ||||
|       }; | ||||
|       if (chartType === "bar") { | ||||
|         const isMobile = window.matchMedia( | ||||
|           "all and (max-width: 450px), all and (max-height: 500px)" | ||||
|         ).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; | ||||
|     } | ||||
|   ); | ||||
|       }, | ||||
|       grid: { | ||||
|         top: 5, | ||||
|         left: 5, | ||||
|         right: 40, | ||||
|         bottom: 0, | ||||
|         containLabel: true, | ||||
|       }, | ||||
|       tooltip: { | ||||
|         show: true, | ||||
|         formatter: this._renderTooltip.bind(this), | ||||
|       }, | ||||
|     }; | ||||
|   }); | ||||
|  | ||||
|   private _getDeviceName(statisticId: string): string { | ||||
|     const suffix = this._compoundStats.includes(statisticId) | ||||
|       ? ` (${this.hass.localize("ui.panel.lovelace.cards.energy.energy_devices_graph.untracked")})` | ||||
|       : ""; | ||||
|     return ( | ||||
|       (this._data?.prefs.device_consumption.find( | ||||
|       this._data?.prefs.device_consumption.find( | ||||
|         (d) => d.stat_consumption === statisticId | ||||
|       )?.name || | ||||
|         getStatisticLabel( | ||||
|           this.hass, | ||||
|           statisticId, | ||||
|           this._data?.statsMetadata[statisticId] | ||||
|         )) + suffix | ||||
|       getStatisticLabel( | ||||
|         this.hass, | ||||
|         statisticId, | ||||
|         this._data?.statsMetadata[statisticId] | ||||
|       ) | ||||
|     ); | ||||
|   } | ||||
|  | ||||
| @@ -210,105 +169,60 @@ export class HuiEnergyDevicesGraphCard | ||||
|     const data = energyData.stats; | ||||
|     const compareData = energyData.statsCompare; | ||||
|  | ||||
|     const chartData: NonNullable<(BarSeriesOption | PieSeriesOption)["data"]> = | ||||
|       []; | ||||
|     const chartDataCompare: NonNullable< | ||||
|       (BarSeriesOption | PieSeriesOption)["data"] | ||||
|     > = []; | ||||
|     const chartData: NonNullable<BarSeriesOption["data"]> = []; | ||||
|     const chartDataCompare: NonNullable<BarSeriesOption["data"]> = []; | ||||
|  | ||||
|     const datasets: (BarSeriesOption | PieSeriesOption)[] = [ | ||||
|     const datasets: BarSeriesOption[] = [ | ||||
|       { | ||||
|         type: this._chartType, | ||||
|         radius: [compareData ? "50%" : "40%", "70%"], | ||||
|         universalTransition: true, | ||||
|         type: "bar", | ||||
|         name: this.hass.localize( | ||||
|           "ui.panel.lovelace.cards.energy.energy_devices_graph.energy_usage" | ||||
|         ), | ||||
|         itemStyle: { | ||||
|           borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4, | ||||
|           borderRadius: [0, 4, 4, 0], | ||||
|         }, | ||||
|         data: chartData, | ||||
|         barWidth: compareData ? 10 : 20, | ||||
|         cursor: "default", | ||||
|         minShowLabelAngle: 15, | ||||
|         label: | ||||
|           this._chartType === "pie" | ||||
|             ? { | ||||
|                 formatter: ({ name }) => this._getDeviceName(name), | ||||
|               } | ||||
|             : undefined, | ||||
|       } as BarSeriesOption | PieSeriesOption, | ||||
|       }, | ||||
|     ]; | ||||
|  | ||||
|     if (compareData) { | ||||
|       datasets.push({ | ||||
|         type: this._chartType, | ||||
|         radius: ["30%", "50%"], | ||||
|         universalTransition: true, | ||||
|         type: "bar", | ||||
|         name: this.hass.localize( | ||||
|           "ui.panel.lovelace.cards.energy.energy_devices_graph.previous_energy_usage" | ||||
|         ), | ||||
|         itemStyle: { | ||||
|           borderRadius: this._chartType === "bar" ? [0, 4, 4, 0] : 4, | ||||
|           borderRadius: [0, 4, 4, 0], | ||||
|         }, | ||||
|         data: chartDataCompare, | ||||
|         barWidth: 10, | ||||
|         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); | ||||
|  | ||||
|     this._compoundStats = energyData.prefs.device_consumption | ||||
|       .map((d) => d.included_in_stat) | ||||
|       .filter(Boolean) as string[]; | ||||
|     const exclude = this._config?.hide_compound_stats | ||||
|       ? energyData.prefs.device_consumption | ||||
|           .map((d) => d.included_in_stat) | ||||
|           .filter(Boolean) | ||||
|       : []; | ||||
|  | ||||
|     const devices = energyData.prefs.device_consumption; | ||||
|     const devicesTotals: Record<string, number> = {}; | ||||
|     devices.forEach((device) => { | ||||
|       devicesTotals[device.stat_consumption] = | ||||
|     energyData.prefs.device_consumption.forEach((device, id) => { | ||||
|       if (exclude.includes(device.stat_consumption)) { | ||||
|         return; | ||||
|       } | ||||
|       const value = | ||||
|         device.stat_consumption in data | ||||
|           ? calculateStatisticSumGrowth(data[device.stat_consumption]) || 0 | ||||
|           : 0; | ||||
|     }); | ||||
|     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); | ||||
|       const color = getGraphColorByIndex(id, computedStyle); | ||||
|  | ||||
|       chartData.push({ | ||||
|         id: device.stat_consumption, | ||||
|         value: [value, device.stat_consumption] as any, | ||||
|         name: device.stat_consumption, | ||||
|         id, | ||||
|         value: [value, device.stat_consumption], | ||||
|         itemStyle: { | ||||
|           color: color + "7F", | ||||
|           borderColor: color, | ||||
| @@ -316,24 +230,16 @@ export class HuiEnergyDevicesGraphCard | ||||
|       }); | ||||
|  | ||||
|       if (compareData) { | ||||
|         let compareValue = | ||||
|         const compareValue = | ||||
|           device.stat_consumption in compareData | ||||
|             ? calculateStatisticSumGrowth( | ||||
|                 compareData[device.stat_consumption] | ||||
|               ) || 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({ | ||||
|           id: device.stat_consumption, | ||||
|           value: [compareValue, device.stat_consumption] as any, | ||||
|           name: device.stat_consumption, | ||||
|           id, | ||||
|           value: [compareValue, device.stat_consumption], | ||||
|           itemStyle: { | ||||
|             color: color + "32", | ||||
|             borderColor: color + "7F", | ||||
| @@ -343,62 +249,11 @@ export class HuiEnergyDevicesGraphCard | ||||
|     }); | ||||
|  | ||||
|     chartData.sort((a: any, b: any) => b.value[0] - a.value[0]); | ||||
|     if (compareData) { | ||||
|       datasets[1].data = chartData.map((d) => | ||||
|         chartDataCompare.find((d2) => (d2 as any).id === d.id) | ||||
|       ) as typeof chartDataCompare; | ||||
|     } | ||||
|  | ||||
|     datasets.forEach((dataset) => { | ||||
|       dataset.data!.length = Math.min( | ||||
|         this._config?.max_devices || Infinity, | ||||
|         dataset.data!.length | ||||
|       ); | ||||
|     }); | ||||
|  | ||||
|     if (this._chartType === "pie") { | ||||
|       const { summedData } = getSummedData(energyData); | ||||
|       const { consumption } = computeConsumptionData(summedData); | ||||
|       const 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) } | ||||
|                 ) | ||||
|               : "", | ||||
|         }, | ||||
|       }); | ||||
|     } | ||||
|     chartData.length = Math.min( | ||||
|       this._config?.max_devices || Infinity, | ||||
|       chartData.length | ||||
|     ); | ||||
|  | ||||
|     this._chartData = datasets; | ||||
|     await this.updateComplete; | ||||
| @@ -413,26 +268,11 @@ export class HuiEnergyDevicesGraphCard | ||||
|       fireEvent(this, "hass-more-info", { | ||||
|         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` | ||||
|     .card-header { | ||||
|       display: flex; | ||||
|       justify-content: space-between; | ||||
|       align-items: center; | ||||
|       padding-bottom: 0; | ||||
|     } | ||||
|     .content { | ||||
| @@ -444,11 +284,6 @@ export class HuiEnergyDevicesGraphCard | ||||
|     ha-chart-base { | ||||
|       --chart-max-height: none; | ||||
|     } | ||||
|     ha-icon-button { | ||||
|       transform: rotate(90deg); | ||||
|       color: var(--secondary-text-color); | ||||
|       cursor: pointer; | ||||
|     } | ||||
|   `; | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -28,7 +28,7 @@ import { | ||||
|   getCommonOptions, | ||||
|   getCompareTransform, | ||||
| } from "./common/energy-chart-options"; | ||||
| import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../../../resources/echarts"; | ||||
| import "./common/hui-energy-graph-chip"; | ||||
| import "../../../../components/ha-tooltip"; | ||||
|  | ||||
|   | ||||
| @@ -32,9 +32,9 @@ import { | ||||
|   getCommonOptions, | ||||
|   getCompareTransform, | ||||
| } from "./common/energy-chart-options"; | ||||
| import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||
| import "./common/hui-energy-graph-chip"; | ||||
| import "../../../../components/ha-tooltip"; | ||||
| import type { ECOption } from "../../../../resources/echarts"; | ||||
|  | ||||
| @customElement("hui-energy-solar-graph-card") | ||||
| export class HuiEnergySolarGraphCard | ||||
|   | ||||
| @@ -37,7 +37,7 @@ import { | ||||
|   getCommonOptions, | ||||
|   getCompareTransform, | ||||
| } from "./common/energy-chart-options"; | ||||
| import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../../../resources/echarts"; | ||||
|  | ||||
| const colorPropertyMap = { | ||||
|   to_grid: "--energy-grid-return-color", | ||||
|   | ||||
| @@ -27,7 +27,7 @@ import { | ||||
|   getCommonOptions, | ||||
|   getCompareTransform, | ||||
| } from "./common/energy-chart-options"; | ||||
| import type { ECOption } from "../../../../resources/echarts/echarts"; | ||||
| import type { ECOption } from "../../../../resources/echarts"; | ||||
| import { formatNumber } from "../../../../common/number/format_number"; | ||||
| import "./common/hui-energy-graph-chip"; | ||||
| import "../../../../components/ha-tooltip"; | ||||
|   | ||||
| @@ -28,7 +28,6 @@ import { | ||||
|   subscribeEntityRegistry, | ||||
| } from "../../../data/entity_registry"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| import type { LovelaceCard } from "../types"; | ||||
| @@ -233,16 +232,12 @@ class HuiAlarmPanelCard extends LitElement implements LovelaceCard { | ||||
|  | ||||
|     const defaultCode = this._entry?.options?.alarm_control_panel?.default_code; | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|  | ||||
|     return html` | ||||
|       <ha-card> | ||||
|         <h1 class="card-header"> | ||||
|           ${name} | ||||
|           ${this._config.name || | ||||
|           stateObj.attributes.friendly_name || | ||||
|           stateLabel} | ||||
|           <ha-assist-chip | ||||
|             filled | ||||
|             style=${styleMap({ | ||||
|   | ||||
| @@ -8,6 +8,7 @@ import { styleMap } from "lit/directives/style-map"; | ||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeStateDomain } from "../../../common/entity/compute_state_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import { | ||||
|   stateColorBrightness, | ||||
|   stateColorCss, | ||||
| @@ -26,7 +27,6 @@ import { CLIMATE_HVAC_ACTION_TO_MODE } from "../../../data/climate"; | ||||
| import { isUnavailableState } from "../../../data/entity"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { computeCardSize } from "../common/compute-card-size"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| @@ -125,11 +125,7 @@ export class HuiEntityCard extends LitElement implements LovelaceCard { | ||||
|       ? this._config.attribute in stateObj.attributes | ||||
|       : !isUnavailableState(stateObj.state); | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const name = this._config.name || computeStateName(stateObj); | ||||
|  | ||||
|     const colored = stateObj && this._getStateColor(stateObj, this._config); | ||||
|  | ||||
|   | ||||
| @@ -2,10 +2,11 @@ import type { HassEntity } from "home-assistant-js-websocket/dist/types"; | ||||
| 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 { ifDefined } from "lit/directives/if-defined"; | ||||
| import { classMap } from "lit/directives/class-map"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| 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 { getNumberFormatOptions } from "../../../common/number/format_number"; | ||||
| import "../../../components/ha-card"; | ||||
| @@ -14,7 +15,6 @@ import { UNAVAILABLE } from "../../../data/entity"; | ||||
| import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { handleAction } from "../common/handle-action"; | ||||
| import { hasAction, hasAnyAction } from "../common/has-action"; | ||||
| @@ -126,19 +126,13 @@ class HuiGaugeCard extends LitElement implements LovelaceCard { | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const name = this._config.name ?? computeStateName(stateObj); | ||||
|  | ||||
|     // Use `stateObj.state` as value to keep formatting (e.g trailing zeros) | ||||
|     // for consistent value display across gauge, entity, entity-row, etc. | ||||
|     return html` | ||||
|       <ha-card | ||||
|         class=${classMap({ | ||||
|           action: hasAnyAction(this._config), | ||||
|         })} | ||||
|         class=${classMap({ action: hasAnyAction(this._config) })} | ||||
|         @action=${this._handleAction} | ||||
|         .actionHandler=${actionHandler({ | ||||
|           hasHold: hasAction(this._config.hold_action), | ||||
|   | ||||
| @@ -33,7 +33,7 @@ import type { HomeSummaryCard } from "./types"; | ||||
| const COLORS: Record<HomeSummary, string> = { | ||||
|   light: "amber", | ||||
|   climate: "deep-orange", | ||||
|   safety: "blue-grey", | ||||
|   security: "blue-grey", | ||||
|   media_players: "blue", | ||||
| }; | ||||
|  | ||||
| @@ -147,20 +147,23 @@ export class HuiHomeSummaryCard extends LitElement implements LovelaceCard { | ||||
|           ? `${formattedMinTemp}°` | ||||
|           : `${formattedMinTemp} - ${formattedMaxTemp}°`; | ||||
|       } | ||||
|       case "safety": { | ||||
|       case "security": { | ||||
|         // Alarm and lock status | ||||
|         const safetyFilters = HOME_SUMMARIES_FILTERS.safety.map((filter) => | ||||
|         const securityFilters = HOME_SUMMARIES_FILTERS.security.map((filter) => | ||||
|           generateEntityFilter(this.hass!, filter) | ||||
|         ); | ||||
|  | ||||
|         const safetyEntities = findEntities(entitiesInsideArea, safetyFilters); | ||||
|         const securityEntities = findEntities( | ||||
|           entitiesInsideArea, | ||||
|           securityFilters | ||||
|         ); | ||||
|  | ||||
|         const locks = safetyEntities.filter((entityId) => { | ||||
|         const locks = securityEntities.filter((entityId) => { | ||||
|           const domain = computeDomain(entityId); | ||||
|           return domain === "lock"; | ||||
|         }); | ||||
|  | ||||
|         const alarms = safetyEntities.filter((entityId) => { | ||||
|         const alarms = securityEntities.filter((entityId) => { | ||||
|           const domain = computeDomain(entityId); | ||||
|           return domain === "alarm_control_panel"; | ||||
|         }); | ||||
|   | ||||
| @@ -6,6 +6,7 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import { stateColorCss } from "../../../common/entity/state_color"; | ||||
| import "../../../components/ha-card"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| @@ -14,7 +15,6 @@ import "../../../state-control/humidifier/ha-state-control-humidifier-humidity"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import "../card-features/hui-card-features"; | ||||
| import type { LovelaceCardFeatureContext } from "../card-features/types"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| import type { | ||||
| @@ -133,11 +133,7 @@ export class HuiHumidifierCard extends LitElement implements LovelaceCard { | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const name = this._config!.name || computeStateName(stateObj); | ||||
|  | ||||
|     const color = stateColorCss(stateObj); | ||||
|  | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import { classMap } from "lit/directives/class-map"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import { stateColorBrightness } from "../../../common/entity/state_color"; | ||||
| import "../../../components/ha-card"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| @@ -17,7 +18,6 @@ import { lightSupportsBrightness } from "../../../data/light"; | ||||
| import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { handleAction } from "../common/handle-action"; | ||||
| import { hasAction } from "../common/has-action"; | ||||
| @@ -92,11 +92,7 @@ export class HuiLightCard extends LitElement implements LovelaceCard { | ||||
|       ((stateObj.attributes.brightness || 0) / 255) * 100 | ||||
|     ); | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const name = this._config.name ?? computeStateName(stateObj); | ||||
|  | ||||
|     return html` | ||||
|       <ha-card> | ||||
|   | ||||
| @@ -12,6 +12,7 @@ import { classMap } from "lit/directives/class-map"; | ||||
| import { styleMap } from "lit/directives/style-map"; | ||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import { supportsFeature } from "../../../common/entity/supports-feature"; | ||||
| import { extractColors } from "../../../common/image/extract_color"; | ||||
| import { stateActive } from "../../../common/entity/state_active"; | ||||
| @@ -35,7 +36,6 @@ import { | ||||
|   mediaPlayerPlayMedia, | ||||
| } from "../../../data/media-player"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||
| import "../components/hui-marquee"; | ||||
| @@ -242,11 +242,8 @@ export class HuiMediaControlCard extends LitElement implements LovelaceCard { | ||||
|                 .hass=${this.hass} | ||||
|               ></ha-state-icon> | ||||
|               <div> | ||||
|                 ${computeLovelaceEntityName( | ||||
|                   this.hass, | ||||
|                   this.hass!.states[this._config!.entity], | ||||
|                   this._config.name | ||||
|                 )} | ||||
|                 ${this._config!.name || | ||||
|                 computeStateName(this.hass!.states[this._config!.entity])} | ||||
|               </div> | ||||
|             </div> | ||||
|             <div> | ||||
|   | ||||
| @@ -126,16 +126,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     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); | ||||
|  | ||||
|     let image: string | undefined = this._config.image; | ||||
|     if (this._config.image_entity) { | ||||
|       const stateObj: ImageEntity | PersonEntity | undefined = | ||||
|         this.hass.states[this._config.image_entity]; | ||||
| @@ -165,7 +156,7 @@ class HuiPictureElementsCard extends LitElement implements LovelaceCard { | ||||
|             .entity=${this._config.entity} | ||||
|             .aspectRatio=${this._config.aspect_ratio} | ||||
|             .darkModeFilter=${this._config.dark_mode_filter} | ||||
|             .darkModeImage=${darkModeImage} | ||||
|             .darkModeImage=${this._config.dark_mode_image} | ||||
|           ></hui-image> | ||||
|           ${this._elements} | ||||
|         </div> | ||||
|   | ||||
| @@ -5,6 +5,7 @@ import { classMap } from "lit/directives/class-map"; | ||||
| import { ifDefined } from "lit/directives/if-defined"; | ||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import "../../../components/ha-card"; | ||||
| import type { CameraEntity } from "../../../data/camera"; | ||||
| import type { ImageEntity } from "../../../data/image"; | ||||
| @@ -13,7 +14,6 @@ import type { ActionHandlerEvent } from "../../../data/lovelace/action_handler"; | ||||
| import type { PersonEntity } from "../../../data/person"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { handleAction } from "../common/handle-action"; | ||||
| import { hasAction } from "../common/has-action"; | ||||
| @@ -126,11 +126,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard { | ||||
|       `; | ||||
|     } | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const name = this._config.name || computeStateName(stateObj); | ||||
|     const entityState = this.hass.formatEntityState(stateObj); | ||||
|  | ||||
|     let footer: TemplateResult | string = ""; | ||||
| @@ -148,10 +144,7 @@ class HuiPictureEntityCard extends LitElement implements LovelaceCard { | ||||
|     } | ||||
|  | ||||
|     const domain: string = computeDomain(this._config.entity); | ||||
|     let image: string | undefined = | ||||
|       (typeof this._config?.image === "object" && | ||||
|         this._config.image.media_content_id) || | ||||
|       (this._config.image as string | undefined); | ||||
|     let image: string | undefined = this._config.image; | ||||
|     if (!image) { | ||||
|       switch (domain) { | ||||
|         case "image": | ||||
|   | ||||
| @@ -179,10 +179,7 @@ class HuiPictureGlanceCard extends LitElement implements LovelaceCard { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     let image: string | undefined = | ||||
|       (typeof this._config?.image === "object" && | ||||
|         this._config.image.media_content_id) || | ||||
|       (this._config.image as string | undefined); | ||||
|     let image: string | undefined = this._config.image; | ||||
|     if (this._config.image_entity) { | ||||
|       const stateObj: ImageEntity | PersonEntity | undefined = | ||||
|         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 { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { batteryLevelIcon } from "../../../common/entity/battery_icon"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import "../../../components/ha-card"; | ||||
| import "../../../components/ha-svg-icon"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { hasConfigOrEntityChanged } from "../common/has-changed"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| @@ -119,7 +119,7 @@ class HuiPlantStatusCard extends LitElement implements LovelaceCard { | ||||
|           style="background-image:url(${stateObj.attributes.entity_picture})" | ||||
|         > | ||||
|           <div class="header"> | ||||
|             ${computeLovelaceEntityName(this.hass, stateObj, this._config.name)} | ||||
|             ${this._config.name || computeStateName(stateObj)} | ||||
|           </div> | ||||
|         </div> | ||||
|         <div class="content"> | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import { styleMap } from "lit/directives/style-map"; | ||||
| import { applyThemesOnElement } from "../../../common/dom/apply_themes_on_element"; | ||||
| import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { computeDomain } from "../../../common/entity/compute_domain"; | ||||
| import { computeStateName } from "../../../common/entity/compute_state_name"; | ||||
| import { stateColorCss } from "../../../common/entity/state_color"; | ||||
| import "../../../components/ha-card"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| @@ -15,7 +16,6 @@ import "../../../state-control/water_heater/ha-state-control-water_heater-temper | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import "../card-features/hui-card-features"; | ||||
| import type { LovelaceCardFeatureContext } from "../card-features/types"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { createEntityNotFoundWarning } from "../components/hui-warning"; | ||||
| import type { | ||||
| @@ -132,11 +132,7 @@ export class HuiThermostatCard extends LitElement implements LovelaceCard { | ||||
|     } | ||||
|     const domain = computeDomain(stateObj.entity_id); | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const name = this._config!.name || computeStateName(stateObj); | ||||
|  | ||||
|     const color = stateColorCss(stateObj); | ||||
|  | ||||
|   | ||||
| @@ -9,6 +9,7 @@ import { computeCssColor } from "../../../common/color/compute-color"; | ||||
| import { hsv2rgb, rgb2hex, rgb2hsv } from "../../../common/color/convert-color"; | ||||
| import { DOMAINS_TOGGLE } from "../../../common/const"; | ||||
| 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 { stateColorCss } from "../../../common/entity/state_color"; | ||||
| import "../../../components/ha-card"; | ||||
| @@ -25,7 +26,6 @@ import type { HomeAssistant } from "../../../types"; | ||||
| import "../card-features/hui-card-features"; | ||||
| import type { LovelaceCardFeatureContext } from "../card-features/types"; | ||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { handleAction } from "../common/handle-action"; | ||||
| import { hasAction } from "../common/has-action"; | ||||
| @@ -47,6 +47,11 @@ export const getEntityDefaultTileIconAction = (entityId: string) => { | ||||
|   return supportsIconAction ? "toggle" : "none"; | ||||
| }; | ||||
|  | ||||
| export const DEFAULT_NAME = [ | ||||
|   { type: "device" }, | ||||
|   { type: "entity" }, | ||||
| ] satisfies EntityNameItem[]; | ||||
|  | ||||
| @customElement("hui-tile-card") | ||||
| export class HuiTileCard extends LitElement implements LovelaceCard { | ||||
|   public static async getConfigElement(): Promise<LovelaceCardEditor> { | ||||
| @@ -255,11 +260,12 @@ export class HuiTileCard extends LitElement implements LovelaceCard { | ||||
|  | ||||
|     const contentClasses = { vertical: Boolean(this._config.vertical) }; | ||||
|  | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const nameConfig = this._config.name; | ||||
|  | ||||
|     const nameDisplay = | ||||
|       typeof nameConfig === "string" | ||||
|         ? nameConfig | ||||
|         : this.hass.formatEntityName(stateObj, nameConfig || DEFAULT_NAME); | ||||
|  | ||||
|     const active = stateActive(stateObj); | ||||
|     const color = this._computeStateColor(stateObj, this._config.color); | ||||
| @@ -272,7 +278,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { | ||||
|             .stateObj=${stateObj} | ||||
|             .hass=${this.hass} | ||||
|             .content=${this._config.state_content} | ||||
|             .name=${name} | ||||
|             .name=${nameDisplay} | ||||
|           > | ||||
|           </state-display> | ||||
|         `; | ||||
| @@ -331,7 +337,7 @@ export class HuiTileCard extends LitElement implements LovelaceCard { | ||||
|               ${renderTileBadge(stateObj, this.hass)} | ||||
|             </ha-tile-icon> | ||||
|             <ha-tile-info id="info"> | ||||
|               <span slot="primary" class="primary">${name}</span> | ||||
|               <span slot="primary" class="primary">${nameDisplay}</span> | ||||
|               ${stateDisplay | ||||
|                 ? html`<span slot="secondary">${stateDisplay}</span>` | ||||
|                 : nothing} | ||||
|   | ||||
| @@ -7,6 +7,7 @@ import { classMap } from "lit/directives/class-map"; | ||||
| import { formatDateWeekdayShort } from "../../../common/datetime/format_date"; | ||||
| import { formatTime } from "../../../common/datetime/format_time"; | ||||
| 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 { formatNumber } from "../../../common/number/format_number"; | ||||
| import "../../../components/ha-card"; | ||||
| @@ -26,7 +27,6 @@ import { | ||||
| } from "../../../data/weather"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { actionHandler } from "../common/directives/action-handler-directive"; | ||||
| import { computeLovelaceEntityName } from "../common/entity/compute-lovelace-entity-name"; | ||||
| import { findEntities } from "../common/find-entities"; | ||||
| import { handleAction } from "../common/handle-action"; | ||||
| import { hasAction } from "../common/has-action"; | ||||
| @@ -229,7 +229,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { | ||||
|       return html` | ||||
|         <ha-card class="unavailable" @click=${this._handleAction}> | ||||
|           ${this.hass.localize("ui.panel.lovelace.warning.entity_unavailable", { | ||||
|             entity: `${computeLovelaceEntityName(this.hass, stateObj, this._config.name)} (${this._config.entity})`, | ||||
|             entity: `${computeStateName(stateObj)} (${this._config.entity})`, | ||||
|           })} | ||||
|         </ha-card> | ||||
|       `; | ||||
| @@ -260,11 +260,7 @@ class HuiWeatherForecastCard extends LitElement implements LovelaceCard { | ||||
|     const dayNight = forecastData?.type === "twice_daily"; | ||||
|  | ||||
|     const weatherStateIcon = getWeatherStateIcon(stateObj.state, this); | ||||
|     const name = computeLovelaceEntityName( | ||||
|       this.hass, | ||||
|       stateObj, | ||||
|       this._config.name | ||||
|     ); | ||||
|     const name = this._config.name ?? computeStateName(stateObj); | ||||
|  | ||||
|     return html` | ||||
|       <ha-card | ||||
|   | ||||
| @@ -40,7 +40,7 @@ export type AlarmPanelCardConfigState = | ||||
|  | ||||
| export interface AlarmPanelCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   states?: AlarmPanelCardConfigState[]; | ||||
|   theme?: string; | ||||
| } | ||||
| @@ -63,9 +63,6 @@ export interface EmptyStateCardConfig extends LovelaceCardConfig { | ||||
| } | ||||
|  | ||||
| export interface EntityCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   icon?: string; | ||||
|   attribute?: string; | ||||
|   unit?: string; | ||||
|   theme?: string; | ||||
| @@ -261,7 +258,7 @@ export interface GaugeSegment { | ||||
| export interface GaugeCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   attribute?: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   unit?: string; | ||||
|   min?: number; | ||||
|   max?: number; | ||||
| @@ -274,14 +271,12 @@ export interface GaugeCardConfig extends LovelaceCardConfig { | ||||
|   double_tap_action?: ActionConfig; | ||||
| } | ||||
|  | ||||
| export interface ActionsConfig { | ||||
| export interface ConfigEntity extends EntityConfig { | ||||
|   tap_action?: ActionConfig; | ||||
|   hold_action?: ActionConfig; | ||||
|   double_tap_action?: ActionConfig; | ||||
| } | ||||
|  | ||||
| export interface ConfigEntity extends EntityConfig, ActionsConfig {} | ||||
|  | ||||
| export interface PictureGlanceEntityConfig extends ConfigEntity { | ||||
|   show_state?: boolean; | ||||
|   attribute?: string; | ||||
| @@ -311,7 +306,7 @@ export interface GlanceCardConfig extends LovelaceCardConfig { | ||||
| export interface HumidifierCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   theme?: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   show_current_as_primary?: boolean; | ||||
|   features?: LovelaceCardFeatureConfig[]; | ||||
| } | ||||
| @@ -327,7 +322,7 @@ export interface IframeCardConfig extends LovelaceCardConfig { | ||||
|  | ||||
| export interface LightCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   theme?: string; | ||||
|   icon?: string; | ||||
|   tap_action?: ActionConfig; | ||||
| @@ -399,7 +394,6 @@ export interface ClockCardConfig extends LovelaceCardConfig { | ||||
|  | ||||
| export interface MediaControlCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   theme?: string; | ||||
| } | ||||
|  | ||||
| @@ -459,7 +453,7 @@ export interface PictureCardConfig extends LovelaceCardConfig { | ||||
|  | ||||
| export interface PictureElementsCardConfig extends LovelaceCardConfig { | ||||
|   title?: string; | ||||
|   image?: string | MediaSelectorValue; | ||||
|   image?: string; | ||||
|   image_entity?: string; | ||||
|   camera_image?: string; | ||||
|   camera_view?: HuiImage["cameraView"]; | ||||
| @@ -469,14 +463,14 @@ export interface PictureElementsCardConfig extends LovelaceCardConfig { | ||||
|   entity?: string; | ||||
|   elements: LovelaceElementConfig[]; | ||||
|   theme?: string; | ||||
|   dark_mode_image?: string | MediaSelectorValue; | ||||
|   dark_mode_image?: string; | ||||
|   dark_mode_filter?: string; | ||||
| } | ||||
|  | ||||
| export interface PictureEntityCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   image?: string | MediaSelectorValue; | ||||
|   name?: string; | ||||
|   image?: string; | ||||
|   camera_image?: string; | ||||
|   camera_view?: HuiImage["cameraView"]; | ||||
|   state_image?: Record<string, unknown>; | ||||
| @@ -494,7 +488,7 @@ export interface PictureEntityCardConfig extends LovelaceCardConfig { | ||||
| export interface PictureGlanceCardConfig extends LovelaceCardConfig { | ||||
|   entities: (string | PictureGlanceEntityConfig)[]; | ||||
|   title?: string; | ||||
|   image?: string | MediaSelectorValue; | ||||
|   image?: string; | ||||
|   image_entity?: string; | ||||
|   camera_image?: string; | ||||
|   camera_view?: HuiImage["cameraView"]; | ||||
| @@ -515,14 +509,14 @@ export interface PlantAttributeTarget extends EventTarget { | ||||
| } | ||||
|  | ||||
| export interface PlantStatusCardConfig extends LovelaceCardConfig { | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   entity: string; | ||||
|   theme?: string; | ||||
| } | ||||
|  | ||||
| export interface SensorCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   icon?: string; | ||||
|   graph?: string; | ||||
|   unit?: string; | ||||
| @@ -558,14 +552,14 @@ export interface GridCardConfig extends StackCardConfig { | ||||
| export interface ThermostatCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   theme?: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   show_current_as_primary?: boolean; | ||||
|   features?: LovelaceCardFeatureConfig[]; | ||||
| } | ||||
|  | ||||
| export interface WeatherForecastCardConfig extends LovelaceCardConfig { | ||||
|   entity: string; | ||||
|   name?: string | EntityNameItem | EntityNameItem[]; | ||||
|   name?: string; | ||||
|   show_current?: boolean; | ||||
|   show_forecast?: boolean; | ||||
|   forecast_type?: ForecastType; | ||||
|   | ||||
| @@ -1,23 +0,0 @@ | ||||
| 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,85 +1,134 @@ | ||||
| import { downSampleLineData } from "../../../../components/chart/down-sample"; | ||||
| import { strokeWidth } from "../../../../data/graph"; | ||||
| 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 = ( | ||||
|   history: [number, number][], | ||||
|   history: any, | ||||
|   hours: number, | ||||
|   width: number, | ||||
|   height: number, | ||||
|   limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } | ||||
| ) => { | ||||
|   let yAxisOrigin = height; | ||||
|   let minY = limits?.minY ?? history[0][1]; | ||||
|   let maxY = limits?.maxY ?? history[0][1]; | ||||
|   const minX = limits?.minX ?? history[0][0]; | ||||
|   const maxX = limits?.maxX ?? history[history.length - 1][0]; | ||||
|   history.forEach(([_, stateValue]) => { | ||||
|     if (stateValue < minY) { | ||||
|       minY = stateValue; | ||||
|     } else if (stateValue > maxY) { | ||||
|       maxY = stateValue; | ||||
|     } | ||||
|   }); | ||||
|   const rangeY = maxY - minY || minY * 0.1; | ||||
|   if (maxY < 0) { | ||||
|     // all values are negative | ||||
|     // add margin | ||||
|     maxY += rangeY * 0.1; | ||||
|     maxY = Math.min(0, maxY); | ||||
|     yAxisOrigin = 0; | ||||
|   } else if (minY < 0) { | ||||
|     // some values are negative | ||||
|     yAxisOrigin = (maxY / (maxY - minY || 1)) * height; | ||||
|   } else { | ||||
|     // all values are positive | ||||
|     // add margin | ||||
|     minY -= rangeY * 0.1; | ||||
|     minY = Math.max(0, minY); | ||||
|   detail: number, | ||||
|   min: number, | ||||
|   max: number | ||||
| ): [number, number][] => { | ||||
|   const coords = [] as [number, number][]; | ||||
|   const height = 80; | ||||
|   let yRatio = (max - min) / height; | ||||
|   yRatio = yRatio !== 0 ? yRatio : height; | ||||
|   let xRatio = width / (hours - (detail === 1 ? 1 : 0)); | ||||
|   xRatio = isFinite(xRatio) ? xRatio : width; | ||||
|  | ||||
|   let first = history.filter(Boolean)[0]; | ||||
|   if (detail > 1) { | ||||
|     first = first.filter(Boolean)[0]; | ||||
|   } | ||||
|   const yDenom = maxY - minY || 1; | ||||
|   const xDenom = maxX - minX || 1; | ||||
|   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 }; | ||||
|   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); | ||||
|  | ||||
|     if (item) { | ||||
|       last = [average(item), lastValue(item)]; | ||||
|     } | ||||
|     const y = getY(item ? last[0] : last[1]); | ||||
|     return coords.push([x, y]); | ||||
|   }; | ||||
|  | ||||
|   for (let i = 0; i < history.length; i += 1) { | ||||
|     getCoords(history[i], i, 0, detail); | ||||
|   } | ||||
|  | ||||
|   coords.push([width, getY(last[1])]); | ||||
|   return coords; | ||||
| }; | ||||
|  | ||||
| export const coordinates = ( | ||||
|   history: [number, number][], | ||||
|   history: any, | ||||
|   hours: number, | ||||
|   width: number, | ||||
|   height: number, | ||||
|   maxDetails: number, | ||||
|   limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } | ||||
| ) => { | ||||
|   history = history.filter((item) => !Number.isNaN(item[1])); | ||||
|   detail: number, | ||||
|   limits?: { min?: number; max?: number } | ||||
| ): [number, number][] | undefined => { | ||||
|   history.forEach((item) => { | ||||
|     item.state = Number(item.state); | ||||
|   }); | ||||
|   history = history.filter((item) => !Number.isNaN(item.state)); | ||||
|  | ||||
|   const sampledData: [number, number][] = downSampleLineData( | ||||
|     history, | ||||
|     maxDetails, | ||||
|     limits?.minX, | ||||
|     limits?.maxX | ||||
|   ); | ||||
|   return calcPoints(sampledData, width, height, limits); | ||||
|   const min = | ||||
|     limits?.min !== undefined | ||||
|       ? limits.min | ||||
|       : Math.min(...history.map((item) => item.state)); | ||||
|   const max = | ||||
|     limits?.max !== undefined | ||||
|       ? limits.max | ||||
|       : 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 = ( | ||||
|   history: EntityHistoryState[] | undefined, | ||||
|   history: EntityHistoryState[], | ||||
|   hours: number, | ||||
|   width: number, | ||||
|   height: number, | ||||
|   maxDetails: number, | ||||
|   limits?: { minX?: number; maxX?: number; minY?: number; maxY?: number } | ||||
| ) => { | ||||
|   if (!history?.length) { | ||||
|     return { points: [], yAxisOrigin: 0 }; | ||||
|   detail: number, | ||||
|   limits?: { min?: number; max?: number } | ||||
| ): [number, number][] | undefined => { | ||||
|   if (!history) { | ||||
|     return undefined; | ||||
|   } | ||||
|   const mappedHistory: [number, number][] = history.map((item) => [ | ||||
|   const numericHistory: NumericEntityHistoryState[] = history.map((item) => ({ | ||||
|     state: Number(item.s), | ||||
|     // 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 | ||||
|     // we already filtered out states that are the same. | ||||
|     item.lu * 1000, | ||||
|     Number(item.s), | ||||
|   ]); | ||||
|   return coordinates(mappedHistory, width, height, maxDetails, limits); | ||||
|     last_changed: item.lu * 1000, | ||||
|   })); | ||||
|   return coordinates(numericHistory, hours, width, detail, limits); | ||||
| }; | ||||
|   | ||||
| @@ -1,11 +1,11 @@ | ||||
| import type { ActionConfig } from "../../../data/lovelace/config/action"; | ||||
| import type { ActionsConfig } from "../cards/types"; | ||||
| import type { ConfigEntity } from "../cards/types"; | ||||
|  | ||||
| export function hasAction(config?: ActionConfig): boolean { | ||||
|   return config !== undefined && config.action !== "none"; | ||||
| } | ||||
|  | ||||
| export function hasAnyAction(config: ActionsConfig): boolean { | ||||
| export function hasAnyAction(config: ConfigEntity): boolean { | ||||
|   return ( | ||||
|     !config.tap_action || | ||||
|     hasAction(config.tap_action) || | ||||
|   | ||||
| @@ -6,10 +6,12 @@ import { fireEvent } from "../../../common/dom/fire_event"; | ||||
| import { entityUseDeviceName } from "../../../common/entity/compute_entity_name"; | ||||
| import { computeRTL } from "../../../common/util/compute_rtl"; | ||||
| import "../../../components/entity/ha-entity-picker"; | ||||
| import type { HaEntityPicker } from "../../../components/entity/ha-entity-picker"; | ||||
| import type { | ||||
|   HaEntityPicker, | ||||
|   HaEntityPickerEntityFilterFunc, | ||||
| } from "../../../components/entity/ha-entity-picker"; | ||||
| import "../../../components/ha-icon-button"; | ||||
| import "../../../components/ha-sortable"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "../../../data/entity"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import type { EntityConfig } from "../entity-rows/types"; | ||||
|  | ||||
|   | ||||
| @@ -6,26 +6,20 @@ import { getPath } from "../common/graph/get-path"; | ||||
|  | ||||
| @customElement("hui-graph-base") | ||||
| export class HuiGraphBase extends LitElement { | ||||
|   @property({ attribute: false }) public coordinates?: number[][]; | ||||
|  | ||||
|   @property({ attribute: "y-axis-origin", type: Number }) | ||||
|   public yAxisOrigin?: number; | ||||
|   @property() public coordinates?: any; | ||||
|  | ||||
|   @state() private _path?: string; | ||||
|  | ||||
|   protected render(): TemplateResult { | ||||
|     const width = this.clientWidth || 500; | ||||
|     const height = this.clientHeight || width / 5; | ||||
|     const yAxisOrigin = this.yAxisOrigin ?? height; | ||||
|     return html` | ||||
|       ${this._path | ||||
|         ? svg`<svg width="100%" height="100%" viewBox="0 0 ${width} ${height}" preserveAspectRatio="none"> | ||||
|         ? svg`<svg width="100%" height="100%" viewBox="0 0 500 100" preserveAspectRatio="none"> | ||||
|           <g> | ||||
|             <mask id="fill"> | ||||
|               <path | ||||
|                 class='fill' | ||||
|                 fill='white' | ||||
|                 d="${this._path} L ${width}, ${yAxisOrigin} L 0, ${yAxisOrigin} z" | ||||
|                 d="${this._path} L 500, 100 L 0, 100 z" | ||||
|               /> | ||||
|             </mask> | ||||
|             <rect height="100%" width="100%" id="fill-rect" fill="var(--accent-color)" mask="url(#fill)"></rect> | ||||
| @@ -44,7 +38,7 @@ export class HuiGraphBase extends LitElement { | ||||
|             <rect height="100%" width="100%" id="rect" fill="var(--accent-color)" mask="url(#line)"></rect> | ||||
|           </g> | ||||
|         </svg>` | ||||
|         : svg`<svg width="100%" height="100%" viewBox="0 0 ${width} ${height}"></svg>`} | ||||
|         : svg`<svg width="100%" height="100%" viewBox="0 0 500 100"></svg>`} | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -15,10 +15,6 @@ import { UNAVAILABLE } from "../../../data/entity"; | ||||
| import type { ImageEntity } from "../../../data/image"; | ||||
| import { computeImageUrl } from "../../../data/image"; | ||||
| import type { HomeAssistant } from "../../../types"; | ||||
| import { | ||||
|   isMediaSourceContentId, | ||||
|   resolveMediaSource, | ||||
| } from "../../../data/media_source"; | ||||
|  | ||||
| const UPDATE_INTERVAL = 10000; | ||||
| const DEFAULT_FILTER = "grayscale(100%)"; | ||||
| @@ -71,12 +67,6 @@ export class HuiImage extends LitElement { | ||||
|  | ||||
|   @state() private _loadedImageSrc?: string; | ||||
|  | ||||
|   @state() private _resolvedImageSrc?: string; | ||||
|  | ||||
|   @state() private _resolvedDarkModeImageSrc?: string; | ||||
|  | ||||
|   @state() private _resolvedStateImages: Record<string, string> = {}; | ||||
|  | ||||
|   @state() private _lastImageHeight?: number; | ||||
|  | ||||
|   private _intersectionObserver?: IntersectionObserver; | ||||
| @@ -140,46 +130,6 @@ export class HuiImage extends LitElement { | ||||
|     if (this._loadState === LoadState.Loading && !this.cameraImage) { | ||||
|       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() { | ||||
| @@ -205,20 +155,20 @@ export class HuiImage extends LitElement { | ||||
|         imageSrc = this._cameraImageSrc; | ||||
|       } | ||||
|     } else if (this.stateImage) { | ||||
|       const stateImage = this._resolvedStateImages[entityState]; | ||||
|       const stateImage = this.stateImage[entityState]; | ||||
|  | ||||
|       if (stateImage) { | ||||
|         imageSrc = stateImage; | ||||
|       } else { | ||||
|         imageSrc = this._resolvedImageSrc; | ||||
|         imageSrc = this.image; | ||||
|         imageFallback = true; | ||||
|       } | ||||
|     } else if (this.darkModeImage && this.hass.themes.darkMode) { | ||||
|       imageSrc = this._resolvedDarkModeImageSrc; | ||||
|       imageSrc = this.darkModeImage; | ||||
|     } else if (stateObj && computeDomain(stateObj.entity_id) === "image") { | ||||
|       imageSrc = computeImageUrl(stateObj as ImageEntity); | ||||
|     } else { | ||||
|       imageSrc = this._resolvedImageSrc; | ||||
|       imageSrc = this.image; | ||||
|     } | ||||
|  | ||||
|     if (imageSrc) { | ||||
|   | ||||
| @@ -2,15 +2,7 @@ import memoizeOne from "memoize-one"; | ||||
| import { mdiGestureTap } from "@mdi/js"; | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { | ||||
|   any, | ||||
|   assert, | ||||
|   literal, | ||||
|   object, | ||||
|   optional, | ||||
|   string, | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import { any, assert, literal, object, optional, string } from "superstruct"; | ||||
| import type { LocalizeFunc } from "../../../../../common/translations/localize"; | ||||
| import { fireEvent } from "../../../../../common/dom/fire_event"; | ||||
| import "../../../../../components/ha-form/ha-form"; | ||||
| @@ -23,7 +15,7 @@ import { actionConfigStruct } from "../../structs/action-struct"; | ||||
| const imageElementConfigStruct = object({ | ||||
|   type: literal("image"), | ||||
|   entity: optional(string()), | ||||
|   image: optional(union([string(), object()])), | ||||
|   image: optional(string()), | ||||
|   style: optional(any()), | ||||
|   title: optional(string()), | ||||
|   tap_action: optional(actionConfigStruct), | ||||
| @@ -95,20 +87,7 @@ export class HuiImageElementEditor | ||||
|             }, | ||||
|           ], | ||||
|         }, | ||||
|         { | ||||
|           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: "image", selector: { image: {} } }, | ||||
|         { name: "camera_image", selector: { entity: { domain: "camera" } } }, | ||||
|         { | ||||
|           name: "camera_view", | ||||
| @@ -140,7 +119,7 @@ export class HuiImageElementEditor | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${this._processData(this._config)} | ||||
|         .data=${this._config} | ||||
|         .schema=${this._schema(this.hass.localize)} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
| @@ -148,13 +127,6 @@ export class HuiImageElementEditor | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _processData = memoizeOne((config: ImageElementConfig) => ({ | ||||
|     ...config, | ||||
|     ...(typeof config.image === "string" | ||||
|       ? { image: { media_content_id: config.image } } | ||||
|       : {}), | ||||
|   })); | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|   | ||||
| @@ -1,34 +1,32 @@ | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import memoizeOne from "memoize-one"; | ||||
| import { array, assert, assign, object, optional, string } from "superstruct"; | ||||
| import type { HassEntity } from "home-assistant-js-websocket"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import { supportsFeature } from "../../../../common/entity/supports-feature"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import { ALARM_MODES } from "../../../../data/alarm_control_panel"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { | ||||
|   ALARM_MODE_STATE_MAP, | ||||
|   DEFAULT_STATES, | ||||
|   filterSupportedAlarmStates, | ||||
| } from "../../cards/hui-alarm-panel-card"; | ||||
| import type { | ||||
|   AlarmPanelCardConfig, | ||||
|   AlarmPanelCardConfigState, | ||||
| } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
| import { | ||||
|   DEFAULT_STATES, | ||||
|   ALARM_MODE_STATE_MAP, | ||||
|   filterSupportedAlarmStates, | ||||
| } from "../../cards/hui-alarm-panel-card"; | ||||
| import { supportsFeature } from "../../../../common/entity/supports-feature"; | ||||
| import { ALARM_MODES } from "../../../../data/alarm_control_panel"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     entity: optional(string()), | ||||
|     name: optional(entityNameStruct), | ||||
|     name: optional(string()), | ||||
|     states: optional(array()), | ||||
|     theme: optional(string()), | ||||
|   }) | ||||
| @@ -63,15 +61,13 @@ export class HuiAlarmPanelCardEditor | ||||
|           selector: { entity: { domain: "alarm_control_panel" } }, | ||||
|         }, | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|           type: "grid", | ||||
|           name: "", | ||||
|           schema: [ | ||||
|             { name: "name", selector: { text: {} } }, | ||||
|             { name: "theme", selector: { theme: {} } }, | ||||
|           ], | ||||
|         }, | ||||
|         { name: "theme", selector: { theme: {} } }, | ||||
|         { | ||||
|           name: "states", | ||||
|           selector: { | ||||
|   | ||||
| @@ -13,7 +13,6 @@ import { | ||||
|   string, | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| @@ -32,7 +31,6 @@ import type { LovelaceBadgeEditor } from "../../types"; | ||||
| import "../hui-sub-element-editor"; | ||||
| import { actionConfigStruct } from "../structs/action-struct"; | ||||
| import { baseLovelaceBadgeConfig } from "../structs/base-badge-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import "./hui-card-features-editor"; | ||||
|  | ||||
| @@ -41,7 +39,7 @@ const badgeConfigStruct = assign( | ||||
|   object({ | ||||
|     entity: optional(string()), | ||||
|     display_type: optional(enums(DISPLAY_TYPES)), | ||||
|     name: optional(entityNameStruct), | ||||
|     name: optional(string()), | ||||
|     icon: optional(string()), | ||||
|     state_content: optional(union([string(), array(string())])), | ||||
|     color: optional(string()), | ||||
| @@ -83,19 +81,16 @@ export class HuiEntityBadgeEditor | ||||
|           flatten: true, | ||||
|           iconPath: mdiTextShort, | ||||
|           schema: [ | ||||
|             { | ||||
|               name: "name", | ||||
|               selector: { | ||||
|                 entity_name: { | ||||
|                   default_name: DEFAULT_ENTITY_NAME, | ||||
|                 }, | ||||
|               }, | ||||
|               context: { entity: "entity" }, | ||||
|             }, | ||||
|             { | ||||
|               name: "", | ||||
|               type: "grid", | ||||
|               schema: [ | ||||
|                 { | ||||
|                   name: "name", | ||||
|                   selector: { | ||||
|                     text: {}, | ||||
|                   }, | ||||
|                 }, | ||||
|                 { | ||||
|                   name: "color", | ||||
|                   selector: { | ||||
|   | ||||
| @@ -1,17 +1,16 @@ | ||||
| import { assert, assign, boolean, object, optional, string } from "superstruct"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import type { HaFormSchema } from "../../../../components/ha-form/types"; | ||||
| import type { EntityCardConfig } from "../../cards/types"; | ||||
| import { headerFooterConfigStructs } from "../../header-footer/structs"; | ||||
| import type { LovelaceConfigForm } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
|  | ||||
| const struct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     entity: optional(string()), | ||||
|     name: optional(entityNameStruct), | ||||
|     name: optional(string()), | ||||
|     icon: optional(string()), | ||||
|     attribute: optional(string()), | ||||
|     unit: optional(string()), | ||||
| @@ -23,19 +22,11 @@ const struct = assign( | ||||
|  | ||||
| const SCHEMA = [ | ||||
|   { name: "entity", required: true, selector: { entity: {} } }, | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   { | ||||
|     type: "grid", | ||||
|     name: "", | ||||
|     schema: [ | ||||
|       { name: "name", selector: { text: {} } }, | ||||
|       { | ||||
|         name: "icon", | ||||
|         selector: { | ||||
| @@ -63,7 +54,7 @@ const SCHEMA = [ | ||||
|  | ||||
| const entityCardConfigForm: LovelaceConfigForm = { | ||||
|   schema: SCHEMA, | ||||
|   assertConfig: (config) => assert(config, struct), | ||||
|   assertConfig: (config: EntityCardConfig) => assert(config, struct), | ||||
|   computeLabel: (schema: HaFormSchema, localize: LocalizeFunc) => { | ||||
|     if (schema.name === "theme") { | ||||
|       return `${localize( | ||||
|   | ||||
| @@ -14,10 +14,8 @@ import { | ||||
|   string, | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity_attributes"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import { DEFAULT_MAX, DEFAULT_MIN } from "../../cards/hui-gauge-card"; | ||||
| import type { GaugeCardConfig } from "../../cards/types"; | ||||
| @@ -25,7 +23,7 @@ import type { UiAction } from "../../components/hui-action-editor"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { actionConfigStruct } from "../structs/action-struct"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
| import { NON_NUMERIC_ATTRIBUTES } from "../../../../data/entity_attributes"; | ||||
|  | ||||
| const TAP_ACTIONS: UiAction[] = [ | ||||
|   "more-info", | ||||
| @@ -45,7 +43,7 @@ const gaugeSegmentStruct = object({ | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     name: optional(entityNameStruct), | ||||
|     name: optional(string()), | ||||
|     entity: optional(string()), | ||||
|     attribute: optional(string()), | ||||
|     unit: optional(string()), | ||||
| @@ -100,15 +98,13 @@ export class HuiGaugeCardEditor | ||||
|           }, | ||||
|         }, | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|           name: "", | ||||
|           type: "grid", | ||||
|           schema: [ | ||||
|             { name: "name", selector: { text: {} } }, | ||||
|             { name: "unit", selector: { text: {} } }, | ||||
|           ], | ||||
|         }, | ||||
|         { name: "unit", selector: { text: {} } }, | ||||
|         { name: "theme", selector: { theme: {} } }, | ||||
|         { | ||||
|           name: "", | ||||
|   | ||||
| @@ -148,10 +148,10 @@ export class HuiHeadingBadgesEditor extends LitElement { | ||||
|           .hass=${this.hass} | ||||
|           id="input" | ||||
|           .placeholder=${this.hass.localize( | ||||
|             "ui.components.entity.entity-picker.choose_entity" | ||||
|             "ui.components.target-picker.add_entity_id" | ||||
|           )} | ||||
|           .searchLabel=${this.hass.localize( | ||||
|             "ui.components.entity.entity-picker.choose_entity" | ||||
|             "ui.components.target-picker.add_entity_id" | ||||
|           )} | ||||
|           @value-changed=${this._entityPicked} | ||||
|           @click=${preventDefault} | ||||
|   | ||||
| @@ -14,7 +14,6 @@ import { | ||||
| } from "superstruct"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-expansion-panel"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { | ||||
| @@ -30,7 +29,6 @@ import type { | ||||
| import type { HumidifierCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
| import type { EditDetailElementEvent, EditSubElementEvent } from "../types"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import "./hui-card-features-editor"; | ||||
| @@ -45,7 +43,7 @@ const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     entity: optional(string()), | ||||
|     name: optional(entityNameStruct), | ||||
|     name: optional(string()), | ||||
|     theme: optional(string()), | ||||
|     show_current_as_primary: optional(boolean()), | ||||
|     features: optional(array(any())), | ||||
| @@ -58,19 +56,13 @@ const SCHEMA = [ | ||||
|     required: true, | ||||
|     selector: { entity: { domain: "humidifier" } }, | ||||
|   }, | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   { | ||||
|     type: "grid", | ||||
|     name: "", | ||||
|     schema: [{ name: "theme", selector: { theme: {} } }], | ||||
|     schema: [ | ||||
|       { name: "name", selector: { text: {} } }, | ||||
|       { name: "theme", selector: { theme: {} } }, | ||||
|     ], | ||||
|   }, | ||||
|   { | ||||
|     name: "show_current_as_primary", | ||||
|   | ||||
| @@ -4,7 +4,6 @@ import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { mdiGestureTap } from "@mdi/js"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { SchemaUnion } from "../../../../components/ha-form/types"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| @@ -12,13 +11,12 @@ import type { LightCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { actionConfigStruct } from "../structs/action-struct"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     name: optional(entityNameStruct), | ||||
|     name: optional(string()), | ||||
|     entity: optional(string()), | ||||
|     theme: optional(string()), | ||||
|     icon: optional(string()), | ||||
| @@ -34,19 +32,11 @@ const SCHEMA = [ | ||||
|     required: true, | ||||
|     selector: { entity: { domain: "light" } }, | ||||
|   }, | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   { | ||||
|     type: "grid", | ||||
|     name: "", | ||||
|     schema: [ | ||||
|       { name: "name", selector: { text: {} } }, | ||||
|       { | ||||
|         name: "icon", | ||||
|         selector: { | ||||
|   | ||||
| @@ -22,8 +22,8 @@ import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { DEFAULT_HOURS_TO_SHOW } from "../../cards/hui-logbook-card"; | ||||
| import { targetStruct } from "../../../../data/script"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "../../../../components/entity/ha-entity-picker"; | ||||
| import { getSensorNumericDeviceClasses } from "../../../../data/sensor"; | ||||
| import type { HaEntityPickerEntityFilterFunc } from "../../../../data/entity"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   | ||||
| @@ -2,45 +2,23 @@ import { html, LitElement, nothing } from "lit"; | ||||
| import { customElement, property, state } from "lit/decorators"; | ||||
| import { assert, assign, object, optional, string } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { | ||||
|   HaFormSchema, | ||||
|   SchemaUnion, | ||||
| } from "../../../../components/ha-form/types"; | ||||
| import "../../../../components/entity/ha-entity-picker"; | ||||
| import "../../../../components/ha-theme-picker"; | ||||
| import type { HomeAssistant } from "../../../../types"; | ||||
| import type { MediaControlCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
| import type { EditorTarget, EntitiesEditorEvent } from "../types"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     entity: optional(string()), | ||||
|     name: optional(entityNameStruct), | ||||
|     theme: optional(string()), | ||||
|   }) | ||||
| ); | ||||
|  | ||||
| const SCHEMA = [ | ||||
|   { | ||||
|     name: "entity", | ||||
|     required: true, | ||||
|     selector: { entity: { domain: "media_player" } }, | ||||
|   }, | ||||
|   { | ||||
|     name: "name", | ||||
|     selector: { | ||||
|       entity_name: { | ||||
|         default_name: DEFAULT_ENTITY_NAME, | ||||
|       }, | ||||
|     }, | ||||
|     context: { entity: "entity" }, | ||||
|   }, | ||||
|   { name: "theme", selector: { theme: {} } }, | ||||
| ] as const satisfies readonly HaFormSchema[]; | ||||
| const includeDomains = ["media_player"]; | ||||
|  | ||||
| @customElement("hui-media-control-card-editor") | ||||
| export class HuiMediaControlCardEditor | ||||
| @@ -56,40 +34,69 @@ export class HuiMediaControlCardEditor | ||||
|     this._config = config; | ||||
|   } | ||||
|  | ||||
|   get _entity(): string { | ||||
|     return this._config!.entity || ""; | ||||
|   } | ||||
|  | ||||
|   get _theme(): string { | ||||
|     return this._config!.theme || ""; | ||||
|   } | ||||
|  | ||||
|   protected render() { | ||||
|     if (!this.hass || !this._config) { | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${this._config} | ||||
|         .schema=${SCHEMA} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
|       ></ha-form> | ||||
|       <div class="card-config"> | ||||
|         <ha-entity-picker | ||||
|           .label=${this.hass.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.entity" | ||||
|           )} | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._entity} | ||||
|           .configValue=${"entity"} | ||||
|           .includeDomains=${includeDomains} | ||||
|           .required=${true} | ||||
|           @value-changed=${this._valueChanged} | ||||
|           allow-custom-entity | ||||
|         ></ha-entity-picker> | ||||
|         <ha-theme-picker | ||||
|           .label=${`${this.hass!.localize( | ||||
|             "ui.panel.lovelace.editor.card.generic.theme" | ||||
|           )} (${this.hass!.localize( | ||||
|             "ui.panel.lovelace.editor.card.config.optional" | ||||
|           )})`} | ||||
|           .hass=${this.hass} | ||||
|           .value=${this._theme} | ||||
|           .configValue=${"theme"} | ||||
|           @value-changed=${this._valueChanged} | ||||
|         ></ha-theme-picker> | ||||
|       </div> | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     fireEvent(this, "config-changed", { config: ev.detail.value }); | ||||
|   } | ||||
|  | ||||
|   private _computeLabelCallback = (schema: SchemaUnion<typeof SCHEMA>) => { | ||||
|     if (schema.name === "theme") { | ||||
|       return `${this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.generic.theme" | ||||
|       )} (${this.hass!.localize( | ||||
|         "ui.panel.lovelace.editor.card.config.optional" | ||||
|       )})`; | ||||
|   private _valueChanged(ev: EntitiesEditorEvent): void { | ||||
|     if (!this._config || !this.hass) { | ||||
|       return; | ||||
|     } | ||||
|     return this.hass!.localize( | ||||
|       `ui.panel.lovelace.editor.card.generic.${schema.name}` | ||||
|     ); | ||||
|   }; | ||||
|  | ||||
|   static styles = configElementStyle; | ||||
|     const target = ev.target! as EditorTarget; | ||||
|     if (this[`_${target.configValue}`] === target.value) { | ||||
|       return; | ||||
|     } | ||||
|     if (target.configValue) { | ||||
|       if (target.value === "") { | ||||
|         this._config = { ...this._config }; | ||||
|         delete this._config[target.configValue!]; | ||||
|       } else { | ||||
|         this._config = { | ||||
|           ...this._config, | ||||
|           [target.configValue!]: target.value, | ||||
|         }; | ||||
|       } | ||||
|     } | ||||
|     fireEvent(this, "config-changed", { config: this._config }); | ||||
|   } | ||||
| } | ||||
|  | ||||
| declare global { | ||||
|   | ||||
| @@ -11,7 +11,6 @@ import { | ||||
|   optional, | ||||
|   string, | ||||
|   type, | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import type { HASSDomEvent } from "../../../../common/dom/fire_event"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| @@ -38,14 +37,14 @@ const genericElementConfigStruct = type({ | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     image: optional(union([string(), object()])), | ||||
|     image: optional(string()), | ||||
|     camera_image: optional(string()), | ||||
|     camera_view: optional(string()), | ||||
|     elements: array(genericElementConfigStruct), | ||||
|     title: optional(string()), | ||||
|     state_filter: optional(any()), | ||||
|     theme: optional(string()), | ||||
|     dark_mode_image: optional(union([string(), object()])), | ||||
|     dark_mode_image: optional(string()), | ||||
|     dark_mode_filter: optional(any()), | ||||
|   }) | ||||
| ); | ||||
| @@ -77,34 +76,8 @@ export class HuiPictureElementsCardEditor | ||||
|           ), | ||||
|           schema: [ | ||||
|             { name: "title", selector: { text: {} } }, | ||||
|             { | ||||
|               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: "dark_mode_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: "image", selector: { image: {} } }, | ||||
|             { name: "dark_mode_image", selector: { image: {} } }, | ||||
|             { | ||||
|               name: "camera_image", | ||||
|               selector: { entity: { domain: "camera" } }, | ||||
| @@ -151,7 +124,7 @@ export class HuiPictureElementsCardEditor | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${this._processData(this._config)} | ||||
|         .data=${this._config} | ||||
|         .schema=${this._schema(this.hass.localize)} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         @value-changed=${this._formChanged} | ||||
| @@ -165,16 +138,6 @@ export class HuiPictureElementsCardEditor | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _processData = memoizeOne((config: PictureElementsCardConfig) => ({ | ||||
|     ...config, | ||||
|     ...(typeof config.image === "string" | ||||
|       ? { image: { media_content_id: config.image } } | ||||
|       : {}), | ||||
|     ...(typeof config.dark_mode_image === "string" | ||||
|       ? { dark_mode_image: { media_content_id: config.dark_mode_image } } | ||||
|       : {}), | ||||
|   })); | ||||
|  | ||||
|   private _formChanged(ev: CustomEvent): void { | ||||
|     ev.stopPropagation(); | ||||
|     if (!this._config || !this.hass) { | ||||
|   | ||||
| @@ -11,11 +11,9 @@ import { | ||||
|   object, | ||||
|   optional, | ||||
|   string, | ||||
|   union, | ||||
| } from "superstruct"; | ||||
| import { fireEvent } from "../../../../common/dom/fire_event"; | ||||
| import { computeDomain } from "../../../../common/entity/compute_domain"; | ||||
| import { DEFAULT_ENTITY_NAME } from "../../../../common/entity/compute_entity_name_display"; | ||||
| import type { LocalizeFunc } from "../../../../common/translations/localize"; | ||||
| import "../../../../components/ha-form/ha-form"; | ||||
| import type { | ||||
| @@ -28,15 +26,14 @@ import type { PictureEntityCardConfig } from "../../cards/types"; | ||||
| import type { LovelaceCardEditor } from "../../types"; | ||||
| import { actionConfigStruct } from "../structs/action-struct"; | ||||
| import { baseLovelaceCardConfig } from "../structs/base-card-struct"; | ||||
| import { entityNameStruct } from "../structs/entity-name-struct"; | ||||
| import { configElementStyle } from "./config-elements-style"; | ||||
|  | ||||
| const cardConfigStruct = assign( | ||||
|   baseLovelaceCardConfig, | ||||
|   object({ | ||||
|     entity: optional(string()), | ||||
|     image: optional(union([string(), object()])), | ||||
|     name: optional(entityNameStruct), | ||||
|     image: optional(string()), | ||||
|     name: optional(string()), | ||||
|     camera_image: optional(string()), | ||||
|     camera_view: optional(enums(["auto", "live"])), | ||||
|     aspect_ratio: optional(string()), | ||||
| @@ -68,29 +65,8 @@ export class HuiPictureEntityCardEditor | ||||
|     (localize: LocalizeFunc) => | ||||
|       [ | ||||
|         { name: "entity", required: true, selector: { entity: {} } }, | ||||
|         { | ||||
|           name: "name", | ||||
|           selector: { | ||||
|             entity_name: { | ||||
|               default_name: DEFAULT_ENTITY_NAME, | ||||
|             }, | ||||
|           }, | ||||
|           context: { entity: "entity" }, | ||||
|         }, | ||||
|         { | ||||
|           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: "name", selector: { text: {} } }, | ||||
|         { name: "image", selector: { image: {} } }, | ||||
|         { name: "camera_image", selector: { entity: { domain: "camera" } } }, | ||||
|         { | ||||
|           name: "", | ||||
| @@ -183,11 +159,21 @@ export class HuiPictureEntityCardEditor | ||||
|       return nothing; | ||||
|     } | ||||
|  | ||||
|     const data = { | ||||
|       show_state: true, | ||||
|       show_name: true, | ||||
|       camera_view: "auto", | ||||
|       fit_mode: "cover", | ||||
|       ...this._config, | ||||
|     }; | ||||
|  | ||||
|     const schema = this._schema(this.hass.localize); | ||||
|  | ||||
|     return html` | ||||
|       <ha-form | ||||
|         .hass=${this.hass} | ||||
|         .data=${this._processData(this._config)} | ||||
|         .schema=${this._schema(this.hass.localize)} | ||||
|         .data=${data} | ||||
|         .schema=${schema} | ||||
|         .computeLabel=${this._computeLabelCallback} | ||||
|         .computeHelper=${this._computeHelperCallback} | ||||
|         @value-changed=${this._valueChanged} | ||||
| @@ -196,17 +182,6 @@ export class HuiPictureEntityCardEditor | ||||
|     `; | ||||
|   } | ||||
|  | ||||
|   private _processData = memoizeOne((config: PictureEntityCardConfig) => ({ | ||||
|     show_state: true, | ||||
|     show_name: true, | ||||
|     camera_view: "auto", | ||||
|     fit_mode: "cover", | ||||
|     ...config, | ||||
|     ...(typeof config.image === "string" | ||||
|       ? { image: { media_content_id: config.image } } | ||||
|       : {}), | ||||
|   })); | ||||
|  | ||||
|   private _valueChanged(ev: CustomEvent): void { | ||||
|     const config = ev.detail.value; | ||||
|     if ( | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user