diff --git a/src/components/data-table/ha-data-table.ts b/src/components/data-table/ha-data-table.ts index 7765a95ab0..afa109088e 100644 --- a/src/components/data-table/ha-data-table.ts +++ b/src/components/data-table/ha-data-table.ts @@ -73,7 +73,7 @@ export interface DataTabelSortColumnData { export interface DataTabelColumnData extends DataTabelSortColumnData { title: string; type?: "numeric"; - template?: (data: any) => TemplateResult; + template?: (data: any, row: DataTabelRowData) => TemplateResult; } export interface DataTabelRowData { @@ -254,7 +254,7 @@ export class HaDataTable extends BaseElement { })}" > ${column.template - ? column.template(row[key]) + ? column.template(row[key], row) : row[key]} `; diff --git a/src/panels/config/devices/device-detail/ha-device-card.js b/src/panels/config/devices/device-detail/ha-device-card.js index 4ef8dfad9c..94ee330e7e 100644 --- a/src/panels/config/devices/device-detail/ha-device-card.js +++ b/src/panels/config/devices/device-detail/ha-device-card.js @@ -1,35 +1,17 @@ -import "@polymer/paper-item/paper-icon-item"; -import "@polymer/paper-item/paper-item-body"; -import "@polymer/paper-dropdown-menu/paper-dropdown-menu"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-listbox/paper-listbox"; import { html } from "@polymer/polymer/lib/utils/html-tag"; import { PolymerElement } from "@polymer/polymer/polymer-element"; import "../../../../components/ha-card"; -import "../../../../layouts/hass-subpage"; import { EventsMixin } from "../../../../mixins/events-mixin"; import LocalizeMixin from "../../../../mixins/localize-mixin"; -import { computeStateName } from "../../../../common/entity/compute_state_name"; -import "../../../../components/entity/state-badge"; import { compare } from "../../../../common/string/compare"; -import { - subscribeDeviceRegistry, - updateDeviceRegistryEntry, -} from "../../../../data/device_registry"; -import { subscribeAreaRegistry } from "../../../../data/area_registry"; +import { updateDeviceRegistryEntry } from "../../../../data/device_registry"; import { loadDeviceRegistryDetailDialog, showDeviceRegistryDetailDialog, } from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail"; -function computeEntityName(hass, entity) { - if (entity.name) return entity.name; - const state = hass.states[entity.entity_id]; - return state ? computeStateName(state) : null; -} - /* * @appliesMixin EventsMixin */ @@ -37,10 +19,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { static get template() { return html` - - - [[_deviceName(device)]] - - - [[device.model]] @@ -122,27 +86,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { - - - - - - - - [[_computeEntityName(entity, hass)]] - [[entity.entity_id]] - - - - - `; } @@ -152,14 +95,11 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { device: Object, devices: Array, areas: Array, - entities: Array, hass: Object, narrow: { type: Boolean, reflectToAttribute: true, }, - hideSettings: { type: Boolean, value: false }, - hideEntities: { type: Boolean, value: false }, _childDevices: { type: Array, computed: "_computeChildDevices(device, devices)", @@ -172,30 +112,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { loadDeviceRegistryDetailDialog(); } - connectedCallback() { - super.connectedCallback(); - this._unsubAreas = subscribeAreaRegistry(this.hass.connection, (areas) => { - this._areas = areas; - }); - this._unsubDevices = subscribeDeviceRegistry( - this.hass.connection, - (devices) => { - this.devices = devices; - this.device = devices.find((device) => device.id === this.device.id); - } - ); - } - - disconnectedCallback() { - super.disconnectedCallback(); - if (this._unsubAreas) { - this._unsubAreas(); - } - if (this._unsubDevices) { - this._unsubDevices(); - } - } - _computeArea(areas, device) { if (!areas || !device || !device.area_id) { return "No Area"; @@ -210,30 +126,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) { .sort((dev1, dev2) => compare(dev1.name, dev2.name)); } - _computeDeviceEntities(hass, device, entities) { - return entities - .filter((entity) => entity.device_id === device.id) - .sort((ent1, ent2) => - compare( - computeEntityName(hass, ent1) || `zzz${ent1.entity_id}`, - computeEntityName(hass, ent2) || `zzz${ent2.entity_id}` - ) - ); - } - - _computeStateObj(entity, hass) { - return hass.states[entity.entity_id]; - } - - _computeEntityName(entity, hass) { - return ( - computeEntityName(hass, entity) || - `(${this.localize( - "ui.panel.config.integrations.config_entry.entity_unavailable" - )})` - ); - } - _deviceName(device) { return device.name_by_user || device.name; } diff --git a/src/panels/config/devices/ha-config-device-page.ts b/src/panels/config/devices/ha-config-device-page.ts index 07a53b6d8d..e238d511b5 100644 --- a/src/panels/config/devices/ha-config-device-page.ts +++ b/src/panels/config/devices/ha-config-device-page.ts @@ -13,6 +13,7 @@ import "../../../layouts/hass-subpage"; import "../../../layouts/hass-error-screen"; import "../ha-config-section"; +import "./device-detail/ha-device-card"; import "./device-detail/ha-device-triggers-card"; import "./device-detail/ha-device-conditions-card"; import "./device-detail/ha-device-actions-card"; @@ -144,9 +145,6 @@ export class HaConfigDevicePage extends LitElement { .areas=${this.areas} .devices=${this.devices} .device=${device} - .entities=${this.entities} - hide-settings - hide-entities > ${entities.length diff --git a/src/panels/config/devices/ha-config-devices-dashboard.ts b/src/panels/config/devices/ha-config-devices-dashboard.ts index d448859b0f..c60bcabe91 100644 --- a/src/panels/config/devices/ha-config-devices-dashboard.ts +++ b/src/panels/config/devices/ha-config-devices-dashboard.ts @@ -1,19 +1,5 @@ -import "@polymer/paper-tooltip/paper-tooltip"; -import "@material/mwc-button"; -import "@polymer/iron-icon/iron-icon"; -import "@polymer/paper-item/paper-item"; -import "@polymer/paper-item/paper-item-body"; - -import "../../../components/ha-card"; -import "../../../components/data-table/ha-data-table"; -import "../../../components/entity/ha-state-icon"; import "../../../layouts/hass-subpage"; -import "../../../resources/ha-style"; -import "../../../components/ha-icon-next"; - -import "../ha-config-section"; - -import memoizeOne from "memoize-one"; +import "./ha-devices-data-table"; import { LitElement, @@ -21,33 +7,14 @@ import { TemplateResult, property, customElement, + CSSResult, + css, } from "lit-element"; import { HomeAssistant } from "../../../types"; -// tslint:disable-next-line -import { - DataTabelColumnContainer, - RowClickedEvent, - DataTabelRowData, -} from "../../../components/data-table/ha-data-table"; -// tslint:disable-next-line import { DeviceRegistryEntry } from "../../../data/device_registry"; import { EntityRegistryEntry } from "../../../data/entity_registry"; import { ConfigEntry } from "../../../data/config_entries"; import { AreaRegistryEntry } from "../../../data/area_registry"; -import { navigate } from "../../../common/navigate"; -import { LocalizeFunc } from "../../../common/translations/localize"; -import { computeStateName } from "../../../common/entity/compute_state_name"; - -interface DeviceRowData extends DeviceRegistryEntry { - device?: DeviceRowData; - area?: string; - integration?: string; - battery_entity?: string; -} - -interface DeviceEntityLookup { - [deviceId: string]: EntityRegistryEntry[]; -} @customElement("ha-config-devices-dashboard") export class HaConfigDeviceDashboard extends LitElement { @@ -59,234 +26,35 @@ export class HaConfigDeviceDashboard extends LitElement { @property() public areas!: AreaRegistryEntry[]; @property() public domain!: string; - private _devices = memoizeOne( - ( - devices: DeviceRegistryEntry[], - entries: ConfigEntry[], - entities: EntityRegistryEntry[], - areas: AreaRegistryEntry[], - domain: string, - localize: LocalizeFunc - ) => { - // Some older installations might have devices pointing at invalid entryIDs - // So we guard for that. - - let outputDevices: DeviceRowData[] = devices; - - const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; - for (const device of devices) { - deviceLookup[device.id] = device; - } - - const deviceEntityLookup: DeviceEntityLookup = {}; - for (const entity of entities) { - if (!entity.device_id) { - continue; - } - if (!(entity.device_id in deviceEntityLookup)) { - deviceEntityLookup[entity.device_id] = []; - } - deviceEntityLookup[entity.device_id].push(entity); - } - - const entryLookup: { [entryId: string]: ConfigEntry } = {}; - for (const entry of entries) { - entryLookup[entry.entry_id] = entry; - } - - const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; - for (const area of areas) { - areaLookup[area.area_id] = area; - } - - if (domain) { - outputDevices = outputDevices.filter((device) => - device.config_entries.find( - (entryId) => - entryId in entryLookup && entryLookup[entryId].domain === domain - ) - ); - } - - outputDevices = outputDevices.map((device) => { - return { - ...device, - name: - device.name_by_user || - device.name || - this._fallbackDeviceName(device.id, deviceEntityLookup) || - "No name", - model: device.model || "", - manufacturer: device.manufacturer || "", - area: device.area_id ? areaLookup[device.area_id].name : "No area", - integration: device.config_entries.length - ? device.config_entries - .filter((entId) => entId in entryLookup) - .map( - (entId) => - localize( - `component.${entryLookup[entId].domain}.config.title` - ) || entryLookup[entId].domain - ) - .join(", ") - : "No integration", - battery_entity: this._batteryEntity(device.id, deviceEntityLookup), - }; - }); - - return outputDevices; - } - ); - - private _columns = memoizeOne( - (narrow: boolean): DataTabelColumnContainer => - narrow - ? { - device: { - title: "Device", - sortable: true, - filterKey: "name", - filterable: true, - direction: "asc", - template: (device: DeviceRowData) => { - const battery = device.battery_entity - ? this.hass.states[device.battery_entity] - : undefined; - // Have to work on a nice layout for mobile - return html` - ${device.name_by_user || device.name} - ${device.area} | ${device.integration} - ${battery - ? html` - ${battery.state}% - - ` - : ""} - `; - }, - }, - } - : { - device_name: { - title: "Device", - sortable: true, - filterable: true, - direction: "asc", - }, - manufacturer: { - title: "Manufacturer", - sortable: true, - filterable: true, - }, - model: { - title: "Model", - sortable: true, - filterable: true, - }, - area: { - title: "Area", - sortable: true, - filterable: true, - }, - integration: { - title: "Integration", - sortable: true, - filterable: true, - }, - battery: { - title: "Battery", - sortable: true, - type: "numeric", - template: (batteryEntity: string) => { - const battery = batteryEntity - ? this.hass.states[batteryEntity] - : undefined; - return battery - ? html` - ${battery.state}% - - ` - : html` - - - `; - }, - }, - } - ); - protected render(): TemplateResult { return html` - { - // We don't need a lot of this data for mobile view, but kept it for filtering... - const data: DataTabelRowData = { - device_name: device.name, - id: device.id, - manufacturer: device.manufacturer, - model: device.model, - area: device.area, - integration: device.integration, - }; - if (this.narrow) { - data.device = device; - return data; - } - data.battery = device.battery_entity; - return data; - })} - @row-click=${this._handleRowClicked} - > + + + `; } - private _batteryEntity( - deviceId: string, - deviceEntityLookup: DeviceEntityLookup - ): string | undefined { - const batteryEntity = (deviceEntityLookup[deviceId] || []).find( - (entity) => - this.hass.states[entity.entity_id] && - this.hass.states[entity.entity_id].attributes.device_class === "battery" - ); - - return batteryEntity ? batteryEntity.entity_id : undefined; - } - - private _fallbackDeviceName( - deviceId: string, - deviceEntityLookup: DeviceEntityLookup - ): string | undefined { - for (const entity of deviceEntityLookup[deviceId] || []) { - const stateObj = this.hass.states[entity.entity_id]; - if (stateObj) { - return computeStateName(stateObj); + static get styles(): CSSResult { + return css` + .content { + padding: 4px; } - } - - return undefined; - } - - private _handleRowClicked(ev: CustomEvent) { - const deviceId = (ev.detail as RowClickedEvent).id; - navigate(this, `/config/devices/device/${deviceId}`); + ha-devices-data-table { + width: 100%; + } + `; } } diff --git a/src/panels/config/devices/ha-devices-data-table.ts b/src/panels/config/devices/ha-devices-data-table.ts new file mode 100644 index 0000000000..d21e31aaa8 --- /dev/null +++ b/src/panels/config/devices/ha-devices-data-table.ts @@ -0,0 +1,265 @@ +import "../../../components/data-table/ha-data-table"; +import "../../../components/entity/ha-state-icon"; + +import memoizeOne from "memoize-one"; + +import { + LitElement, + html, + TemplateResult, + property, + customElement, +} from "lit-element"; +import { HomeAssistant } from "../../../types"; +// tslint:disable-next-line +import { + DataTabelColumnContainer, + RowClickedEvent, + DataTabelRowData, +} from "../../../components/data-table/ha-data-table"; +// tslint:disable-next-line +import { DeviceRegistryEntry } from "../../../data/device_registry"; +import { EntityRegistryEntry } from "../../../data/entity_registry"; +import { ConfigEntry } from "../../../data/config_entries"; +import { AreaRegistryEntry } from "../../../data/area_registry"; +import { navigate } from "../../../common/navigate"; +import { LocalizeFunc } from "../../../common/translations/localize"; +import { computeStateName } from "../../../common/entity/compute_state_name"; + +export interface DeviceRowData extends DeviceRegistryEntry { + device?: DeviceRowData; + area?: string; + integration?: string; + battery_entity?: string; +} + +export interface DeviceEntityLookup { + [deviceId: string]: EntityRegistryEntry[]; +} + +@customElement("ha-devices-data-table") +export class HaDevicesDataTable extends LitElement { + @property() public hass!: HomeAssistant; + @property() public narrow = false; + @property() public devices!: DeviceRegistryEntry[]; + @property() public entries!: ConfigEntry[]; + @property() public entities!: EntityRegistryEntry[]; + @property() public areas!: AreaRegistryEntry[]; + @property() public domain!: string; + + private _devices = memoizeOne( + ( + devices: DeviceRegistryEntry[], + entries: ConfigEntry[], + entities: EntityRegistryEntry[], + areas: AreaRegistryEntry[], + domain: string, + localize: LocalizeFunc + ) => { + // Some older installations might have devices pointing at invalid entryIDs + // So we guard for that. + + let outputDevices: DeviceRowData[] = devices; + + const deviceLookup: { [deviceId: string]: DeviceRegistryEntry } = {}; + for (const device of devices) { + deviceLookup[device.id] = device; + } + + const deviceEntityLookup: DeviceEntityLookup = {}; + for (const entity of entities) { + if (!entity.device_id) { + continue; + } + if (!(entity.device_id in deviceEntityLookup)) { + deviceEntityLookup[entity.device_id] = []; + } + deviceEntityLookup[entity.device_id].push(entity); + } + + const entryLookup: { [entryId: string]: ConfigEntry } = {}; + for (const entry of entries) { + entryLookup[entry.entry_id] = entry; + } + + const areaLookup: { [areaId: string]: AreaRegistryEntry } = {}; + for (const area of areas) { + areaLookup[area.area_id] = area; + } + + if (domain) { + outputDevices = outputDevices.filter((device) => + device.config_entries.find( + (entryId) => + entryId in entryLookup && entryLookup[entryId].domain === domain + ) + ); + } + + outputDevices = outputDevices.map((device) => { + return { + ...device, + name: + device.name_by_user || + device.name || + this._fallbackDeviceName(device.id, deviceEntityLookup) || + "No name", + model: device.model || "", + manufacturer: device.manufacturer || "", + area: device.area_id ? areaLookup[device.area_id].name : "No area", + integration: device.config_entries.length + ? device.config_entries + .filter((entId) => entId in entryLookup) + .map( + (entId) => + localize( + `component.${entryLookup[entId].domain}.config.title` + ) || entryLookup[entId].domain + ) + .join(", ") + : "No integration", + battery_entity: this._batteryEntity(device.id, deviceEntityLookup), + }; + }); + + return outputDevices; + } + ); + + private _columns = memoizeOne( + (narrow: boolean): DataTabelColumnContainer => + narrow + ? { + name: { + title: "Device", + sortable: true, + filterKey: "name", + filterable: true, + direction: "asc", + template: (name, device: DataTabelRowData) => { + const battery = device.battery_entity + ? this.hass.states[device.battery_entity] + : undefined; + // Have to work on a nice layout for mobile + return html` + ${name} + ${device.area} | ${device.integration} + ${battery + ? html` + ${battery.state}% + + ` + : ""} + `; + }, + }, + } + : { + name: { + title: "Device", + sortable: true, + filterable: true, + direction: "asc", + }, + manufacturer: { + title: "Manufacturer", + sortable: true, + filterable: true, + }, + model: { + title: "Model", + sortable: true, + filterable: true, + }, + area: { + title: "Area", + sortable: true, + filterable: true, + }, + integration: { + title: "Integration", + sortable: true, + filterable: true, + }, + battery_entity: { + title: "Battery", + sortable: true, + type: "numeric", + template: (batteryEntity: string) => { + const battery = batteryEntity + ? this.hass.states[batteryEntity] + : undefined; + return battery + ? html` + ${battery.state}% + + ` + : html` + - + `; + }, + }, + } + ); + + protected render(): TemplateResult { + return html` + + `; + } + + private _batteryEntity( + deviceId: string, + deviceEntityLookup: DeviceEntityLookup + ): string | undefined { + const batteryEntity = (deviceEntityLookup[deviceId] || []).find( + (entity) => + this.hass.states[entity.entity_id] && + this.hass.states[entity.entity_id].attributes.device_class === "battery" + ); + + return batteryEntity ? batteryEntity.entity_id : undefined; + } + + private _fallbackDeviceName( + deviceId: string, + deviceEntityLookup: DeviceEntityLookup + ): string | undefined { + for (const entity of deviceEntityLookup[deviceId] || []) { + const stateObj = this.hass.states[entity.entity_id]; + if (stateObj) { + return computeStateName(stateObj); + } + } + + return undefined; + } + + private _handleRowClicked(ev: CustomEvent) { + const deviceId = (ev.detail as RowClickedEvent).id; + navigate(this, `/config/devices/device/${deviceId}`); + } +} + +declare global { + interface HTMLElementTagNameMap { + "ha-devices-data-table": HaDevicesDataTable; + } +} diff --git a/src/panels/config/integrations/config-entry/ha-ce-entities-card.js b/src/panels/config/integrations/config-entry/ha-ce-entities-card.js index 10d9e7f2bc..63cd18774d 100644 --- a/src/panels/config/integrations/config-entry/ha-ce-entities-card.js +++ b/src/panels/config/integrations/config-entry/ha-ce-entities-card.js @@ -20,7 +20,7 @@ class HaCeEntitiesCard extends LocalizeMixIn(EventsMixin(PolymerElement)) { return html`