diff --git a/src/data/config_entries.ts b/src/data/config_entries.ts index 25e49636af..f4c4cea7ca 100644 --- a/src/data/config_entries.ts +++ b/src/data/config_entries.ts @@ -1,6 +1,6 @@ -import { UnsubscribeFunc } from "home-assistant-js-websocket"; -import { HomeAssistant } from "../types"; -import { IntegrationType } from "./integration"; +import type { UnsubscribeFunc } from "home-assistant-js-websocket"; +import type { HomeAssistant } from "../types"; +import type { IntegrationManifest, IntegrationType } from "./integration"; export interface ConfigEntry { entry_id: string; @@ -143,3 +143,23 @@ export const enableConfigEntry = (hass: HomeAssistant, configEntryId: string) => entry_id: configEntryId, disabled_by: null, }); + +export const sortConfigEntries = ( + configEntries: ConfigEntry[], + manifestLookup: { [domain: string]: IntegrationManifest } +): ConfigEntry[] => { + const sortedConfigEntries = [...configEntries]; + + const getScore = (entry: ConfigEntry) => { + const manifest = manifestLookup[entry.domain] as + | IntegrationManifest + | undefined; + const isHelper = manifest?.integration_type === "helper"; + return isHelper ? -1 : 1; + }; + + const configEntriesCompare = (a: ConfigEntry, b: ConfigEntry) => + getScore(b) - getScore(a); + + return sortedConfigEntries.sort(configEntriesCompare); +}; diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index e55733aa3f..d2b4d27445 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -41,6 +41,7 @@ import { ConfigEntry, disableConfigEntry, DisableConfigEntryResult, + sortConfigEntries, } from "../../../data/config_entries"; import { computeDeviceName, @@ -60,7 +61,7 @@ import { findBatteryEntity, updateEntityRegistryEntry, } from "../../../data/entity_registry"; -import { domainToName } from "../../../data/integration"; +import { IntegrationManifest, domainToName } from "../../../data/integration"; import { SceneEntities, showSceneEditor } from "../../../data/scene"; import { findRelated, RelatedResult } from "../../../data/search"; import { @@ -115,6 +116,8 @@ export class HaConfigDevicePage extends LitElement { @property({ attribute: false }) public areas!: AreaRegistryEntry[]; + @property({ attribute: false }) public manifests!: IntegrationManifest[]; + @property() public deviceId!: string; @property({ type: Boolean, reflect: true }) public narrow!: boolean; @@ -145,14 +148,24 @@ export class HaConfigDevicePage extends LitElement { ); private _integrations = memoizeOne( - (device: DeviceRegistryEntry, entries: ConfigEntry[]): ConfigEntry[] => { + ( + device: DeviceRegistryEntry, + entries: ConfigEntry[], + manifests: IntegrationManifest[] + ): ConfigEntry[] => { const entryLookup: { [entryId: string]: ConfigEntry } = {}; for (const entry of entries) { entryLookup[entry.entry_id] = entry; } - return device.config_entries - .map((entry) => entryLookup[entry]) - .filter(Boolean); + const manifestLookup: { [domain: string]: IntegrationManifest } = {}; + for (const manifest of manifests) { + manifestLookup[manifest.domain] = manifest; + } + const deviceEntries = device.config_entries + .filter((entId) => entId in entryLookup) + .map((entry) => entryLookup[entry]); + + return sortConfigEntries(deviceEntries, manifestLookup); } ); @@ -292,7 +305,11 @@ export class HaConfigDevicePage extends LitElement { } const deviceName = computeDeviceName(device, this.hass); - const integrations = this._integrations(device, this.entries); + const integrations = this._integrations( + device, + this.entries, + this.manifests + ); const entities = this._entities(this.deviceId, this.entities); const entitiesByCategory = this._entitiesByCategory(entities); const batteryEntity = this._batteryEntity(entities); @@ -920,7 +937,7 @@ export class HaConfigDevicePage extends LitElement { } let links = await Promise.all( - this._integrations(device, this.entries).map( + this._integrations(device, this.entries, this.manifests).map( async (entry): Promise => { if (entry.state !== "loaded") { return false; @@ -983,50 +1000,55 @@ export class HaConfigDevicePage extends LitElement { } const buttons: DeviceAction[] = []; - this._integrations(device, this.entries).forEach((entry) => { - if (entry.state !== "loaded" || !entry.supports_remove_device) { - return; + this._integrations(device, this.entries, this.manifests).forEach( + (entry) => { + if (entry.state !== "loaded" || !entry.supports_remove_device) { + return; + } + buttons.push({ + action: async () => { + const confirmed = await showConfirmationDialog(this, { + text: + this._integrations(device, this.entries, this.manifests) + .length > 1 + ? this.hass.localize( + `ui.panel.config.devices.confirm_delete_integration`, + { + integration: domainToName( + this.hass.localize, + entry.domain + ), + } + ) + : this.hass.localize( + `ui.panel.config.devices.confirm_delete` + ), + }); + + if (!confirmed) { + return; + } + + await removeConfigEntryFromDevice( + this.hass!, + this.deviceId, + entry.entry_id + ); + }, + classes: "warning", + icon: mdiDelete, + label: + this._integrations(device, this.entries, this.manifests).length > 1 + ? this.hass.localize( + `ui.panel.config.devices.delete_device_integration`, + { + integration: domainToName(this.hass.localize, entry.domain), + } + ) + : this.hass.localize(`ui.panel.config.devices.delete_device`), + }); } - buttons.push({ - action: async () => { - const confirmed = await showConfirmationDialog(this, { - text: - this._integrations(device, this.entries).length > 1 - ? this.hass.localize( - `ui.panel.config.devices.confirm_delete_integration`, - { - integration: domainToName( - this.hass.localize, - entry.domain - ), - } - ) - : this.hass.localize(`ui.panel.config.devices.confirm_delete`), - }); - - if (!confirmed) { - return; - } - - await removeConfigEntryFromDevice( - this.hass!, - this.deviceId, - entry.entry_id - ); - }, - classes: "warning", - icon: mdiDelete, - label: - this._integrations(device, this.entries).length > 1 - ? this.hass.localize( - `ui.panel.config.devices.delete_device_integration`, - { - integration: domainToName(this.hass.localize, entry.domain), - } - ) - : this.hass.localize(`ui.panel.config.devices.delete_device`), - }); - }); + ); if (buttons.length > 0) { this._deleteButtons = buttons; @@ -1061,9 +1083,11 @@ export class HaConfigDevicePage extends LitElement { }); } - const domains = this._integrations(device, this.entries).map( - (int) => int.domain - ); + const domains = this._integrations( + device, + this.entries, + this.manifests + ).map((int) => int.domain); if (domains.includes("mqtt")) { const mqtt = await import( @@ -1103,9 +1127,11 @@ export class HaConfigDevicePage extends LitElement { const deviceAlerts: DeviceAlert[] = []; - const domains = this._integrations(device, this.entries).map( - (int) => int.domain - ); + const domains = this._integrations( + device, + this.entries, + this.manifests + ).map((int) => int.domain); if (domains.includes("zwave_js")) { const zwave = await import( diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index f6f88f3246..576aa4413f 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -25,7 +25,7 @@ import "../../../components/ha-check-list-item"; import "../../../components/ha-fab"; import "../../../components/ha-icon-button"; import { AreaRegistryEntry } from "../../../data/area_registry"; -import { ConfigEntry } from "../../../data/config_entries"; +import { ConfigEntry, sortConfigEntries } from "../../../data/config_entries"; import { computeDeviceName, DeviceEntityLookup, @@ -36,7 +36,7 @@ import { findBatteryChargingEntity, findBatteryEntity, } from "../../../data/entity_registry"; -import { domainToName } from "../../../data/integration"; +import { IntegrationManifest, domainToName } from "../../../data/integration"; import "../../../layouts/hass-tabs-subpage-data-table"; import { haStyle } from "../../../resources/styles"; import { HomeAssistant, Route } from "../../../types"; @@ -68,6 +68,8 @@ export class HaConfigDeviceDashboard extends LitElement { @property() public areas!: AreaRegistryEntry[]; + @property() public manifests!: IntegrationManifest[]; + @property() public route!: Route; @state() private _searchParms = new URLSearchParams(window.location.search); @@ -149,6 +151,7 @@ export class HaConfigDeviceDashboard extends LitElement { entries: ConfigEntry[], entities: EntityRegistryEntry[], areas: AreaRegistryEntry[], + manifests: IntegrationManifest[], filters: URLSearchParams, showDisabled: boolean, localize: LocalizeFunc @@ -186,6 +189,11 @@ export class HaConfigDeviceDashboard extends LitElement { areaLookup[area.area_id] = area; } + const manifestLookup: { [domain: string]: IntegrationManifest } = {}; + for (const manifest of manifests) { + manifestLookup[manifest.domain] = manifest; + } + let filterConfigEntry: ConfigEntry | undefined; const filteredDomains = new Set(); @@ -217,47 +225,51 @@ export class HaConfigDeviceDashboard extends LitElement { outputDevices = outputDevices.filter((device) => !device.disabled_by); } - outputDevices = outputDevices.map((device) => ({ - ...device, - name: computeDeviceName( - device, - this.hass, - deviceEntityLookup[device.id] - ), - model: - device.model || - `<${localize("ui.panel.config.devices.data_table.unknown")}>`, - manufacturer: - device.manufacturer || - `<${localize("ui.panel.config.devices.data_table.unknown")}>`, - area: - device.area_id && areaLookup[device.area_id] - ? areaLookup[device.area_id].name - : "—", - integration: device.config_entries.length - ? device.config_entries - .filter((entId) => entId in entryLookup) - .map( - (entId) => - localize(`component.${entryLookup[entId].domain}.title`) || - entryLookup[entId].domain - ) - .join(", ") - : this.hass.localize( - "ui.panel.config.devices.data_table.no_integration" - ), - domains: device.config_entries - .filter((entId) => entId in entryLookup) - .map((entId) => entryLookup[entId].domain), - battery_entity: [ - this._batteryEntity(device.id, deviceEntityLookup), - this._batteryChargingEntity(device.id, deviceEntityLookup), - ], - battery_level: - this.hass.states[ - this._batteryEntity(device.id, deviceEntityLookup) || "" - ]?.state, - })); + outputDevices = outputDevices.map((device) => { + const deviceEntries = sortConfigEntries( + device.config_entries + .filter((entId) => entId in entryLookup) + .map((entId) => entryLookup[entId]), + manifestLookup + ); + return { + ...device, + name: computeDeviceName( + device, + this.hass, + deviceEntityLookup[device.id] + ), + model: + device.model || + `<${localize("ui.panel.config.devices.data_table.unknown")}>`, + manufacturer: + device.manufacturer || + `<${localize("ui.panel.config.devices.data_table.unknown")}>`, + area: + device.area_id && areaLookup[device.area_id] + ? areaLookup[device.area_id].name + : "—", + integration: deviceEntries.length + ? deviceEntries + .map( + (entry) => + localize(`component.${entry.domain}.title`) || entry.domain + ) + .join(", ") + : this.hass.localize( + "ui.panel.config.devices.data_table.no_integration" + ), + domains: deviceEntries.map((entry) => entry.domain), + battery_entity: [ + this._batteryEntity(device.id, deviceEntityLookup), + this._batteryChargingEntity(device.id, deviceEntityLookup), + ], + battery_level: + this.hass.states[ + this._batteryEntity(device.id, deviceEntityLookup) || "" + ]?.state, + }; + }); this._numHiddenDevices = startLength - outputDevices.length; return { @@ -429,6 +441,7 @@ export class HaConfigDeviceDashboard extends LitElement { this.entries, this.entities, this.areas, + this.manifests, this._searchParms, this._showDisabled, this.hass.localize @@ -565,6 +578,7 @@ export class HaConfigDeviceDashboard extends LitElement { this.entries, this.entities, this.areas, + this.manifests, this._searchParms, this._showDisabled, this.hass.localize diff --git a/src/panels/config/devices/ha-config-devices.ts b/src/panels/config/devices/ha-config-devices.ts index fe5625565c..08a5e0188d 100644 --- a/src/panels/config/devices/ha-config-devices.ts +++ b/src/panels/config/devices/ha-config-devices.ts @@ -14,6 +14,10 @@ import { EntityRegistryEntry, subscribeEntityRegistry, } from "../../../data/entity_registry"; +import { + IntegrationManifest, + fetchIntegrationManifests, +} from "../../../data/integration"; import { HassRouterPage, RouterOptions, @@ -47,6 +51,8 @@ class HaConfigDevices extends HassRouterPage { @state() private _configEntries: ConfigEntry[] = []; + @state() private _manifests: IntegrationManifest[] = []; + @state() private _entityRegistryEntries: EntityRegistryEntry[] = []; @@ -99,6 +105,7 @@ class HaConfigDevices extends HassRouterPage { pageEl.entities = this._entityRegistryEntries; pageEl.entries = this._configEntries; + pageEl.manifests = this._manifests; pageEl.devices = this._deviceRegistryEntries; pageEl.areas = this._areas; pageEl.narrow = this.narrow; @@ -111,6 +118,10 @@ class HaConfigDevices extends HassRouterPage { getConfigEntries(this.hass).then((configEntries) => { this._configEntries = configEntries; }); + fetchIntegrationManifests(this.hass).then((manifests) => { + this._manifests = manifests; + }); + if (this._unsubs) { return; }