mirror of
https://github.com/home-assistant/frontend.git
synced 2025-07-28 19:56:42 +00:00
Convert integration entry page to data table (#3963)
* Convert integration entry page to data table * Simplify device-card In a future PR this has to be changed further * Center no devices text * Review comments
This commit is contained in:
parent
fc3f7ca4b2
commit
320be2e5d9
@ -73,7 +73,7 @@ export interface DataTabelSortColumnData {
|
|||||||
export interface DataTabelColumnData extends DataTabelSortColumnData {
|
export interface DataTabelColumnData extends DataTabelSortColumnData {
|
||||||
title: string;
|
title: string;
|
||||||
type?: "numeric";
|
type?: "numeric";
|
||||||
template?: (data: any) => TemplateResult;
|
template?: (data: any, row: DataTabelRowData) => TemplateResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface DataTabelRowData {
|
export interface DataTabelRowData {
|
||||||
@ -254,7 +254,7 @@ export class HaDataTable extends BaseElement {
|
|||||||
})}"
|
})}"
|
||||||
>
|
>
|
||||||
${column.template
|
${column.template
|
||||||
? column.template(row[key])
|
? column.template(row[key], row)
|
||||||
: row[key]}
|
: row[key]}
|
||||||
</td>
|
</td>
|
||||||
`;
|
`;
|
||||||
|
@ -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 { html } from "@polymer/polymer/lib/utils/html-tag";
|
||||||
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
import { PolymerElement } from "@polymer/polymer/polymer-element";
|
||||||
|
|
||||||
import "../../../../components/ha-card";
|
import "../../../../components/ha-card";
|
||||||
import "../../../../layouts/hass-subpage";
|
|
||||||
|
|
||||||
import { EventsMixin } from "../../../../mixins/events-mixin";
|
import { EventsMixin } from "../../../../mixins/events-mixin";
|
||||||
import LocalizeMixin from "../../../../mixins/localize-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 { compare } from "../../../../common/string/compare";
|
||||||
import {
|
import { updateDeviceRegistryEntry } from "../../../../data/device_registry";
|
||||||
subscribeDeviceRegistry,
|
|
||||||
updateDeviceRegistryEntry,
|
|
||||||
} from "../../../../data/device_registry";
|
|
||||||
import { subscribeAreaRegistry } from "../../../../data/area_registry";
|
|
||||||
import {
|
import {
|
||||||
loadDeviceRegistryDetailDialog,
|
loadDeviceRegistryDetailDialog,
|
||||||
showDeviceRegistryDetailDialog,
|
showDeviceRegistryDetailDialog,
|
||||||
} from "../../../../dialogs/device-registry-detail/show-dialog-device-registry-detail";
|
} 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
|
* @appliesMixin EventsMixin
|
||||||
*/
|
*/
|
||||||
@ -37,10 +19,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
static get template() {
|
static get template() {
|
||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
:host(:not([narrow])) .device-entities {
|
|
||||||
max-height: 225px;
|
|
||||||
overflow: auto;
|
|
||||||
}
|
|
||||||
ha-card {
|
ha-card {
|
||||||
flex: 1 0 100%;
|
flex: 1 0 100%;
|
||||||
padding-bottom: 10px;
|
padding-bottom: 10px;
|
||||||
@ -70,11 +48,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
.extra-info {
|
.extra-info {
|
||||||
margin-top: 8px;
|
margin-top: 8px;
|
||||||
}
|
}
|
||||||
paper-icon-item {
|
|
||||||
cursor: pointer;
|
|
||||||
padding-top: 4px;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
.manuf,
|
.manuf,
|
||||||
.entity-id,
|
.entity-id,
|
||||||
.area {
|
.area {
|
||||||
@ -82,15 +55,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
<ha-card>
|
<ha-card>
|
||||||
<div class="card-header">
|
|
||||||
<template is="dom-if" if="[[!hideSettings]]">
|
|
||||||
<div class="name">[[_deviceName(device)]]</div>
|
|
||||||
<paper-icon-button
|
|
||||||
icon="hass:settings"
|
|
||||||
on-click="_gotoSettings"
|
|
||||||
></paper-icon-button>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
<div class="card-content">
|
<div class="card-content">
|
||||||
<div class="info">
|
<div class="info">
|
||||||
<div class="model">[[device.model]]</div>
|
<div class="model">[[device.model]]</div>
|
||||||
@ -122,27 +86,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<template is="dom-if" if="[[!hideEntities]]">
|
|
||||||
<div class="device-entities">
|
|
||||||
<template
|
|
||||||
is="dom-repeat"
|
|
||||||
items="[[_computeDeviceEntities(hass, device, entities)]]"
|
|
||||||
as="entity"
|
|
||||||
>
|
|
||||||
<paper-icon-item on-click="_openMoreInfo">
|
|
||||||
<state-badge
|
|
||||||
state-obj="[[_computeStateObj(entity, hass)]]"
|
|
||||||
slot="item-icon"
|
|
||||||
></state-badge>
|
|
||||||
<paper-item-body>
|
|
||||||
<div class="name">[[_computeEntityName(entity, hass)]]</div>
|
|
||||||
<div class="secondary entity-id">[[entity.entity_id]]</div>
|
|
||||||
</paper-item-body>
|
|
||||||
</paper-icon-item>
|
|
||||||
</template>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
</ha-card>
|
</ha-card>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@ -152,14 +95,11 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
device: Object,
|
device: Object,
|
||||||
devices: Array,
|
devices: Array,
|
||||||
areas: Array,
|
areas: Array,
|
||||||
entities: Array,
|
|
||||||
hass: Object,
|
hass: Object,
|
||||||
narrow: {
|
narrow: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
reflectToAttribute: true,
|
reflectToAttribute: true,
|
||||||
},
|
},
|
||||||
hideSettings: { type: Boolean, value: false },
|
|
||||||
hideEntities: { type: Boolean, value: false },
|
|
||||||
_childDevices: {
|
_childDevices: {
|
||||||
type: Array,
|
type: Array,
|
||||||
computed: "_computeChildDevices(device, devices)",
|
computed: "_computeChildDevices(device, devices)",
|
||||||
@ -172,30 +112,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
loadDeviceRegistryDetailDialog();
|
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) {
|
_computeArea(areas, device) {
|
||||||
if (!areas || !device || !device.area_id) {
|
if (!areas || !device || !device.area_id) {
|
||||||
return "No Area";
|
return "No Area";
|
||||||
@ -210,30 +126,6 @@ class HaDeviceCard extends EventsMixin(LocalizeMixin(PolymerElement)) {
|
|||||||
.sort((dev1, dev2) => compare(dev1.name, dev2.name));
|
.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) {
|
_deviceName(device) {
|
||||||
return device.name_by_user || device.name;
|
return device.name_by_user || device.name;
|
||||||
}
|
}
|
||||||
|
@ -13,6 +13,7 @@ import "../../../layouts/hass-subpage";
|
|||||||
import "../../../layouts/hass-error-screen";
|
import "../../../layouts/hass-error-screen";
|
||||||
import "../ha-config-section";
|
import "../ha-config-section";
|
||||||
|
|
||||||
|
import "./device-detail/ha-device-card";
|
||||||
import "./device-detail/ha-device-triggers-card";
|
import "./device-detail/ha-device-triggers-card";
|
||||||
import "./device-detail/ha-device-conditions-card";
|
import "./device-detail/ha-device-conditions-card";
|
||||||
import "./device-detail/ha-device-actions-card";
|
import "./device-detail/ha-device-actions-card";
|
||||||
@ -144,9 +145,6 @@ export class HaConfigDevicePage extends LitElement {
|
|||||||
.areas=${this.areas}
|
.areas=${this.areas}
|
||||||
.devices=${this.devices}
|
.devices=${this.devices}
|
||||||
.device=${device}
|
.device=${device}
|
||||||
.entities=${this.entities}
|
|
||||||
hide-settings
|
|
||||||
hide-entities
|
|
||||||
></ha-device-card>
|
></ha-device-card>
|
||||||
|
|
||||||
${entities.length
|
${entities.length
|
||||||
|
@ -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 "../../../layouts/hass-subpage";
|
||||||
import "../../../resources/ha-style";
|
import "./ha-devices-data-table";
|
||||||
import "../../../components/ha-icon-next";
|
|
||||||
|
|
||||||
import "../ha-config-section";
|
|
||||||
|
|
||||||
import memoizeOne from "memoize-one";
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
LitElement,
|
LitElement,
|
||||||
@ -21,33 +7,14 @@ import {
|
|||||||
TemplateResult,
|
TemplateResult,
|
||||||
property,
|
property,
|
||||||
customElement,
|
customElement,
|
||||||
|
CSSResult,
|
||||||
|
css,
|
||||||
} from "lit-element";
|
} from "lit-element";
|
||||||
import { HomeAssistant } from "../../../types";
|
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 { DeviceRegistryEntry } from "../../../data/device_registry";
|
||||||
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
import { EntityRegistryEntry } from "../../../data/entity_registry";
|
||||||
import { ConfigEntry } from "../../../data/config_entries";
|
import { ConfigEntry } from "../../../data/config_entries";
|
||||||
import { AreaRegistryEntry } from "../../../data/area_registry";
|
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")
|
@customElement("ha-config-devices-dashboard")
|
||||||
export class HaConfigDeviceDashboard extends LitElement {
|
export class HaConfigDeviceDashboard extends LitElement {
|
||||||
@ -59,234 +26,35 @@ export class HaConfigDeviceDashboard extends LitElement {
|
|||||||
@property() public areas!: AreaRegistryEntry[];
|
@property() public areas!: AreaRegistryEntry[];
|
||||||
@property() public domain!: string;
|
@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 || "<unknown>",
|
|
||||||
manufacturer: device.manufacturer || "<unknown>",
|
|
||||||
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}<br />
|
|
||||||
${device.area} | ${device.integration}<br />
|
|
||||||
${battery
|
|
||||||
? html`
|
|
||||||
${battery.state}%
|
|
||||||
<ha-state-icon
|
|
||||||
.hass=${this.hass!}
|
|
||||||
.stateObj=${battery}
|
|
||||||
></ha-state-icon>
|
|
||||||
`
|
|
||||||
: ""}
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
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}%
|
|
||||||
<ha-state-icon
|
|
||||||
.hass=${this.hass!}
|
|
||||||
.stateObj=${battery}
|
|
||||||
></ha-state-icon>
|
|
||||||
`
|
|
||||||
: html`
|
|
||||||
-
|
|
||||||
`;
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
protected render(): TemplateResult {
|
protected render(): TemplateResult {
|
||||||
return html`
|
return html`
|
||||||
<hass-subpage
|
<hass-subpage
|
||||||
header=${this.hass.localize("ui.panel.config.devices.caption")}
|
header=${this.hass.localize("ui.panel.config.devices.caption")}
|
||||||
>
|
>
|
||||||
<ha-data-table
|
<div class="content">
|
||||||
.columns=${this._columns(this.narrow)}
|
<ha-devices-data-table
|
||||||
.data=${this._devices(
|
.hass=${this.hass}
|
||||||
this.devices,
|
.narrow=${this.narrow}
|
||||||
this.entries,
|
.devices=${this.devices}
|
||||||
this.entities,
|
.entries=${this.entries}
|
||||||
this.areas,
|
.entities=${this.entities}
|
||||||
this.domain,
|
.areas=${this.areas}
|
||||||
this.hass.localize
|
.domain=${this.domain}
|
||||||
).map((device: DeviceRowData) => {
|
></ha-devices-data-table>
|
||||||
// We don't need a lot of this data for mobile view, but kept it for filtering...
|
</div>
|
||||||
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}
|
|
||||||
></ha-data-table>
|
|
||||||
</hass-subpage>
|
</hass-subpage>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _batteryEntity(
|
static get styles(): CSSResult {
|
||||||
deviceId: string,
|
return css`
|
||||||
deviceEntityLookup: DeviceEntityLookup
|
.content {
|
||||||
): string | undefined {
|
padding: 4px;
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
}
|
ha-devices-data-table {
|
||||||
|
width: 100%;
|
||||||
return undefined;
|
}
|
||||||
}
|
`;
|
||||||
|
|
||||||
private _handleRowClicked(ev: CustomEvent) {
|
|
||||||
const deviceId = (ev.detail as RowClickedEvent).id;
|
|
||||||
navigate(this, `/config/devices/device/${deviceId}`);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
265
src/panels/config/devices/ha-devices-data-table.ts
Normal file
265
src/panels/config/devices/ha-devices-data-table.ts
Normal file
@ -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 || "<unknown>",
|
||||||
|
manufacturer: device.manufacturer || "<unknown>",
|
||||||
|
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}<br />
|
||||||
|
${device.area} | ${device.integration}<br />
|
||||||
|
${battery
|
||||||
|
? html`
|
||||||
|
${battery.state}%
|
||||||
|
<ha-state-icon
|
||||||
|
.hass=${this.hass!}
|
||||||
|
.stateObj=${battery}
|
||||||
|
></ha-state-icon>
|
||||||
|
`
|
||||||
|
: ""}
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
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}%
|
||||||
|
<ha-state-icon
|
||||||
|
.hass=${this.hass!}
|
||||||
|
.stateObj=${battery}
|
||||||
|
></ha-state-icon>
|
||||||
|
`
|
||||||
|
: html`
|
||||||
|
-
|
||||||
|
`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
protected render(): TemplateResult {
|
||||||
|
return html`
|
||||||
|
<ha-data-table
|
||||||
|
.columns=${this._columns(this.narrow)}
|
||||||
|
.data=${this._devices(
|
||||||
|
this.devices,
|
||||||
|
this.entries,
|
||||||
|
this.entities,
|
||||||
|
this.areas,
|
||||||
|
this.domain,
|
||||||
|
this.hass.localize
|
||||||
|
)}
|
||||||
|
@row-click=${this._handleRowClicked}
|
||||||
|
></ha-data-table>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
}
|
@ -20,7 +20,7 @@ class HaCeEntitiesCard extends LocalizeMixIn(EventsMixin(PolymerElement)) {
|
|||||||
return html`
|
return html`
|
||||||
<style>
|
<style>
|
||||||
ha-card {
|
ha-card {
|
||||||
flex: 1 0 100%;
|
margin-top: 8px;
|
||||||
padding-bottom: 8px;
|
padding-bottom: 8px;
|
||||||
}
|
}
|
||||||
paper-icon-item {
|
paper-icon-item {
|
||||||
|
@ -2,10 +2,7 @@ import memoizeOne from "memoize-one";
|
|||||||
import "../../../../layouts/hass-subpage";
|
import "../../../../layouts/hass-subpage";
|
||||||
import "../../../../layouts/hass-error-screen";
|
import "../../../../layouts/hass-error-screen";
|
||||||
|
|
||||||
import "../../../../components/entity/state-badge";
|
import "../../devices/ha-devices-data-table";
|
||||||
import { compare } from "../../../../common/string/compare";
|
|
||||||
|
|
||||||
import "../../devices/device-detail/ha-device-card";
|
|
||||||
import "./ha-ce-entities-card";
|
import "./ha-ce-entities-card";
|
||||||
import { showOptionsFlowDialog } from "../../../../dialogs/config-flow/show-dialog-options-flow";
|
import { showOptionsFlowDialog } from "../../../../dialogs/config-flow/show-dialog-options-flow";
|
||||||
import { property, LitElement, CSSResult, css, html } from "lit-element";
|
import { property, LitElement, CSSResult, css, html } from "lit-element";
|
||||||
@ -43,15 +40,9 @@ class HaConfigEntryPage extends LitElement {
|
|||||||
if (!devices) {
|
if (!devices) {
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
return devices
|
return devices.filter((device) =>
|
||||||
.filter((device) =>
|
device.config_entries.includes(configEntry.entry_id)
|
||||||
device.config_entries.includes(configEntry.entry_id)
|
);
|
||||||
)
|
|
||||||
.sort(
|
|
||||||
(dev1, dev2) =>
|
|
||||||
Number(!!dev1.via_device_id) - Number(!!dev2.via_device_id) ||
|
|
||||||
compare(dev1.name || "", dev2.name || "")
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@ -116,24 +107,19 @@ class HaConfigEntryPage extends LitElement {
|
|||||||
)}
|
)}
|
||||||
</p>
|
</p>
|
||||||
`
|
`
|
||||||
: ""}
|
: html`
|
||||||
${configEntryDevices.map(
|
<ha-devices-data-table
|
||||||
(device) => html`
|
.hass=${this.hass}
|
||||||
<ha-device-card
|
.narrow=${this.narrow}
|
||||||
class="card"
|
.devices=${configEntryDevices}
|
||||||
.hass=${this.hass}
|
.entries=${this.configEntries}
|
||||||
.areas=${this.areas}
|
.entities=${this.entityRegistryEntries}
|
||||||
.devices=${this.deviceRegistryEntries}
|
.areas=${this.areas}
|
||||||
.device=${device}
|
></ha-devices-data-table>
|
||||||
.entities=${this.entityRegistryEntries}
|
`}
|
||||||
.narrow=${this.narrow}
|
|
||||||
></ha-device-card>
|
|
||||||
`
|
|
||||||
)}
|
|
||||||
${noDeviceEntities.length > 0
|
${noDeviceEntities.length > 0
|
||||||
? html`
|
? html`
|
||||||
<ha-ce-entities-card
|
<ha-ce-entities-card
|
||||||
class="card"
|
|
||||||
.heading=${this.hass.localize(
|
.heading=${this.hass.localize(
|
||||||
"ui.panel.config.integrations.config_entry.no_device"
|
"ui.panel.config.integrations.config_entry.no_device"
|
||||||
)}
|
)}
|
||||||
@ -185,18 +171,13 @@ class HaConfigEntryPage extends LitElement {
|
|||||||
static get styles(): CSSResult {
|
static get styles(): CSSResult {
|
||||||
return css`
|
return css`
|
||||||
.content {
|
.content {
|
||||||
display: flex;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 4px;
|
padding: 4px;
|
||||||
justify-content: center;
|
|
||||||
}
|
}
|
||||||
.card {
|
p {
|
||||||
box-sizing: border-box;
|
text-align: center;
|
||||||
display: flex;
|
}
|
||||||
flex: 1 0 300px;
|
ha-devices-data-table {
|
||||||
min-width: 0;
|
width: 100%;
|
||||||
max-width: 500px;
|
|
||||||
padding: 8px;
|
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user